From fbb04105a62b47f9cf5f9e9d5c82d7086f9d32e8 Mon Sep 17 00:00:00 2001 From: JUN Date: Sat, 12 Sep 2026 14:19:59 +0900 Subject: [PATCH 1/4] feat(providers): persist explicit per-model capability declarations Refs #3377. Add strict management writes, per-axis load recovery, exact model IDs, and declaration-preserving DTO/mutation paths. Upstream tier/video activation remains evidence-gated. --- .../docs/reference/configuration/providers.md | 6 ++ src/cli/provider.ts | 3 + src/codex/catalog/provider-fetch.ts | 1 + src/config.ts | 26 +++++ src/config/provider-validation.ts | 96 +++++++++++++++++++ src/server/auth-cors.ts | 4 + src/server/management/provider-routes.ts | 18 ++++ src/types.ts | 1 + src/types/provider.ts | 9 ++ structure/adapters/registry.md | 2 + structure/catalog.md | 2 + structure/clients/claude-desktop.md | 2 + structure/codex-home.md | 2 + structure/config.md | 4 + structure/data-planes/images.md | 2 + structure/data-planes/inbound-compat.md | 2 + structure/gui-and-management-api.md | 2 + structure/ops/docs-and-release.md | 2 + structure/ops/service-and-sidecars.md | 2 + structure/overview.md | 2 + structure/providers/openai-tiers.md | 2 + structure/providers/xai-grok.md | 2 + structure/runtime.md | 2 + structure/subagents.md | 2 + structure/transports/inventory.md | 2 + structure/transports/responses.md | 2 + structure/transports/streaming-health.md | 2 + .../codex-gather-authority.test.ts | 41 ++++++++ tests/config/config-load-degrade.test.ts | 34 +++++++ .../oauth-upsert-preserves-api-key.test.ts | 11 +++ .../management-provider-validation.test.ts | 43 +++++++++ 31 files changed, 331 insertions(+) diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index 23b7e5e92d..2e713e58d2 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -1043,3 +1043,9 @@ or expiry does not extend the history-recovery contract. Sender and recipient on routed Responses are context for the receiving model, not a new machine-readable routing protocol. Tool routing continues to use the existing collaboration contracts. + +### Per-model capability declarations + +`modelCapabilities` stores explicit declarations keyed by exact upstream model ID. IDs preserve case and must not contain surrounding whitespace. Each entry may contain `inputModalities` (`text`, `image`, `audio`, `video`), `contextTier` (`default`, `long_context`) and `video.processing` (`static`, `agentic`). These are operator declarations, not proof of provider support. Context-tier and video fields currently record intent only and do not activate upstream behavior or increase catalog windows. + +The raw provider editor and provider API expose this map. POST/PUT replace an explicitly supplied map and reject null entries. PATCH merges individual axes; null clears a map, model, axis or video processing value, while `{}` makes no change. Omitted provider overwrites preserve the existing map. Malformed hand-edited files retain valid independent axes and treat malformed explicit input modalities as text-only, with a diagnostic. diff --git a/src/cli/provider.ts b/src/cli/provider.ts index 47f23fee62..0f33ff98a5 100644 --- a/src/cli/provider.ts +++ b/src/cli/provider.ts @@ -224,6 +224,9 @@ async function handleAdd(args: string[]): Promise { } const existingProvider = config.providers[name]; + if (existingProvider?.modelCapabilities !== undefined && provConfig.modelCapabilities === undefined) { + provConfig.modelCapabilities = structuredClone(existingProvider.modelCapabilities); + } const { initializeProviderModelSelection } = await import("../providers/initial-model-selection"); initializeProviderModelSelection(name, provConfig, existingProvider, config); config.providers[name] = provConfig; diff --git a/src/codex/catalog/provider-fetch.ts b/src/codex/catalog/provider-fetch.ts index 68910ce0eb..ae03da9f0d 100644 --- a/src/codex/catalog/provider-fetch.ts +++ b/src/codex/catalog/provider-fetch.ts @@ -600,6 +600,7 @@ function providerCatalogFingerprint(name: string, prov: OcxProviderConfig): Reco maxOut: prov.modelMaxOutputTokens ?? null, autoCompact: prov.modelAutoCompactTokenLimits ?? null, inMod: prov.modelInputModalities ?? null, + capabilities: prov.modelCapabilities ?? null, re: prov.modelReasoningEfforts ?? null, defRe: prov.modelDefaultReasoningEfforts ?? null, rsSum: prov.modelSupportsReasoningSummaries ?? null, diff --git a/src/config.ts b/src/config.ts index 5e81a7e5f1..1bcd06de03 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,3 +1,4 @@ +import { modelCapabilitiesConfigError, mergeModelCapabilities, sanitizeModelCapabilitiesForLoad } from "./config/provider-validation"; import { createHash } from "node:crypto"; import { chmodSync, constants as fsConstants, copyFileSync, existsSync, linkSync, lstatSync, mkdirSync, readFileSync, truncateSync, unlinkSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; @@ -577,7 +578,13 @@ const modelPinnedEffortsSchema = z.unknown().superRefine((value, ctx) => { * Zod schema for one provider entry: known fields are validated strictly while unknown * fields pass through (preserved for runtime extensions). */ +const modelCapabilitiesSchema = z.unknown().superRefine((value, ctx) => { + const error = modelCapabilitiesConfigError(value); + if (error) ctx.addIssue({ code: "custom", message: error }); +}).transform(value => mergeModelCapabilities(undefined, value)); + const providerConfigSchema = z.object({ + modelCapabilities: modelCapabilitiesSchema.optional(), pinnedReasoningEffort: pinnedReasoningEffortSchema.optional(), modelPinnedReasoningEfforts: modelPinnedEffortsSchema.optional(), // Validated rather than left to passthrough: an unrecognized strategy would otherwise @@ -1907,6 +1914,23 @@ export function retryOn429PolicyConfigError(policy: unknown): string | null { * with a warning; strict rejection stays at the management/write boundary * (providerManagementConfigError). */ +function sanitizeCapabilityDeclarationsForLoad(parsed: unknown): void { + if (!parsed || typeof parsed !== "object") return; + const providers = (parsed as Record).providers; + if (!providers || typeof providers !== "object" || Array.isArray(providers)) return; + for (const [name, value] of Object.entries(providers)) { + if (!value || typeof value !== "object" || Array.isArray(value)) continue; + const provider = value as Record; + if (provider.modelCapabilities === undefined) continue; + if (modelCapabilitiesConfigError(provider.modelCapabilities) !== null) { + console.warn(`config.json provider ${JSON.stringify(redactSecretString(name))} has malformed modelCapabilities; retaining valid axes and restricting malformed input modalities to text`); + const repaired = sanitizeModelCapabilitiesForLoad(provider.modelCapabilities); + if (repaired) provider.modelCapabilities = repaired; + else delete provider.modelCapabilities; + } + } +} + function sanitizeModelCostsForLoad(parsed: unknown): void { if (!parsed || typeof parsed !== "object") return; const root = parsed as Record; @@ -2403,6 +2427,7 @@ export function loadConfig(): OcxConfig { sanitizeModelDisplayNamesForLoad(parsed); sanitizeRetryOn429ForLoad(parsed); sanitizeModelCostsForLoad(parsed); + sanitizeCapabilityDeclarationsForLoad(parsed); const result = configSchema.safeParse(parsed); if (result.success) { const config = normalizeApiKeyIds(result.data as OcxConfig); @@ -3027,6 +3052,7 @@ function configDiagnosticsFromRaw(raw: string): ConfigDiagnostics { sanitizeModelDisplayNamesForLoad(parsed); sanitizeRetryOn429ForLoad(parsed); sanitizeModelCostsForLoad(parsed); + sanitizeCapabilityDeclarationsForLoad(parsed); const result = configSchema.safeParse(parsed); if (result.success) { return validFileConfigDiagnostics(normalizeApiKeyIds(result.data as OcxConfig), parsed); diff --git a/src/config/provider-validation.ts b/src/config/provider-validation.ts index c1e60033e8..76e2f86b94 100644 --- a/src/config/provider-validation.ts +++ b/src/config/provider-validation.ts @@ -11,6 +11,7 @@ import { REASONING_SUMMARY_DELIVERY_VALUES, UPSTREAM_HTTP_VERSION_VALUES, type OcxProviderConfig, + type ModelCapabilities, } from "../types"; const HEADER_NAME_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/; @@ -295,3 +296,98 @@ export function modelAdapterRecordConfigError( } return null; } + + +function capabilityRecord(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value) + && [Object.prototype, null].includes(Object.getPrototypeOf(value)); +} + +/** Strict writes; only PATCH may carry deletion tombstones. Model IDs are exact. */ +export function modelCapabilitiesConfigError(value: unknown, allowTombstones = false): string | null { + if (value === undefined || (allowTombstones && value === null)) return null; + if (!capabilityRecord(value)) return "modelCapabilities must be a plain object"; + if (Object.keys(value).length > MODEL_DISCOVERY_MAX_MODELS) return "modelCapabilities has too many models"; + for (const [id, row] of Object.entries(value)) { + if (!isValidModelDiscoveryModelId(id) || ["__proto__", "prototype", "constructor"].includes(id)) { + return "modelCapabilities keys must be exact non-reserved model ids without surrounding whitespace"; + } + if (allowTombstones && row === null) continue; + if (!capabilityRecord(row)) return "modelCapabilities rows must be plain objects"; + for (const [axis, declaration] of Object.entries(row)) { + if (!["inputModalities", "contextTier", "video"].includes(axis)) return "modelCapabilities contains an unknown axis"; + if (allowTombstones && declaration === null) continue; + if (axis === "inputModalities") { + if (!Array.isArray(declaration) || declaration.length === 0 + || declaration.some(item => typeof item !== "string" || !["text", "image", "audio", "video"].includes(item))) { + return "modelCapabilities inputModalities must be a nonempty array of text, image, audio or video"; + } + } else if (axis === "contextTier") { + if (declaration !== "default" && declaration !== "long_context") return "modelCapabilities contextTier must be default or long_context"; + } else { + if (!capabilityRecord(declaration) || Object.keys(declaration).some(key => key !== "processing")) { + return "modelCapabilities video must be a plain object containing only processing"; + } + if (Object.hasOwn(declaration, "processing") && declaration.processing !== "static" && declaration.processing !== "agentic" + && !(allowTombstones && declaration.processing === null)) return "modelCapabilities video processing must be static or agentic"; + } + } + } + return null; +} + +/** Merge a validated patch without sharing nested objects with the live provider. */ +export function mergeModelCapabilities( + current: Record | undefined, + patch: unknown, +): Record | undefined { + if (patch === null) return undefined; + const next = Object.fromEntries(Object.entries(current ?? {}).map(([id, row]) => [id, structuredClone(row)])); + if (patch !== undefined) for (const [id, raw] of Object.entries(patch as Record | null>)) { + if (raw === null) { delete next[id]; continue; } + const row: ModelCapabilities = Object.hasOwn(next, id) ? next[id]! : {}; + for (const [axis, value] of Object.entries(raw)) { + if (axis === "inputModalities") { + if (value === null) delete row.inputModalities; + else row.inputModalities = [...(value as NonNullable)]; + } else if (axis === "contextTier") { + if (value === null) delete row.contextTier; + else row.contextTier = value as ModelCapabilities["contextTier"]; + } else if (axis === "video") { + if (value === null) delete row.video; + else { + const video = { ...(row.video ?? {}) }; + const change = value as { processing?: "static" | "agentic" | null }; + if (Object.hasOwn(change, "processing")) { + if (change.processing === null) delete video.processing; + else video.processing = change.processing; + } + if (Object.keys(video).length) row.video = video; + else delete row.video; + } + } + } + if (Object.keys(row).length) next[id] = row; + else delete next[id]; + } + return Object.keys(next).length ? next : undefined; +} + +/** Load-only repair preserves independent valid axes; malformed explicit modalities restrict to text. */ +export function sanitizeModelCapabilitiesForLoad(value: unknown): Record | undefined { + if (!capabilityRecord(value)) return undefined; + const rows: Record = Object.create(null); + for (const [id, raw] of Object.entries(value).slice(0, MODEL_DISCOVERY_MAX_MODELS)) { + if (!capabilityRecord(raw) || !isValidModelDiscoveryModelId(id) || ["__proto__", "prototype", "constructor"].includes(id)) continue; + const row: Record = {}; + for (const axis of ["inputModalities", "contextTier", "video"] as const) { + if (!Object.hasOwn(raw, axis)) continue; + const declaration = raw[axis]; + if (modelCapabilitiesConfigError({ [id]: { [axis]: declaration } }) === null) row[axis] = declaration; + else if (axis === "inputModalities") row.inputModalities = ["text"]; + } + const normalized = mergeModelCapabilities(undefined, { [id]: row }); + if (normalized?.[id]) rows[id] = normalized[id]; + } + return Object.keys(rows).length ? rows : undefined; +} diff --git a/src/server/auth-cors.ts b/src/server/auth-cors.ts index 0910698a0c..972b415f1f 100644 --- a/src/server/auth-cors.ts +++ b/src/server/auth-cors.ts @@ -1,3 +1,4 @@ +import { modelCapabilitiesConfigError } from "../config/provider-validation"; import { timingSafeEqual } from "node:crypto"; import { initialModelSelection } from "../providers/initial-model-selection"; import { extractAccountId } from "../oauth/chatgpt"; @@ -588,6 +589,8 @@ export function providerManagementConfigError(name: unknown, provider: unknown): return "provider must be a plain object"; } const raw = provider as Record; + const capabilitiesError = modelCapabilitiesConfigError(raw.modelCapabilities); + if (capabilitiesError) return capabilitiesError; const pinsError = providerReasoningPinsConfigError(raw); if (pinsError) return pinsError; for (const field of FORBIDDEN_PROVIDER_RUNTIME_FIELDS) { @@ -833,6 +836,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = { contextWindow: "editor", modelContextWindows: "editor", modelInputModalities: "editor", + modelCapabilities: "editor", modelMaxInputTokens: "runtime", modelAutoCompactTokenLimits: "editor", defaultMaxOutputTokens: "editor", diff --git a/src/server/management/provider-routes.ts b/src/server/management/provider-routes.ts index 6977a17783..863dbd79ed 100644 --- a/src/server/management/provider-routes.ts +++ b/src/server/management/provider-routes.ts @@ -1,3 +1,4 @@ +import { modelCapabilitiesConfigError, mergeModelCapabilities } from "../../config/provider-validation"; import { randomUUID } from "node:crypto"; import { readFileSync } from "node:fs"; import { isDeepStrictEqual } from "node:util"; @@ -287,6 +288,9 @@ function providerEditorCandidate( if (provider.modelPinnedReasoningEfforts !== undefined) { provider.modelPinnedReasoningEfforts = validated.config.providers[name]!.modelPinnedReasoningEfforts; } + const capabilities = validated.config.providers[name]!.modelCapabilities; + if (capabilities === undefined) delete provider.modelCapabilities; + else provider.modelCapabilities = capabilities; } return { ok: true, config: candidate, removedProviders }; } @@ -490,6 +494,14 @@ function applyProviderPatchFields( } touched = true; } + if (Object.hasOwn(rawBody, "modelCapabilities")) { + const error = modelCapabilitiesConfigError(rawBody.modelCapabilities, true); + if (error) return { error }; + const capabilities = mergeModelCapabilities(next.modelCapabilities, rawBody.modelCapabilities); + if (capabilities === undefined) delete next.modelCapabilities; + else next.modelCapabilities = capabilities; + touched = true; + } if (Object.hasOwn(rawBody, "modelContextWindows")) { const value = rawBody.modelContextWindows; if (value === null) { @@ -784,6 +796,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise; + /** Requested tier only; does not imply an upstream window or activate an unverified wire. */ + contextTier?: "default" | "long_context"; + video?: { processing?: "static" | "agentic" }; +} + export interface OcxProviderConfig { /** Optional short provider namespace used only at request/catalog presentation time. */ alias?: string; @@ -478,6 +486,7 @@ export interface OcxProviderConfig { modelContextWindows?: Record; /** Model-specific Codex catalog input modalities, e.g. ["text"] or ["text", "image"]. */ modelInputModalities?: Record; + modelCapabilities?: Record; /** Model-specific max input token limits. Values cap auto_compact_token_limit. */ modelMaxInputTokens?: Record; /** diff --git a/structure/adapters/registry.md b/structure/adapters/registry.md index a4dc21adbf..c321306fd1 100644 --- a/structure/adapters/registry.md +++ b/structure/adapters/registry.md @@ -66,3 +66,5 @@ see [Combo editor routing quota](../gui-and-management-api.md#combo-editor-routi Claude replay carries [Go conversation affinity](../data-planes/inbound-compat.md#claude-affinity-at-final-go-dispatch) privately to final dispatch; preliminary route selection does not inject Go-only headers. + +The [explicit model-capability contract](../config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior. diff --git a/structure/catalog.md b/structure/catalog.md index 0ba4acca3e..3913e08dd3 100644 --- a/structure/catalog.md +++ b/structure/catalog.md @@ -278,3 +278,5 @@ see [Combo editor routing quota](gui-and-management-api.md#combo-editor-routing- Claude replay carries [Go conversation affinity](data-planes/inbound-compat.md#claude-affinity-at-final-go-dispatch) privately to final dispatch; preliminary route selection does not inject Go-only headers. + +The [explicit model-capability contract](config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior. diff --git a/structure/clients/claude-desktop.md b/structure/clients/claude-desktop.md index 2914823958..bc988164fb 100644 --- a/structure/clients/claude-desktop.md +++ b/structure/clients/claude-desktop.md @@ -91,3 +91,5 @@ The explicit sync coordinator also accepts Cline CLI as a separate file integrat `claudeCode.stabilizePromptCache` is a default-off operator setting for [translated instruction stabilization](../data-planes/inbound-compat.md#opt-in-claude-instruction-stabilization). Config JSON preserves the boolean; only literal true activates the role-changing transform. + +The [explicit model-capability contract](../config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior. diff --git a/structure/codex-home.md b/structure/codex-home.md index b11ddd3f1a..8faf8f1c04 100644 --- a/structure/codex-home.md +++ b/structure/codex-home.md @@ -236,3 +236,5 @@ Injection preflights affected history using the normalized config candidate befo The legacy external writer is now refused for affected rows in any store whose schema includes history_mode, even while their row mode is still legacy. This deliberately sacrifices automatic relabeling on migration-capable stores rather than racing native conversion. Synchronous/asynchronous restore, inline journal restore, and direct config removal preserve all artifacts on the same refusal. Native restore preflight also checks manifest-owned targets whose rows already returned to `openai`, including interrupted restores. Preimage capture distinguishes absent files from unreadable artifacts and aborts before mutation when a complete snapshot cannot be read. + +The [explicit model-capability contract](config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior. diff --git a/structure/config.md b/structure/config.md index 80bb62bc73..00f763efae 100644 --- a/structure/config.md +++ b/structure/config.md @@ -205,3 +205,7 @@ The Cline client keeps connection settings and models in a separate native file `claudeCode.stabilizePromptCache` is a default-off operator setting for [translated instruction stabilization](data-planes/inbound-compat.md#opt-in-claude-instruction-stabilization). Config JSON preserves the boolean; only literal true activates the role-changing transform. + +## Explicit per-model capability declarations + +`modelCapabilities` on `src/types/provider.ts` stores exact model-ID entries with optional inputModalities, contextTier and video.processing axes. `src/config/provider-validation.ts` strictly validates writes and merges PATCH axes without sharing live objects; null map/model/axis/processing tombstones delete, while empty PATCH objects do nothing. Complete POST/PUT replacements reject tombstones. File reads retain valid axes; malformed explicit modalities restrict to text with a diagnostic. The two catalog writers receive explicit config and gather fingerprints include the map. This storage contract alone does not activate a context tier, advertise a larger window or enable video processing. diff --git a/structure/data-planes/images.md b/structure/data-planes/images.md index 25646c7de4..4ce8c300e7 100644 --- a/structure/data-planes/images.md +++ b/structure/data-planes/images.md @@ -79,3 +79,5 @@ see [Combo editor routing quota](../gui-and-management-api.md#combo-editor-routi Claude replay carries [Go conversation affinity](inbound-compat.md#claude-affinity-at-final-go-dispatch) privately to final dispatch; preliminary route selection does not inject Go-only headers. + +The [explicit model-capability contract](../config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior. diff --git a/structure/data-planes/inbound-compat.md b/structure/data-planes/inbound-compat.md index 2d17c11875..3065f7e212 100644 --- a/structure/data-planes/inbound-compat.md +++ b/structure/data-planes/inbound-compat.md @@ -128,3 +128,5 @@ changes prompt roles, not conversation identity, and cannot guarantee upstream c Instruction notice extraction scans fence ranges once and walks original lines backwards with a decreasing cursor. It accepts exactly one ASCII space inside the token notice, preserves unmatched prefix bytes, and does not repeatedly scan or copy shrinking prompt prefixes. + +The [explicit model-capability contract](../config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior. diff --git a/structure/gui-and-management-api.md b/structure/gui-and-management-api.md index 73090d646e..3352286201 100644 --- a/structure/gui-and-management-api.md +++ b/structure/gui-and-management-api.md @@ -543,3 +543,5 @@ integration IO adapter. Its snapshot fingerprint cannot be checked against provi [the integration contract](clients/integrations.md#cline-paired-files) defines recovery. The existing dashboard file-client maps include Cline CLI and reuse its committed color mark. The export panel labels its download as a settings/catalog bundle; all locales explain that Undo restores both original files. + +The [explicit model-capability contract](config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior. diff --git a/structure/ops/docs-and-release.md b/structure/ops/docs-and-release.md index 827540194a..232a57e650 100644 --- a/structure/ops/docs-and-release.md +++ b/structure/ops/docs-and-release.md @@ -314,3 +314,5 @@ see [Combo editor routing quota](../gui-and-management-api.md#combo-editor-routi `src/codex/history-provider.ts` refuses external writes to paginated or migration-capable history. `src/codex/inject.ts` checks affected rows and manifest-owned restore targets before artifact changes and compensates detected migration. Failed config restore stops later catalog/history work. See the [history writer contract](../codex-home.md#paginated-history-writer-boundary) for guarantees and concurrent-writer limits. The integrations guide documents Cline CLI as a two-file, loopback-only integration. Hosted CI validates its source-backed fixtures; the packaged dashboard exposes it through the existing client list. + +The [explicit model-capability contract](../config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior. diff --git a/structure/ops/service-and-sidecars.md b/structure/ops/service-and-sidecars.md index 39dc9a82da..cd4e35d6ca 100644 --- a/structure/ops/service-and-sidecars.md +++ b/structure/ops/service-and-sidecars.md @@ -142,3 +142,5 @@ see [Combo editor routing quota](../gui-and-management-api.md#combo-editor-routi Claude replay carries [Go conversation affinity](../data-planes/inbound-compat.md#claude-affinity-at-final-go-dispatch) privately to final dispatch; preliminary route selection does not inject Go-only headers. + +The [explicit model-capability contract](../config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior. diff --git a/structure/overview.md b/structure/overview.md index d5d1a2207a..c378aafe89 100644 --- a/structure/overview.md +++ b/structure/overview.md @@ -108,3 +108,5 @@ The management quota DTO keeps Combo editing aligned with scoped inference evide see [Combo editor routing quota](gui-and-management-api.md#combo-editor-routing-quota). Cline CLI is a managed file integration: its provider settings and catalog share one recoverable journal operation. The [paired-file contract](clients/integrations.md#cline-paired-files) defines its stop/restart requirement. + +The [explicit model-capability contract](config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior. diff --git a/structure/providers/openai-tiers.md b/structure/providers/openai-tiers.md index a44edff557..3cbacac96b 100644 --- a/structure/providers/openai-tiers.md +++ b/structure/providers/openai-tiers.md @@ -402,3 +402,5 @@ successful main usage refresh clears the runtime mark. ## Paginated history writer boundary `src/codex/history-provider.ts` refuses external writes to paginated or migration-capable history. `src/codex/inject.ts` checks affected rows and manifest-owned restore targets before artifact changes and compensates detected migration. Failed config restore stops later catalog/history work. See the [history writer contract](../codex-home.md#paginated-history-writer-boundary) for guarantees and concurrent-writer limits. + +The [explicit model-capability contract](../config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior. diff --git a/structure/providers/xai-grok.md b/structure/providers/xai-grok.md index 5b149ac6a2..5555e3f158 100644 --- a/structure/providers/xai-grok.md +++ b/structure/providers/xai-grok.md @@ -65,3 +65,5 @@ see [Combo editor routing quota](../gui-and-management-api.md#combo-editor-routi Claude replay carries [Go conversation affinity](../data-planes/inbound-compat.md#claude-affinity-at-final-go-dispatch) privately to final dispatch; preliminary route selection does not inject Go-only headers. + +The [explicit model-capability contract](../config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior. diff --git a/structure/runtime.md b/structure/runtime.md index 522e5cabb9..bb175079a4 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -225,3 +225,5 @@ Cline CLI joins the existing export/client integration registries. Explicit CLI `claudeCode.stabilizePromptCache` is a default-off operator setting for [translated instruction stabilization](data-planes/inbound-compat.md#opt-in-claude-instruction-stabilization). Config JSON preserves the boolean; only literal true activates the role-changing transform. + +The [explicit model-capability contract](config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior. diff --git a/structure/subagents.md b/structure/subagents.md index f190aab084..e60d77a76c 100644 --- a/structure/subagents.md +++ b/structure/subagents.md @@ -214,3 +214,5 @@ see [Combo editor routing quota](gui-and-management-api.md#combo-editor-routing- Claude replay carries [Go conversation affinity](data-planes/inbound-compat.md#claude-affinity-at-final-go-dispatch) privately to final dispatch; preliminary route selection does not inject Go-only headers. + +The [explicit model-capability contract](config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior. diff --git a/structure/transports/inventory.md b/structure/transports/inventory.md index b2fc3b3fae..96a724e4fd 100644 --- a/structure/transports/inventory.md +++ b/structure/transports/inventory.md @@ -70,3 +70,5 @@ see [Combo editor routing quota](../gui-and-management-api.md#combo-editor-routi Claude replay carries [Go conversation affinity](../data-planes/inbound-compat.md#claude-affinity-at-final-go-dispatch) privately to final dispatch; preliminary route selection does not inject Go-only headers. + +The [explicit model-capability contract](../config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior. diff --git a/structure/transports/responses.md b/structure/transports/responses.md index 2321d78dd6..8034aeb76a 100644 --- a/structure/transports/responses.md +++ b/structure/transports/responses.md @@ -523,3 +523,5 @@ see [Combo editor routing quota](../gui-and-management-api.md#combo-editor-routi Claude replay carries [Go conversation affinity](../data-planes/inbound-compat.md#claude-affinity-at-final-go-dispatch) privately to final dispatch; preliminary route selection does not inject Go-only headers. + +The [explicit model-capability contract](../config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior. diff --git a/structure/transports/streaming-health.md b/structure/transports/streaming-health.md index 68093843ea..d63b97bb6b 100644 --- a/structure/transports/streaming-health.md +++ b/structure/transports/streaming-health.md @@ -199,3 +199,5 @@ see [Combo editor routing quota](../gui-and-management-api.md#combo-editor-routi Claude replay carries [Go conversation affinity](../data-planes/inbound-compat.md#claude-affinity-at-final-go-dispatch) privately to final dispatch; preliminary route selection does not inject Go-only headers. + +The [explicit model-capability contract](../config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior. diff --git a/tests/codex-integration/codex-gather-authority.test.ts b/tests/codex-integration/codex-gather-authority.test.ts index ae3204a0a7..5e8f287d01 100644 --- a/tests/codex-integration/codex-gather-authority.test.ts +++ b/tests/codex-integration/codex-gather-authority.test.ts @@ -365,3 +365,44 @@ describe("catalog gather discovery-policy authority", () => { } }); }); + + +test("overlapping gathers with different explicit capability declarations stay isolated", async () => { + clearModelCache("together"); + clearGatherRoutedModelsInflight(); + const arrived = [deferred(), deferred()]; + const release = deferred(); + let count = 0; + globalThis.fetch = (async () => { + const index = count++; + arrived[index]?.resolve(); + await release.promise; + return Response.json({ data: [{ id: `cap-model-${index}` }] }); + }) as typeof fetch; + const a = togetherConfig(); + const b = togetherConfig(); + a.providers.together!.modelCapabilities = { model: { contextTier: "default" } }; + b.providers.together!.modelCapabilities = { model: { contextTier: "long_context" } }; + const first = gatherRoutedModels(a); + let second: ReturnType | undefined; + try { + await arrived[0]!.promise; + second = gatherRoutedModels(b); + let timeout: ReturnType | undefined; + try { + await Promise.race([arrived[1]!.promise, new Promise((_resolve, reject) => { + timeout = setTimeout(() => reject(new Error("second capability gather joined the first flight")), 10_000); + })]); + } finally { + if (timeout !== undefined) clearTimeout(timeout); + } + expect(count).toBe(2); + release.resolve(); + const [firstRows, secondRows] = await Promise.all([first, second]); + expect(firstRows.some(row => row.id === "cap-model-0")).toBe(true); + expect(secondRows.some(row => row.id === "cap-model-1")).toBe(true); + } finally { + release.resolve(); + await Promise.allSettled([first, ...(second ? [second] : [])]); + } +}, 20_000); diff --git a/tests/config/config-load-degrade.test.ts b/tests/config/config-load-degrade.test.ts index 4d8cca68f7..93a36e845d 100644 --- a/tests/config/config-load-degrade.test.ts +++ b/tests/config/config-load-degrade.test.ts @@ -139,3 +139,37 @@ test("Fast rows default on for fresh and omitted config; explicit false and malf expect(loaded.providers.xai.note).toBe("keep me"); } }); + + +test("model capability writes stay strict while load preserves independent restrictions", () => { + const raw = { ...candidate(undefined), providers: { xai: { + ...candidate(undefined).providers.xai, + apiKey: "fixture-key", modelCapabilities: { + ModelA: { inputModalities: ["text"] }, + modela: { contextTier: "long_context" }, + broken: { inputModalities: "image", contextTier: "typo" }, + }, + } } }; + expect(validateConfigCandidate(raw).ok).toBe(false); + writeFileSync(getConfigPath(), JSON.stringify(raw), "utf8"); + const loaded = loadConfig(); + expect(loaded.providers.xai.modelCapabilities).toEqual({ + ModelA: { inputModalities: ["text"] }, modela: { contextTier: "long_context" }, + broken: { inputModalities: ["text"] }, + }); + expect(loaded.providers.xai.apiKey).toBe("fixture-key"); + expect(validateConfigCandidate(loaded).ok).toBe(true); +}); + +test("model capabilities round-trip all explicit axes without expanding inference", () => { + const raw = { ...candidate(undefined), providers: { xai: { + ...candidate(undefined).providers.xai, + modelCapabilities: { ModelA: { inputModalities: ["text", "image"], contextTier: "long_context", video: { processing: "agentic" } } }, + } } }; + const validated = validateConfigCandidate(raw); + expect(validated.ok).toBe(true); + if (!validated.ok) return; + saveConfig(validated.config); + expect(loadConfig().providers.xai.modelCapabilities).toEqual(raw.providers.xai.modelCapabilities); + expect(loadConfig().providers.xai.modelContextWindows).toBeUndefined(); +}); diff --git a/tests/oauth/oauth-upsert-preserves-api-key.test.ts b/tests/oauth/oauth-upsert-preserves-api-key.test.ts index a22239dd69..710089a7ad 100644 --- a/tests/oauth/oauth-upsert-preserves-api-key.test.ts +++ b/tests/oauth/oauth-upsert-preserves-api-key.test.ts @@ -582,3 +582,14 @@ describe("upsertOAuthProvider credential preservation", () => { expect(config.providers["command-code"]!.note).toBe("operator-note"); }); }); + + +test("OAuth upsert preserves explicit per-model capabilities", () => { + const config: OcxConfig = { port: 10100, defaultProvider: "xai", providers: { xai: { + adapter: "openai-responses", baseUrl: "https://api.x.ai/v1", + modelCapabilities: { "grok-4.6": { inputModalities: ["text"], contextTier: "default", video: { processing: "static" } } }, + } } }; + const before = structuredClone(config.providers.xai!.modelCapabilities); + upsertOAuthProvider(config, "xai"); + expect(config.providers.xai!.modelCapabilities).toEqual(before); +}); diff --git a/tests/server/management-provider-validation.test.ts b/tests/server/management-provider-validation.test.ts index 949791f2d6..c9d3832bca 100644 --- a/tests/server/management-provider-validation.test.ts +++ b/tests/server/management-provider-validation.test.ts @@ -5005,3 +5005,46 @@ describe("remembered provider context selections", () => { } }); }); + + +test("model capability PATCH merges axes while strict replacement and DTO state agree", async () => { + mkdirSync(TEST_DIR, { recursive: true }); + process.env.OPENCODEX_HOME = TEST_DIR; + const live: OcxConfig = { port: 0, defaultProvider: "caps", providers: { caps: { + adapter: "openai-chat", baseUrl: "https://example.test/v1", liveModels: false, models: ["ModelA", "modela"], + modelCapabilities: { ModelA: { inputModalities: ["text"], contextTier: "long_context", video: { processing: "agentic" } }, modela: { inputModalities: ["text", "image"] } }, + } } }; + saveConfig(live); + const request = async (method: string, body?: unknown) => { + const url = new URL(method === "PATCH" ? "http://localhost/api/providers?name=caps" : "http://localhost/api/providers"); + return (await handleManagementAPI(new Request(url, { method, headers: { "content-type": "application/json" }, + ...(body === undefined ? {} : { body: JSON.stringify(body) }), + }), url, live, { createManagementConvergeCodex: catalogConvergenceFactory() }))!; + }; + const before = structuredClone(live.providers.caps!.modelCapabilities); + expect((await request("PATCH", { modelCapabilities: { ModelA: { contextTier: "default", video: { processing: null } } } })).status).toBe(200); + expect(live.providers.caps!.modelCapabilities).toEqual({ + ModelA: { inputModalities: ["text"], contextTier: "default" }, modela: { inputModalities: ["text", "image"] }, + }); + expect(before!.ModelA!.video).toEqual({ processing: "agentic" }); + expect(loadConfig().providers.caps!.modelCapabilities).toEqual(live.providers.caps!.modelCapabilities); + const listed = await (await request("GET")).json() as Array<{ name: string; modelCapabilities?: unknown }>; + expect(listed.find(row => row.name === "caps")!.modelCapabilities).toEqual(live.providers.caps!.modelCapabilities); + expect(providerEditorConfigDTO(live).providers.caps!.modelCapabilities).toEqual(live.providers.caps!.modelCapabilities); + for (const patch of [{ " ModelA ": {} }, { ModelA: { unknown: true } }, { ModelA: { inputModalities: [] } }]) { + expect((await request("PATCH", { modelCapabilities: patch })).status).toBe(400); + } + const baseline = providerEditorConfigDTO(loadConfig()); + const invalidNext = structuredClone(baseline); + Object.assign(invalidNext.providers.caps!, { modelCapabilities: null }); + expect((await request("PUT", { baseline, next: invalidNext })).status).toBe(400); + const next = structuredClone(baseline); + next.providers.caps!.modelCapabilities = {}; + expect((await request("PUT", { baseline, next })).status).toBe(200); + expect(live.providers.caps!.modelCapabilities).toBeUndefined(); + expect(loadConfig().providers.caps!.modelCapabilities).toBeUndefined(); + const newBaseline = providerEditorConfigDTO(loadConfig()); + const followup = structuredClone(newBaseline); + followup.providers.caps!.note = "fresh baseline"; + expect((await request("PUT", { baseline: newBaseline, next: followup })).status).toBe(200); +}); From 2617fd157f058503a025e55ef5c523ecea8486d3 Mon Sep 17 00:00:00 2001 From: JUN Date: Sat, 12 Sep 2026 14:20:47 +0900 Subject: [PATCH 2/4] docs(config): keep schema and load comments beside their owners --- src/config.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/config.ts b/src/config.ts index 1bcd06de03..b7a3fc145a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -574,15 +574,15 @@ const modelPinnedEffortsSchema = z.unknown().superRefine((value, ctx) => { Object.entries(value as Record).map(([key, effort]) => [key.trim(), effort]), )); -/** - * Zod schema for one provider entry: known fields are validated strictly while unknown - * fields pass through (preserved for runtime extensions). - */ const modelCapabilitiesSchema = z.unknown().superRefine((value, ctx) => { const error = modelCapabilitiesConfigError(value); if (error) ctx.addIssue({ code: "custom", message: error }); }).transform(value => mergeModelCapabilities(undefined, value)); +/** + * Zod schema for one provider entry: known fields are validated strictly while unknown + * fields pass through (preserved for runtime extensions). + */ const providerConfigSchema = z.object({ modelCapabilities: modelCapabilitiesSchema.optional(), pinnedReasoningEffort: pinnedReasoningEffortSchema.optional(), @@ -1905,15 +1905,6 @@ export function retryOn429PolicyConfigError(policy: unknown): string | null { return `retryOn429.${field} is invalid (${first.message})`; } -/** - * Load-time degradation for `providers..modelCosts`, mirroring - * {@link sanitizeRetryOn429ForLoad}. A hand-edited malformed display-price row - * must not fail the whole config parse — that would back up config.json and - * fall back to defaults, dropping otherwise valid providers and the default - * route for a typo in a non-runtime display field. Invalid rows are dropped - * with a warning; strict rejection stays at the management/write boundary - * (providerManagementConfigError). - */ function sanitizeCapabilityDeclarationsForLoad(parsed: unknown): void { if (!parsed || typeof parsed !== "object") return; const providers = (parsed as Record).providers; @@ -1931,6 +1922,15 @@ function sanitizeCapabilityDeclarationsForLoad(parsed: unknown): void { } } +/** + * Load-time degradation for `providers..modelCosts`, mirroring + * {@link sanitizeRetryOn429ForLoad}. A hand-edited malformed display-price row + * must not fail the whole config parse — that would back up config.json and + * fall back to defaults, dropping otherwise valid providers and the default + * route for a typo in a non-runtime display field. Invalid rows are dropped + * with a warning; strict rejection stays at the management/write boundary + * (providerManagementConfigError). + */ function sanitizeModelCostsForLoad(parsed: unknown): void { if (!parsed || typeof parsed !== "object") return; const root = parsed as Record; From d1dee687ff627bb3d106c474b39d45b91d68f662 Mon Sep 17 00:00:00 2001 From: JUN Date: Sat, 12 Sep 2026 14:23:18 +0900 Subject: [PATCH 3/4] test(providers): cover capability mutation isolation and overwrite preservation --- tests/cli/cli-provider.test.ts | 13 +++++++ .../management-provider-validation.test.ts | 38 ++++++++++++++++++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/tests/cli/cli-provider.test.ts b/tests/cli/cli-provider.test.ts index ea29c5535f..b37bf973ed 100644 --- a/tests/cli/cli-provider.test.ts +++ b/tests/cli/cli-provider.test.ts @@ -644,3 +644,16 @@ describe("ocx provider add --sync", () => { } }); }); + + +test("provider add --force preserves all explicit model capability axes", () => { + const declarations = { ModelA: { inputModalities: ["text"], contextTier: "long_context", video: { processing: "agentic" } }, modela: { inputModalities: ["text", "image"] } }; + const { dir } = freshConfig({ defaultProvider: "caps", providers: { caps: { + adapter: "openai-chat", baseUrl: "https://example.test/v1", modelCapabilities: declarations, + } } }); + try { + const result = runCli(["provider", "add", "caps", "--adapter", "openai-chat", "--base-url", "https://example.test/v1", "--force", "--json"], { OPENCODEX_HOME: dir }); + expect(result.status, result.stderr).toBe(0); + expect(readConfig(dir).providers.caps.modelCapabilities).toEqual(declarations); + } finally { removeTreeWithRetry(dir); } +}); diff --git a/tests/server/management-provider-validation.test.ts b/tests/server/management-provider-validation.test.ts index c9d3832bca..dc69f46b79 100644 --- a/tests/server/management-provider-validation.test.ts +++ b/tests/server/management-provider-validation.test.ts @@ -5021,12 +5021,18 @@ test("model capability PATCH merges axes while strict replacement and DTO state ...(body === undefined ? {} : { body: JSON.stringify(body) }), }), url, live, { createManagementConvergeCodex: catalogConvergenceFactory() }))!; }; - const before = structuredClone(live.providers.caps!.modelCapabilities); + const before = live.providers.caps!.modelCapabilities; + const originalRow = before!.ModelA!; + const originalVideo = originalRow.video; expect((await request("PATCH", { modelCapabilities: { ModelA: { contextTier: "default", video: { processing: null } } } })).status).toBe(200); expect(live.providers.caps!.modelCapabilities).toEqual({ ModelA: { inputModalities: ["text"], contextTier: "default" }, modela: { inputModalities: ["text", "image"] }, }); expect(before!.ModelA!.video).toEqual({ processing: "agentic" }); + expect(originalRow.contextTier).toBe("long_context"); + expect(originalVideo).toEqual({ processing: "agentic" }); + expect(live.providers.caps!.modelCapabilities).not.toBe(before); + expect(live.providers.caps!.modelCapabilities!.ModelA).not.toBe(originalRow); expect(loadConfig().providers.caps!.modelCapabilities).toEqual(live.providers.caps!.modelCapabilities); const listed = await (await request("GET")).json() as Array<{ name: string; modelCapabilities?: unknown }>; expect(listed.find(row => row.name === "caps")!.modelCapabilities).toEqual(live.providers.caps!.modelCapabilities); @@ -5034,6 +5040,36 @@ test("model capability PATCH merges axes while strict replacement and DTO state for (const patch of [{ " ModelA ": {} }, { ModelA: { unknown: true } }, { ModelA: { inputModalities: [] } }]) { expect((await request("PATCH", { modelCapabilities: patch })).status).toBe(400); } + const admitted = structuredClone(live.providers.caps!.modelCapabilities); + const diskBeforeReject = readFileSync(join(TEST_DIR, "config.json"), "utf8"); + for (const patch of [JSON.parse('{"__proto__":null}'), { ModelA: { video: [] } }, { ModelA: { video: { processing: "invalid" } } }]) { + expect((await request("PATCH", { modelCapabilities: patch })).status).toBe(400); + expect(live.providers.caps!.modelCapabilities).toEqual(admitted); + expect(readFileSync(join(TEST_DIR, "config.json"), "utf8")).toBe(diskBeforeReject); + } + const resolved = spyOn(destinationPolicy, "providerDestinationResolvedError").mockResolvedValue(null); + try { + const replacement = { adapter: "openai-chat", baseUrl: "https://example.test/v1", liveModels: false, models: ["ModelA", "modela"] }; + expect((await request("POST", { name: "caps", provider: replacement })).status).toBe(200); + expect(live.providers.caps!.modelCapabilities).toEqual(admitted); + expect((await request("POST", { name: "caps", provider: { ...replacement, modelCapabilities: {} } })).status).toBe(200); + expect(live.providers.caps!.modelCapabilities).toBeUndefined(); + expect((await request("PATCH", { modelCapabilities: admitted })).status).toBe(200); + const stale = providerEditorConfigDTO(loadConfig()); + expect((await request("PATCH", { modelCapabilities: { ModelA: { contextTier: null } } })).status).toBe(200); + expect(live.providers.caps!.modelCapabilities!.ModelA).toEqual({ inputModalities: ["text"] }); + expect(live.providers.caps!.modelCapabilities!.modela).toEqual({ inputModalities: ["text", "image"] }); + const staleEdit = structuredClone(stale); + staleEdit.providers.caps!.note = "stale"; + expect((await request("PUT", { baseline: stale, next: staleEdit })).status).toBe(409); + expect((await request("PATCH", { modelCapabilities: { ModelA: null } })).status).toBe(200); + expect(live.providers.caps!.modelCapabilities).toEqual({ modela: { inputModalities: ["text", "image"] } }); + expect((await request("PATCH", { modelCapabilities: null })).status).toBe(200); + expect(live.providers.caps!.modelCapabilities).toBeUndefined(); + expect((await request("PATCH", { modelCapabilities: admitted })).status).toBe(200); + } finally { + resolved.mockRestore(); + } const baseline = providerEditorConfigDTO(loadConfig()); const invalidNext = structuredClone(baseline); Object.assign(invalidNext.providers.caps!, { modelCapabilities: null }); From fee142cd69444ba7c8efdae1f4883609beefb2b0 Mon Sep 17 00:00:00 2001 From: JUN Date: Sun, 13 Sep 2026 10:59:43 +0900 Subject: [PATCH 4/4] [skip ci] docs(structure): keep stack notes in their own sections The dev merge appended this layer's cross-reference notes after sections origin/dev had added, so they rendered under Context relay ownership, the OAuth Fast Tier section, and the capability section. Move them back beside the prose they describe; no wording changes. --- structure/config.md | 4 ++-- structure/providers/openai-tiers.md | 4 ++-- structure/providers/xai-grok.md | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/structure/config.md b/structure/config.md index 2d225d7228..e72d06bfe6 100644 --- a/structure/config.md +++ b/structure/config.md @@ -213,8 +213,8 @@ The Cline client keeps connection settings and models in a separate native file [translated instruction stabilization](data-planes/inbound-compat.md#opt-in-claude-instruction-stabilization). Config JSON preserves the boolean; only literal true activates the role-changing transform. +The lightweight top-level CLI help counts Cline CLI among the fifteen registered export clients; registry parity remains covered by the client help and integration tests. + ## Explicit per-model capability declarations `modelCapabilities` on `src/types/provider.ts` stores exact model-ID entries with optional inputModalities, contextTier and video.processing axes. `src/config/provider-validation.ts` strictly validates writes and merges PATCH axes without sharing live objects; null map/model/axis/processing tombstones delete, while empty PATCH objects do nothing. Complete POST/PUT replacements reject tombstones. File reads retain valid axes; malformed explicit modalities restrict to text with a diagnostic. The two catalog writers receive explicit config and gather fingerprints include the map. This storage contract alone does not activate a context tier, advertise a larger window or enable video processing. - -The lightweight top-level CLI help counts Cline CLI among the fifteen registered export clients; registry parity remains covered by the client help and integration tests. diff --git a/structure/providers/openai-tiers.md b/structure/providers/openai-tiers.md index f626214544..72121b4654 100644 --- a/structure/providers/openai-tiers.md +++ b/structure/providers/openai-tiers.md @@ -421,6 +421,8 @@ successful main usage refresh clears the runtime mark. `src/codex/history-provider.ts` refuses external writes to paginated or migration-capable history. `src/codex/inject.ts` checks affected rows and manifest-owned restore targets before and after config/profile/journal changes, including successful journal and fallback restores, and compensates detected migration. Failed config restore stops later catalog/history work and rolls back a coordinated remove transition. See the [history writer contract](../codex-home.md#paginated-history-writer-boundary) for guarantees and concurrent-writer limits. +The [explicit model-capability contract](../config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior. + ## Context relay ownership `src/codex/context-owner.ts` records which account actually served a root session, taken from the @@ -453,5 +455,3 @@ separately, nothing is dispatched upstream after either, and notes writes are ne Context relay dispatch rechecks the native experimental opt-in after body and credential waits. A disabled gate prevents upstream dispatch even when the request entered while enabled. Final materialized headers pass the proxy-credential exclusion check before owner matching. - -The [explicit model-capability contract](../config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior. diff --git a/structure/providers/xai-grok.md b/structure/providers/xai-grok.md index 67fcebf9c8..edd8f168e9 100644 --- a/structure/providers/xai-grok.md +++ b/structure/providers/xai-grok.md @@ -70,6 +70,8 @@ Devin CLI credential path composition in `src/oauth/devin-cli.ts` follows the se Native Chat applies qualifying effort ceilings independently of model pins; pin selection precedes the cap and only pins or cap rewrites enter wire mapping. The [catalog effort contract](../catalog.md#ultra-reasoning-level) records the V1/compaction exemptions and caller-preservation boundary. +The [explicit model-capability contract](../config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior. + ### OAuth Fast Tier (Priority Processing) xAI's Priority Processing (`service_tier: "priority"` on Chat Completions and Responses, @@ -90,5 +92,3 @@ The upstream tier echo relays to the client on every Chat Completions delivery s (`src/chat/outbound.ts` projections and `src/server/chat-native-sse.ts` chunks), matching what the Responses lane already relayed for responses-wire upstreams; the responses-lane assembly for chat-wire upstreams tracks the echo in attempt telemetry only. - -The [explicit model-capability contract](../config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior.