Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions devlog/_plan/260905_fast_default_exports/000_plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Fast discovery and external exports

Class C3; one work phase (wp1), spec-satisfaction repair.
Trigger: Fast selectors require opt-in and do not reach pi/config exports.
Goal: default-on eligible Fast rows on discovery and every shared external export.
Non-goals: global fastMode changes, Ultra Fast, service deployment, new client protocols.
Verifier: GitHub Cross-platform CI on exact PR head; local tests and typecheck are prohibited by user. CI definitions in .github/workflows/ci.yml own runtime/typecheck checks. git diff --check observes the patch; no claim that it verifies behavior.
Stop: passing CI, independent review, authorized admin merge, fetched dev ancestry.
Memory: this unit and session-bound goalplan. Outcome DONE or evidence-backed external blockage.
Scope: existing repository credentials for branch push/PR/merge; no external account or service changes. Two-hour work phase, no paid external AI or additional resource spend. Subagents may inspect/audit and implement disjoint declared slices; parent reclaims after two distinct failed dispatches.

Existing structure: src/server/fast-row.ts owns selectors and canonical Fast eligibility; src/clients/config-export/ owns common metadata and client serializers; src/server/management/model-rows.ts and src/cli/opencode.ts own catalog projections. Reuse these boundaries; no dependency or UI changes. Source of truth: structure/09_client-integrations.md and docs-site configuration reference.

Omission means on; explicit false and malformed hand edits mean off. Native rows additionally require upstream speed-tier metadata. Real complete IDs win over synthetic selectors. Remote catalog authority must survive export without guessing from the local client config. Existing ordinary rows stay selectable.
18 changes: 18 additions & 0 deletions devlog/_plan/260905_fast_default_exports/010_implementation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# wp1 implementation

- MODIFY src/config.ts: fastRows optional/catch(false) -> default(true)/catch(false); seed getDefaultConfig true. MODIFY src/types/config.ts semantics.
- MODIFY src/server/fast-row.ts and src/server/index.ts: off iff === false, on iff !== false, across listing and ingress. Preserve grammar collision/eligibility logic. Add reusable catalog Fast availability predicate if needed to share native-tier and resolved routed eligibility with exports.
- MODIFY src/server/management/model-rows.ts: compute an optional boolean fastRowAvailable from live server config and row metadata, including native metadata slug; preserve it in toExportModel. No extra picker rows in management itself.
- MODIFY src/clients/config-export/contracts.ts, src/cli/opencode.ts and src/cli/export-command.ts: carry fastRowAvailable through each typed projection, including false; remote rows own their hub switch state.
- NEW src/clients/config-export/fast-models.ts (or shared metadata owner): expand eligible rows, preserve metadata, name Fast distinctly, reserve exact input IDs before synthesis, avoid repeated expansion, stable first-wins dedupe. Fallback to canonical local policy only if no resolved remote flag is supplied. Native fallback requires upstream advertised Fast.
- MODIFY config-export.ts and its omp/dsh/mcode/zcode serializers: use shared projection; OpenCode direct provider blocks use it too. Preserve pure normalizeExportModels compatibility if useful.
- MODIFY existing focused tests: omission/on/off/malformed config; parser/listing default; pi and every EXPORT_CLIENT_ID, OpenCode V1/V2 direct path, remote enabled/disabled with no local provider, duplicate/collision and metadata preservation. CI executes tests; no local suite or typecheck.
- MODIFY docs-site/src/content/docs/reference/configuration.md and structure/09_client-integrations.md: default true, false opt-out, supported export paths, refresh previously written client configs.

Activation scenarios: omitted flag publishes and parses Fast; explicit false preserves old rows; malformed flag disables without dropping providers; eligible provider publishes but ineligible does not; native needs upstream fast; real base--fast wins over synthesis; re-expansion adds no nested rows; remote false stays false even when local default on. CI plus independent static review is the acceptance gate.

Delegation: export-path explorer/auditor reads clients and callers; implementation worker may own config-export subtree plus CLI projections and their existing tests. Parent owns config/server/model-rows/default tests/docs. Reviews are read-only and run no suites.

## Audit amendment

Reviewer Huygens GO-WITH-FIXES identified old hubs without metadata and disabled real-ID collisions. Accept both. Freeze contract: `fastRowAvailable?: boolean` is resolved by the hub on EVERY management row. Export projection emits Fast ONLY when this field is true; no serializer-side local policy fallback at all. Missing metadata from old hubs conservatively means unavailable. Parent owns hub availability, using full management row IDs plus knownEffortRowIds BEFORE filtering disabled rows. Worker owns export contracts/CLI transports and a pure generic expansion helper. It changes namespaced/displayName only, preserves provider/id/native, marks synthesized rows fastRowAvailable:false for idempotence, and reserves all supplied exact IDs. Direct OpenCode launcher consumes the same hub projection, so does not need local inference. Tests cover hub/local conflicting flags, missing metadata and disabled complete-ID collisions.
13 changes: 13 additions & 0 deletions devlog/_plan/260905_fast_default_exports/011_review.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Review synthesis

A: Huygens GO-WITH-FIXES. Accepted old-hub metadata absence and disabled exact-ID collision blockers; final implementation uses explicit hub booleans only and checks complete IDs before filtering.
B: Mendel found one documentation contradiction (old export exclusion). Accepted and replaced the stale paragraph. No runtime/auth/secret blocker in parent scope. Worker Noether completed all shared serializers and CLI transport with no local tests. Parent preserved direct OpenCode ordering and corrected CLI emitted counts using serializer summaries.
Verification pending: exact-head CI and final independent export review. No local suite or typecheck executed.

C: Nietzsche independently reviewed the complete export slice at 13f8d0391. No blockers; accepted P3 launcher count correction so both CLI commands report expanded model counts. All twelve serializer paths, remote authority, collision/idempotence, metadata and auth preservation reviewed.

## C repair: suffix-shaped real model identities

CodeRabbit suggested suppressing every selector already ending in --fast. Independent adjudication by Nietzsche rebutted that blanket fix: fastRowBases seeds configured real IDs before its structural suffix refusal, and parsing strips exactly one suffix. Thus a configured model--fast legitimately has a model--fast--fast priority selector. Synthetic export rows already carry false availability, so repeated expansion is inert.

Accepted the narrower defect: live-only suffix-shaped bases are deliberately not recognized by the parser, but capability-only discovery could advertise them. Fix the shared listing/export eligibility predicate to reject suffix-shaped bases absent from fastRowBases, and test configured versus live-only base behavior plus all-client configured-suffix exports. Both raw discovery and management exports consume this predicate, avoiding divergent fixes. Previous 4ca1ec5ff full CI passed; this repair requires a new exact-head CI run.
17 changes: 10 additions & 7 deletions docs-site/src/content/docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,17 @@ a known configured model id. Cursor may require a model-list refresh or restart

### Fast rows

`fastRows` is an optional boolean and defaults to `false`. When enabled, the raw OpenAI-style
`/v1/models` list and Claude Code discovery add a `<base-id>--fast` selector for every model whose
`fastRows` is an optional boolean and defaults to `true`. The raw OpenAI-style
`/v1/models` list, Claude Code discovery, and client config exports (including pi, OpenCode,
OMP, Hermes, OpenClaw, Kimi, Gajae, DSH, MCode, ZCode, Prime, and Aside) add a `<base-id>--fast` selector for every model whose
resolved Fast policy is eligible. Selecting one routes the base model and requests the `priority`
service tier — the same Fast the Codex app exposes through its picker toggle. The base row stays
listed, so the row is an addition rather than a replacement.

The flag exists because Fast was otherwise reachable only from Codex. Codex reads the tier from
catalog metadata and renders a toggle; every other client selects a model by id alone, so a
Claude Code or OpenAI-compatible client had no way to ask for it.
Set `"fastRows": false` to hide generated Fast selectors. Malformed values also disable them.
Refresh the client model list or regenerate/refresh an existing managed client configuration to
receive the new entries. Connected clients use the serving proxy's availability metadata; older
proxies without that metadata do not gain guessed Fast entries. Codex keeps its native Fast toggle.

The suffix is `--fast`, with two hyphens, because a terminal `-fast` is already a real model id for
several providers (`grok-4-fast`, `glm-5.3-fast`, and Cursor's own fast variants), and a single
Expand All @@ -85,8 +87,9 @@ the two surfaces cannot disagree about which natives have Fast.

Scope: this covers the request-serving surfaces — `/v1/models`, Claude Code discovery, and the
`/v1/responses`, `/v1/chat/completions`, `/v1/messages`, `/v1/messages/count_tokens`, and
`/v1/responses/compact` endpoints. `ocx export` and the OpenCode integration emit base ids only,
because those identities are written into config files that outlive the flag.
`/v1/responses/compact` endpoints, plus `ocx export`, managed client integrations, and
the OpenCode launcher. After disabling Fast rows, refresh saved client configs and select a base
model instead of a previously saved Fast selector.

Valid values in `config.json` override built-in defaults. Missing optional fields use the defaults
documented on the domain pages. `OPENCODEX_HOME` takes precedence over the default configuration
Expand Down
12 changes: 3 additions & 9 deletions src/cli/export-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,6 @@ type ExportProxyModelRow = OpencodeProxyModelRow & {
defaultReasoningEffort?: string;
};

/** Same authoritativeness rule the serializers apply, for the degraded-count line. */
function hasContextLimit(model: ExportModel): boolean {
return typeof model.contextWindow === "number"
&& Number.isFinite(model.contextWindow)
&& model.contextWindow > 0;
}

/**
* Export rows from proxy `/api/models` rows.
*
Expand Down Expand Up @@ -106,6 +99,7 @@ export function exportModelsFromProxyRows(
id: entry.id ?? entry.namespaced,
};
if (entry.native) model.native = true;
if (entry.fastRowAvailable !== undefined) model.fastRowAvailable = entry.fastRowAvailable;
if (entry.displayName) model.displayName = entry.displayName;
if (entry.contextWindow !== undefined) model.contextWindow = entry.contextWindow;
if (entry.reasoningEfforts && entry.reasoningEfforts.length > 0) {
Expand Down Expand Up @@ -195,7 +189,7 @@ export async function handleExportCommand(argv: string[], deps: ExportCommandDep
// stderr, so `--json` stdout stays a standalone JSON document.
if (out !== undefined && wantsJson) console.error(`Wrote ${out}`);

const degraded = models.filter(model => !hasContextLimit(model)).length;
const { modelCount, modelsWithoutLimits } = spec.summarize(clientConfig);
// `--json` keeps emitting the DOCUMENT at the top level as JSON for scripts;
// `--out` is the path that writes the selected client's native format.
// Format metadata rides in the human lines below.
Expand All @@ -206,7 +200,7 @@ export async function handleExportCommand(argv: string[], deps: ExportCommandDep
`Destination: ${spec.destination(process.env)}`,
"Merge this generated configuration into that file; do not replace it.",
`Before launching: ${spec.exportHint}`,
`${models.length} model${models.length === 1 ? "" : "s"}; ${degraded} omit context limits (the client applies its own defaults).`,
`${modelCount} model${modelCount === 1 ? "" : "s"}; ${modelsWithoutLimits} omit context limits (the client applies its own defaults).`,
]);
});
}
Expand Down
5 changes: 4 additions & 1 deletion src/cli/opencode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ export interface OpencodeRoutedModel {

/** Row shape from authenticated GET /api/models on the running proxy. */
export interface OpencodeProxyModelRow {
/** Hub-resolved availability, independent of the launcher's local Fast setting. */
fastRowAvailable?: boolean;
provider?: string;
id?: string;
namespaced?: string;
Expand Down Expand Up @@ -389,6 +391,7 @@ export function opencodeCatalogFromProxyRows(
id: row.id,
contextWindow: row.contextWindow,
displayName: row.displayNameSource === "fallback" ? undefined : row.displayName,
...(typeof row.fastRowAvailable === "boolean" ? { fastRowAvailable: row.fastRowAvailable } : {}),
...(Array.isArray(row.reasoningEfforts) && row.reasoningEfforts.length > 0
? { reasoningEfforts: [...row.reasoningEfforts] }
: {}),
Expand Down Expand Up @@ -648,7 +651,7 @@ export async function cmdOpencode(args: string[]): Promise<number> {
const catalog = opencodeCatalogFromProxyRows(proxyModels, config);
const blocks = buildOpencodeProviderBlocksFromCatalog(live.port, catalog, live.hostname, config);
const baseUrl = blocks.v1.options.baseURL;
const modelCount = catalog.length;
const modelCount = Object.keys(blocks.v1.models).length;
console.error(`✅ opencode wired to ${baseUrl} — ${modelCount} model(s) under provider \`${OPENCODE_PROVIDER_ID}\`.`);
console.error(" Your existing opencode config files are left untouched; only the runtime provider blocks are injected.");
const providerOverride = opencodeProviderOverridePath(process.cwd());
Expand Down
4 changes: 2 additions & 2 deletions src/clients/config-export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { isAbsolute, join, resolve } from "node:path";
import { shouldInjectApiAuthHeader, standaloneCodexRoutingTarget } from "../codex/inject";
import { FORMAT_MEDIA_TYPE, serializeDocument, type ConfigFormat } from "../integrations/serialize";
import { canonicalizeReasoningEfforts } from "../reasoning-effort";
import { expandFastExportModels } from "./config-export/fast-models";
import { probeHostname } from "../server/proxy-liveness";
import type { OcxConfig } from "../types";

Expand Down Expand Up @@ -587,9 +588,8 @@ export function opencodeProviderBlocks(
): OpencodeProviderBlocks {
const v1Models: Record<string, OpencodeModelEntry> = {};
const v2Models: Record<string, OpencodeV2ModelEntry> = {};
for (const model of catalogModels) {
for (const model of expandFastExportModels(catalogModels)) {
const key = model.namespaced;
if (v1Models[key]) continue; // first entry wins; native rows lead /api/models
const entry: OpencodeModelEntry = { name: exportModelLabel(model) };
const context = authoritativeContextWindow(model.contextWindow);
if (context !== undefined) {
Expand Down
4 changes: 4 additions & 0 deletions src/clients/config-export/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export interface OpencodeLaunchEnv {
/** Visible catalog entry keyed by the proxy's canonical namespaced selector. */
export interface OpencodeCatalogModel {
namespaced: string;
/** Hub-resolved Fast availability. Missing metadata means unavailable. */
fastRowAvailable?: boolean;
native?: boolean;
provider?: string;
id?: string;
Expand All @@ -54,6 +56,8 @@ export interface OpencodeCatalogModel {
export interface ExportModel {
/** Canonical proxy selector: `provider/id`, or bare slug for native. */
namespaced: string;
/** Hub-resolved Fast availability; exporters never infer it from local config. */
fastRowAvailable?: boolean;
provider: string;
id: string;
/** Native OpenAI entry. Read by the shared label rule. */
Expand Down
29 changes: 29 additions & 0 deletions src/clients/config-export/fast-models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { OpencodeCatalogModel } from "./contracts";

const FAST_ROW_SUFFIX = "--fast";

/**
* Expand only hub-resolved availability; older hubs without the field advertise no Fast.
* Reserve every exact input selector before synthesis and retain the first duplicate.
* Keep routing/capability metadata intact: only the selector, label and availability change.
*/
export function expandFastExportModels<T extends OpencodeCatalogModel>(models: readonly T[]): T[] {
const exact = new Map<string, T>();
for (const model of models) {
if (!exact.has(model.namespaced)) exact.set(model.namespaced, model);
}
const expanded = [...exact.values()];
for (const model of exact.values()) {
if (model.fastRowAvailable !== true) continue;
const namespaced = `${model.namespaced}${FAST_ROW_SUFFIX}`;
Comment thread
lidge-jun marked this conversation as resolved.
if (exact.has(namespaced)) continue;
expanded.push({
...model,
namespaced,
displayName: `${model.displayName || model.id || model.namespaced} Fast`,
// Re-normalizing cannot synthesize a Fast row from this synthetic row.
fastRowAvailable: false,
});
}
return expanded;
}
15 changes: 5 additions & 10 deletions src/clients/config-export/model-metadata.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Shared client export model metadata.
import { SCHEMA_REQUIRED_OUTPUT_BUDGET } from "./constants";
import { expandFastExportModels } from "./fast-models";
import type { OpencodeCatalogModel, ExportModel, ExportClientId, ManagedContribution } from "./contracts";
import type { OcxConfig } from "../../types";
import { shouldInjectApiAuthHeader } from "../../codex/inject";
Expand Down Expand Up @@ -86,20 +87,14 @@ export function exportModelLabel(model: OpencodeCatalogModel): string {
}

/**
* Shared precondition for every serializer: drop duplicate `namespaced` (first wins,
* native rows lead `/api/models`) and sort by `namespaced` so two calls with the same
* Shared precondition for every serializer: expand hub-approved Fast rows, drop duplicate
* `namespaced` (first wins, native rows lead `/api/models`) and sort so calls with the same
* models produce identical bytes. Stability matters because the GUI shows a diffable
* preview and agents may checksum the payload.
*/
export function normalizeExportModels(models: readonly ExportModel[]): ExportModel[] {
const seen = new Set<string>();
const unique: ExportModel[] = [];
for (const model of models) {
if (seen.has(model.namespaced)) continue;
seen.add(model.namespaced);
unique.push(model);
}
return unique.sort((a, b) => (a.namespaced < b.namespaced ? -1 : a.namespaced > b.namespaced ? 1 : 0));
return expandFastExportModels(models)
.sort((a, b) => (a.namespaced < b.namespaced ? -1 : a.namespaced > b.namespaced ? 1 : 0));
}

/** Extra headers a non-loopback bind needs, or nothing on loopback. */
Expand Down
6 changes: 3 additions & 3 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1113,9 +1113,8 @@ const configSchema = z.object({
defaultModelAliases: z.boolean().optional(),
// Malformed hand edits disable this opt-in projection without rejecting providers.
cursorEffortRows: z.boolean().optional().catch(false),
// Same opt-in discipline: a malformed hand edit degrades to off rather than rejecting
// every provider.
fastRows: z.boolean().optional().catch(false),
// Fast selectors default on; malformed hand edits disable them without rejecting providers.
fastRows: z.boolean().default(true).catch(false),
// Ultra Fast is opt-in for the same reason and degrades the same way: a malformed hand
// edit turns the tier off rather than rejecting the config that carries it.
ultraFastTier: z.boolean().optional().catch(false),
Expand Down Expand Up @@ -3684,6 +3683,7 @@ export function getDefaultConfig(): OcxConfig {
return {
port: 10100,
emptyCompletionRetry: false,
fastRows: true,
managementUsageMaxReadBytes: 64 * 1024 * 1024,
appOwnedMemoryBudgetMb: DEFAULT_APP_OWNED_MEMORY_BUDGET_BYTES / (1024 * 1024),
// Fresh/re-initialized configs are already written in the current three-tier
Expand Down
Loading
Loading