@bitkyc08/opencodex 2.59.0 → 2.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/index-BTuCbqQd.css +1 -0
- package/gui/dist/assets/index-DoBVdPHP.js +134 -0
- package/gui/dist/index.html +2 -2
- package/package.json +4 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +4 -1
- package/src/adapters/base.ts +23 -0
- package/src/adapters/coding-agent/turn.ts +22 -2
- package/src/adapters/command-code.ts +50 -3
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/request-builder.ts +7 -1
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +3 -1
- package/src/adapters/devin/cloud-direct/chat.ts +3 -1
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
- package/src/adapters/devin.ts +50 -13
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +29 -5
- package/src/adapters/google-http.ts +49 -10
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +37 -6
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +5 -1
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +15 -3
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/bridge/sse.ts +3 -151
- package/src/cli/account-extended.ts +4 -4
- package/src/cli/dispatch.ts +3 -3
- package/src/cli/doctor.ts +28 -9
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +7 -2
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/client/machine-api.ts +2 -2
- package/src/client/machine-listener.ts +2 -2
- package/src/client/runtime.ts +26 -2
- package/src/codex/account-store.ts +65 -0
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/pool-quota-probe.ts +30 -7
- package/src/codex/catalog/gather-capture.ts +21 -2
- package/src/codex/catalog/model-hints.ts +29 -28
- package/src/codex/catalog/parsing.ts +7 -0
- package/src/codex/catalog/provider-models.ts +19 -2
- package/src/codex/catalog/retained-sync.ts +22 -26
- package/src/codex/catalog/routed-gather.ts +19 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +20 -166
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/model-cache.ts +47 -0
- package/src/codex/model-entitlements.ts +29 -10
- package/src/codex/observed-model-denials.ts +101 -8
- package/src/codex/prompt-text-probe.ts +9 -6
- package/src/codex/routing.ts +7 -1
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +22 -4
- package/src/combos/failover.ts +3 -0
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/diagnostics.ts +22 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +18 -0
- package/src/config/proxy-env.ts +8 -2
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +4 -0
- package/src/config/schema/leaf-validators.ts +12 -0
- package/src/config.ts +2 -2
- package/src/generated/compatibility-version.json +249 -169
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/mutation-plan.ts +815 -0
- package/src/integrations/writer.ts +85 -99
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +23 -0
- package/src/lib/errors.ts +79 -0
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-outbound.ts +50 -2
- package/src/lib/socks5-fetch.ts +136 -26
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +218 -27
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +1 -0
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +3 -107
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/key-failover.ts +66 -18
- package/src/providers/model-rename-migration.ts +55 -1
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/vendor-probes-key.ts +1 -1
- package/src/providers/registry/entries-core.ts +39 -17
- package/src/providers/registry/entries-extended.ts +35 -1
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +9 -0
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/responses/apply-patch-envelope.ts +0 -12
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +91 -115
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/auth-cors.ts +3 -0
- package/src/server/chat-completions.ts +5 -2
- package/src/server/claude-messages.ts +6 -3
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/index/serve-options.ts +86 -29
- package/src/server/index/spend-ledger-lifecycle.ts +66 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +16 -13
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/config-routes.ts +18 -2
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/route-registry.ts +14 -0
- package/src/server/management/shared.ts +10 -3
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/management-auth.ts +15 -1
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +119 -10
- package/src/server/request-log.ts +55 -2
- package/src/server/request-metrics.ts +236 -0
- package/src/server/responses/adapter-continuation.ts +3 -3
- package/src/server/responses/adapter-dispatch.ts +11 -6
- package/src/server/responses/compact.ts +32 -10
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +8 -3
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +6 -12
- package/src/server/responses/core-opaque-recovery.ts +1 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/passthrough-delivery.ts +31 -14
- package/src/server/responses/passthrough-dispatch.ts +36 -9
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +67 -17
- package/src/server/responses/request-send-budget.ts +5 -1
- package/src/server/responses/request-sidecar-auth.ts +1 -1
- package/src/server/responses/request-transport.ts +2 -2
- package/src/server/responses/run-turn-execution.ts +25 -3
- package/src/server/responses/sidecar-execution.ts +17 -2
- package/src/server/responses/ws-upstream.ts +14 -27
- package/src/server/responses-custom-tool-repair.ts +27 -54
- package/src/server/responses-undeclared-tool-guard.ts +31 -1
- package/src/server/sse-payload-rewrite.ts +1 -1
- package/src/tray/windows-tray.ps1 +155 -3
- package/src/types/config.ts +11 -3
- package/src/types/provider.ts +18 -0
- package/src/types/request.ts +2 -0
- package/src/types/tools.ts +14 -0
- package/src/types.ts +1 -0
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/gui/dist/assets/index-C5IebErG.js +0 -136
- package/gui/dist/assets/index-OESInAjC.css +0 -1
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import type { ModelCapabilities, OcxProviderConfig } from "../types";
|
|
2
|
+
import { MODEL_ADAPTER_OVERRIDE_ALLOWED, pinnedWireAdapter } from "../types";
|
|
3
|
+
import { isCanonicalOpenAiForwardProvider } from "./openai-tiers";
|
|
4
|
+
import { resolveProviderAuthTransport } from "./fastwire";
|
|
5
|
+
import { registryEntrySupportsLiveModelDiscovery } from "./static-model-discovery";
|
|
6
|
+
import type { InboundWire, ProviderRegistryEntry, ResponsesTerminalRepairPolicy } from "./registry/types";
|
|
7
|
+
import {
|
|
8
|
+
anthropicFamilyContextWindow,
|
|
9
|
+
detachedClone,
|
|
10
|
+
legacyModelValue,
|
|
11
|
+
legacyModelSource,
|
|
12
|
+
mapFill,
|
|
13
|
+
nestedMapFill,
|
|
14
|
+
positiveCapMap,
|
|
15
|
+
recursivelyFreeze,
|
|
16
|
+
resolvedBaseUrl,
|
|
17
|
+
sameStringArray,
|
|
18
|
+
scalar,
|
|
19
|
+
stableUnion,
|
|
20
|
+
staticHeaders,
|
|
21
|
+
wireDefault,
|
|
22
|
+
type StaticPolicySource,
|
|
23
|
+
} from "./resolved-model-policy-merge";
|
|
24
|
+
export type { StaticPolicySource } from "./resolved-model-policy-merge";
|
|
25
|
+
export type StaticProviderPolicyField =
|
|
26
|
+
| "adapter" | "baseUrl" | "apiKeyTransport" | "headers" | "authMode" | "codexAccountMode"
|
|
27
|
+
| "responsesPath" | "chatCompletionsPath" | "keyOptional" | "freeTier" | "modelSuffixBracketStrip"
|
|
28
|
+
| "defaultModel" | "models" | "liveModels" | "contextWindow" | "modelContextWindows"
|
|
29
|
+
| "modelDisplayNames" | "modelInputModalities" | "modelMaxInputTokens" | "defaultMaxOutputTokens"
|
|
30
|
+
| "modelMaxOutputTokens" | "reasoningEfforts" | "modelReasoningEfforts" | "modelReasoningEffortsAuthoritative"
|
|
31
|
+
| "modelDefaultReasoningEfforts" | "reasoningEffortMap" | "modelReasoningEffortMap"
|
|
32
|
+
| "reasoningWireFormat" | "noVisionModels" | "noReasoningModels" | "noTemperatureModels"
|
|
33
|
+
| "noTopPModels" | "noPenaltyModels" | "noJsonSchemaModels" | "parallelToolCalls"
|
|
34
|
+
| "promptCacheKey" | "chatServiceTier" | "openaiChatEofTolerance" | "statelessResponses"
|
|
35
|
+
| "requiresAdjacentResponsesToolResults" | "requiresPairedResponsesToolResults" | "annotateEmptyToolOutputs"
|
|
36
|
+
| "fastWire" | "supportsServiceTier" | "modelSupportsServiceTier" | "supportsOpenAiWebSearchToolFields"
|
|
37
|
+
| "supportsResponsesCustomTools" | "preserveResponsesReasoningContent" | "dropResponsesReasoningItems"
|
|
38
|
+
| "modelSupportsReasoningSummaries"
|
|
39
|
+
| "supportsVerbosity" | "modelSupportsVerbosity" | "responsesItemIdRepair" | "autoToolChoiceOnlyModels"
|
|
40
|
+
| "preserveReasoningContentModels" | "requiresReasoningPlaceholderModels" | "reasoningSplitModels"
|
|
41
|
+
| "reasoningDetailsModels" | "thinkingToggleModels" | "thinkingBudgetModels" | "showThinkingSummary"
|
|
42
|
+
| "escapeBuiltinToolNames" | "googleMode" | "project" | "location" | "modelCapabilities"
|
|
43
|
+
| "modelAutoCompactTokenLimits" | "modelSuppressSyntheticMax" | "modelReasoningSummaryDelivery"
|
|
44
|
+
| "codexToolMode" | "modelAdapters";
|
|
45
|
+
type StaticProviderPolicyShape = Pick<OcxProviderConfig, StaticProviderPolicyField>;
|
|
46
|
+
export type ResolvedProviderStaticPolicy = Readonly<Partial<StaticProviderPolicyShape>> & Readonly<{
|
|
47
|
+
adapter: string;
|
|
48
|
+
baseUrl: string;
|
|
49
|
+
}>;
|
|
50
|
+
export interface ResolvedPerModelStaticPolicy {
|
|
51
|
+
readonly adapter: string;
|
|
52
|
+
readonly contextWindow?: number;
|
|
53
|
+
readonly inputModalities?: readonly string[];
|
|
54
|
+
readonly maxInputTokens?: number;
|
|
55
|
+
readonly maxOutputTokens?: number;
|
|
56
|
+
readonly reasoningEfforts?: readonly string[];
|
|
57
|
+
readonly defaultReasoningEffort?: string;
|
|
58
|
+
readonly supportsReasoningSummaries?: boolean;
|
|
59
|
+
readonly supportsVerbosity?: boolean;
|
|
60
|
+
readonly supportsServiceTier?: boolean;
|
|
61
|
+
readonly fastTierDescription?: string;
|
|
62
|
+
readonly directReasoningEffort?: boolean;
|
|
63
|
+
readonly responsesUpstreamStreaming?: boolean;
|
|
64
|
+
readonly responsesTerminalRepair?: Readonly<ResponsesTerminalRepairPolicy>;
|
|
65
|
+
}
|
|
66
|
+
export interface ResolvedModelPolicy {
|
|
67
|
+
readonly version: 1;
|
|
68
|
+
readonly providerName: string;
|
|
69
|
+
readonly modelId: string;
|
|
70
|
+
readonly transportMatchedRegistry: boolean;
|
|
71
|
+
readonly effectiveAlias?: string | null;
|
|
72
|
+
readonly provider: ResolvedProviderStaticPolicy;
|
|
73
|
+
readonly model: Readonly<ResolvedPerModelStaticPolicy>;
|
|
74
|
+
readonly provenance: Readonly<{
|
|
75
|
+
alias: StaticPolicySource;
|
|
76
|
+
provider: Readonly<Partial<Record<StaticProviderPolicyField, StaticPolicySource>>>;
|
|
77
|
+
model: Readonly<Partial<Record<keyof ResolvedPerModelStaticPolicy, StaticPolicySource>>>;
|
|
78
|
+
}>;
|
|
79
|
+
}
|
|
80
|
+
export interface ResolveModelPolicyInput {
|
|
81
|
+
readonly providerName: string;
|
|
82
|
+
/** Final wire identity after resolveModelAlias and resolveOpenAiVirtualModel; public selection stays diagnostic-only. */
|
|
83
|
+
readonly modelId: string;
|
|
84
|
+
readonly provider: Readonly<OcxProviderConfig>;
|
|
85
|
+
readonly registryEntry?: Readonly<ProviderRegistryEntry>;
|
|
86
|
+
readonly transportMatchedRegistry: boolean;
|
|
87
|
+
readonly inboundWire?: InboundWire;
|
|
88
|
+
/** Exact post-rewrite model capability declaration captured by the caller. */
|
|
89
|
+
readonly modelCapabilities?: Readonly<ModelCapabilities>;
|
|
90
|
+
/** Credential-free live admission result: key only after a usable override, otherwise the registry mode. */
|
|
91
|
+
readonly effectiveAuth?: Readonly<{ authMode: NonNullable<OcxProviderConfig["authMode"]> }>;
|
|
92
|
+
/** Capture-time collision-aware alias decision. Omit to use provider then registry fallback. */
|
|
93
|
+
readonly effectiveAlias?: string | null;
|
|
94
|
+
readonly effectiveAliasSource?: Exclude<StaticPolicySource, "hard-pin" | "provider-default">;
|
|
95
|
+
}
|
|
96
|
+
export interface ObservedModelLimits {
|
|
97
|
+
readonly contextWindow?: number;
|
|
98
|
+
readonly maxInputTokens?: number;
|
|
99
|
+
readonly maxOutputTokens?: number;
|
|
100
|
+
}
|
|
101
|
+
/** Call-local projection: observed values stay outside ResolvedModelPolicy and may only narrow caps. */
|
|
102
|
+
export function clampObservedModelLimits(
|
|
103
|
+
policy: Readonly<Pick<ResolvedPerModelStaticPolicy, "contextWindow" | "maxInputTokens" | "maxOutputTokens">>,
|
|
104
|
+
observed: Readonly<ObservedModelLimits>,
|
|
105
|
+
): Readonly<ObservedModelLimits> {
|
|
106
|
+
const clamp = (staticCap: number | undefined, observedValue: number | undefined): number | undefined => {
|
|
107
|
+
if (observedValue === undefined) return staticCap;
|
|
108
|
+
return staticCap === undefined ? observedValue : Math.min(staticCap, observedValue);
|
|
109
|
+
};
|
|
110
|
+
const contextWindow = clamp(policy.contextWindow, observed.contextWindow);
|
|
111
|
+
const clampedMaxInputTokens = clamp(policy.maxInputTokens, observed.maxInputTokens);
|
|
112
|
+
const maxInputTokens = clampedMaxInputTokens !== undefined && contextWindow !== undefined
|
|
113
|
+
? Math.min(clampedMaxInputTokens, contextWindow)
|
|
114
|
+
: clampedMaxInputTokens;
|
|
115
|
+
const maxOutputTokens = clamp(policy.maxOutputTokens, observed.maxOutputTokens);
|
|
116
|
+
return Object.freeze({
|
|
117
|
+
...(contextWindow !== undefined ? { contextWindow } : {}),
|
|
118
|
+
...(maxInputTokens !== undefined ? { maxInputTokens } : {}),
|
|
119
|
+
...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
export function resolveModelPolicy(input: ResolveModelPolicyInput): ResolvedModelPolicy {
|
|
123
|
+
const entry = input.transportMatchedRegistry ? input.registryEntry : undefined;
|
|
124
|
+
const provider = input.provider;
|
|
125
|
+
const providerPolicy: Partial<StaticProviderPolicyShape> = {};
|
|
126
|
+
const providerProvenance: Partial<Record<StaticProviderPolicyField, StaticPolicySource>> = {};
|
|
127
|
+
const [baseUrl, baseUrlSource] = resolvedBaseUrl(entry, provider);
|
|
128
|
+
const put = <K extends StaticProviderPolicyField>(key: K, value: StaticProviderPolicyShape[K] | undefined, source: StaticPolicySource): void => {
|
|
129
|
+
if (value !== undefined) providerPolicy[key] = detachedClone(value) as never;
|
|
130
|
+
providerProvenance[key] = source;
|
|
131
|
+
};
|
|
132
|
+
const putScalar = <K extends StaticProviderPolicyField>(key: K, registryValue: StaticProviderPolicyShape[K] | undefined): void => {
|
|
133
|
+
const [value, source] = scalar(provider[key], registryValue);
|
|
134
|
+
put(key, value, source);
|
|
135
|
+
};
|
|
136
|
+
const putUnion = (key: StaticProviderPolicyField, registryValue: readonly string[] | undefined): void => {
|
|
137
|
+
const [value, source] = stableUnion(registryValue, provider[key] as string[] | undefined);
|
|
138
|
+
put(key, value as never, source);
|
|
139
|
+
};
|
|
140
|
+
const putMergedMap = <K extends StaticProviderPolicyField, T>(
|
|
141
|
+
key: K,
|
|
142
|
+
registryValue: Readonly<Record<string, T>> | undefined,
|
|
143
|
+
operatorValue: Readonly<Record<string, T>> | undefined,
|
|
144
|
+
): void => {
|
|
145
|
+
const [value, source] = mapFill(registryValue, operatorValue);
|
|
146
|
+
put(key, value as StaticProviderPolicyShape[K], source);
|
|
147
|
+
};
|
|
148
|
+
put("adapter", entry?.adapter ?? provider.adapter, entry ? "registry" : "operator");
|
|
149
|
+
put("baseUrl", baseUrl, baseUrlSource);
|
|
150
|
+
putScalar("apiKeyTransport", entry?.apiKeyTransport);
|
|
151
|
+
const [headers, headersSource] = staticHeaders(entry?.staticHeaders, provider.headers);
|
|
152
|
+
put("headers", headers, headersSource);
|
|
153
|
+
const resolvedAuthMode = input.effectiveAuth?.authMode
|
|
154
|
+
?? (entry?.authKind === "forward" || entry?.authKind === "oauth"
|
|
155
|
+
? entry.authKind
|
|
156
|
+
: provider.authMode === "forward" ? undefined : provider.authMode ?? entry?.authKind);
|
|
157
|
+
put("authMode", resolvedAuthMode, input.effectiveAuth
|
|
158
|
+
? "captured-auth"
|
|
159
|
+
: entry?.authKind === "forward" || entry?.authKind === "oauth" ? "registry"
|
|
160
|
+
: provider.authMode !== undefined ? "operator" : entry ? "registry" : "unknown");
|
|
161
|
+
putScalar("codexAccountMode", entry?.codexAccountMode);
|
|
162
|
+
const keyAuthDefaults = entry?.allowKeyAuthOverride === true
|
|
163
|
+
&& resolvedAuthMode === "key"
|
|
164
|
+
&& ["authorization_bearer", "x_api_key"].includes(resolveProviderAuthTransport(
|
|
165
|
+
provider.adapter,
|
|
166
|
+
resolvedAuthMode,
|
|
167
|
+
provider.apiKeyTransport,
|
|
168
|
+
))
|
|
169
|
+
? entry.keyAuthServiceTier
|
|
170
|
+
: undefined;
|
|
171
|
+
for (const key of [
|
|
172
|
+
"responsesPath", "chatCompletionsPath", "keyOptional", "freeTier", "modelSuffixBracketStrip",
|
|
173
|
+
"defaultModel", "models", "liveModels", "contextWindow", "defaultMaxOutputTokens",
|
|
174
|
+
"reasoningWireFormat", "parallelToolCalls", "promptCacheKey",
|
|
175
|
+
"openaiChatEofTolerance", "statelessResponses",
|
|
176
|
+
"requiresAdjacentResponsesToolResults", "requiresPairedResponsesToolResults",
|
|
177
|
+
"annotateEmptyToolOutputs", "fastWire", "supportsServiceTier",
|
|
178
|
+
"supportsOpenAiWebSearchToolFields", "supportsResponsesCustomTools",
|
|
179
|
+
"preserveResponsesReasoningContent", "dropResponsesReasoningItems",
|
|
180
|
+
"supportsVerbosity", "responsesItemIdRepair",
|
|
181
|
+
"showThinkingSummary", "escapeBuiltinToolNames", "googleMode", "project", "location",
|
|
182
|
+
] as const) putScalar(key, entry?.[key] as StaticProviderPolicyShape[typeof key] | undefined);
|
|
183
|
+
const legacyClinePassLadder = entry !== undefined && input.providerName === "cline-pass"
|
|
184
|
+
&& provider.reasoningWireFormat === "gateway-object"
|
|
185
|
+
&& sameStringArray(provider.reasoningEfforts, ["low"]);
|
|
186
|
+
put("reasoningEfforts", legacyClinePassLadder ? entry?.reasoningEfforts : provider.reasoningEfforts ?? entry?.reasoningEfforts,
|
|
187
|
+
legacyClinePassLadder || provider.reasoningEfforts === undefined ? entry?.reasoningEfforts ? "registry" : "unknown" : "operator");
|
|
188
|
+
put("chatServiceTier", provider.chatServiceTier ?? keyAuthDefaults?.chatServiceTier ?? entry?.chatServiceTier,
|
|
189
|
+
provider.chatServiceTier !== undefined ? "operator" : keyAuthDefaults?.chatServiceTier !== undefined ? "registry" : entry?.chatServiceTier !== undefined ? "registry" : "unknown");
|
|
190
|
+
put("supportsServiceTier", provider.supportsServiceTier ?? keyAuthDefaults?.supportsServiceTier ?? entry?.supportsServiceTier,
|
|
191
|
+
provider.supportsServiceTier !== undefined ? "operator" : keyAuthDefaults?.supportsServiceTier !== undefined ? "registry" : entry?.supportsServiceTier !== undefined ? "registry" : "unknown");
|
|
192
|
+
if (entry && !registryEntrySupportsLiveModelDiscovery(entry)) put("liveModels", false, "registry");
|
|
193
|
+
putMergedMap("modelDisplayNames", entry?.modelDisplayNames, provider.modelDisplayNames);
|
|
194
|
+
putMergedMap("modelInputModalities", entry?.modelInputModalities, provider.modelInputModalities);
|
|
195
|
+
putMergedMap("modelMaxOutputTokens", entry?.modelMaxOutputTokens, provider.modelMaxOutputTokens);
|
|
196
|
+
putMergedMap("modelReasoningEfforts", entry?.modelReasoningEfforts, provider.modelReasoningEfforts);
|
|
197
|
+
// Operator-only: no registry entry declares it, and it decides whether the map above is the
|
|
198
|
+
// wire contract or only the catalog's. A policy reader that omitted it would report the same
|
|
199
|
+
// effective ladder for two providers that send different efforts.
|
|
200
|
+
put("modelReasoningEffortsAuthoritative", provider.modelReasoningEffortsAuthoritative,
|
|
201
|
+
provider.modelReasoningEffortsAuthoritative !== undefined ? "operator" : "unknown");
|
|
202
|
+
putMergedMap("modelDefaultReasoningEfforts", entry?.modelDefaultReasoningEfforts, provider.modelDefaultReasoningEfforts);
|
|
203
|
+
const registryServiceTier = !entry?.modelServiceTierCapabilityBaseUrlGuard
|
|
204
|
+
|| entry.modelServiceTierCapabilityBaseUrlGuard(provider.baseUrl)
|
|
205
|
+
? entry?.modelSupportsServiceTier
|
|
206
|
+
: undefined;
|
|
207
|
+
const registryServiceTierDefaults = registryServiceTier || keyAuthDefaults?.modelSupportsServiceTier
|
|
208
|
+
? { ...(registryServiceTier ?? {}), ...(keyAuthDefaults?.modelSupportsServiceTier ?? {}) }
|
|
209
|
+
: undefined;
|
|
210
|
+
putMergedMap("modelSupportsServiceTier", registryServiceTierDefaults, provider.modelSupportsServiceTier);
|
|
211
|
+
putMergedMap("modelSupportsReasoningSummaries", entry?.modelSupportsReasoningSummaries, provider.modelSupportsReasoningSummaries);
|
|
212
|
+
putMergedMap("modelSupportsVerbosity", entry?.modelSupportsVerbosity, provider.modelSupportsVerbosity);
|
|
213
|
+
for (const key of [
|
|
214
|
+
"modelCapabilities", "modelAutoCompactTokenLimits", "modelSuppressSyntheticMax",
|
|
215
|
+
"modelReasoningSummaryDelivery", "codexToolMode", "modelAdapters",
|
|
216
|
+
] as const) putScalar(key, undefined);
|
|
217
|
+
for (const key of ["modelContextWindows", "modelMaxInputTokens"] as const) {
|
|
218
|
+
const [value, source] = input.providerName === "openai-apikey"
|
|
219
|
+
? positiveCapMap(entry?.[key], provider[key])
|
|
220
|
+
: mapFill(entry?.[key], provider[key]);
|
|
221
|
+
put(key, value, source);
|
|
222
|
+
}
|
|
223
|
+
const [effortMap, effortMapSource] = mapFill(entry?.reasoningEffortMap, provider.reasoningEffortMap);
|
|
224
|
+
put("reasoningEffortMap", effortMap, effortMapSource);
|
|
225
|
+
const [modelEffortMap, modelEffortMapSource] = nestedMapFill(entry?.modelReasoningEffortMap, provider.modelReasoningEffortMap);
|
|
226
|
+
put("modelReasoningEffortMap", modelEffortMap, modelEffortMapSource);
|
|
227
|
+
for (const key of [
|
|
228
|
+
"noVisionModels", "noReasoningModels", "noTemperatureModels", "noTopPModels",
|
|
229
|
+
"noPenaltyModels", "noJsonSchemaModels", "autoToolChoiceOnlyModels",
|
|
230
|
+
"preserveReasoningContentModels", "requiresReasoningPlaceholderModels",
|
|
231
|
+
"reasoningSplitModels", "reasoningDetailsModels", "thinkingToggleModels", "thinkingBudgetModels",
|
|
232
|
+
] as const) putUnion(key, entry?.[key]);
|
|
233
|
+
for (const directModel of entry?.directReasoningEffortModels ?? []) {
|
|
234
|
+
const staleBudget = [directModel, ...(entry?.thinkingBudgetModels ?? [])];
|
|
235
|
+
const routedStaleBudget = [...(entry?.thinkingBudgetModels ?? []), directModel];
|
|
236
|
+
if (sameStringArray(provider.thinkingBudgetModels, staleBudget)
|
|
237
|
+
|| sameStringArray(provider.thinkingBudgetModels, routedStaleBudget)) {
|
|
238
|
+
providerPolicy.thinkingBudgetModels = [...(entry?.thinkingBudgetModels ?? [])];
|
|
239
|
+
providerProvenance.thinkingBudgetModels = "registry";
|
|
240
|
+
}
|
|
241
|
+
const explicitEfforts = Object.entries(provider.modelReasoningEfforts ?? {})
|
|
242
|
+
.filter(([key]) => key.toLowerCase() === directModel.toLowerCase());
|
|
243
|
+
if (explicitEfforts.length > 0) {
|
|
244
|
+
for (const key of Object.keys(providerPolicy.modelReasoningEfforts ?? {})) {
|
|
245
|
+
if (key.toLowerCase() === directModel.toLowerCase()) delete providerPolicy.modelReasoningEfforts![key];
|
|
246
|
+
}
|
|
247
|
+
for (const [key, value] of explicitEfforts) (providerPolicy.modelReasoningEfforts ??= {})[key] = [...value];
|
|
248
|
+
} else if (entry?.modelReasoningEfforts?.[directModel] !== undefined) {
|
|
249
|
+
(providerPolicy.modelReasoningEfforts ??= {})[directModel] = [...entry.modelReasoningEfforts[directModel]!];
|
|
250
|
+
}
|
|
251
|
+
const explicitDefault = Object.entries(provider.modelDefaultReasoningEfforts ?? {})
|
|
252
|
+
.filter(([key]) => key.toLowerCase() === directModel.toLowerCase());
|
|
253
|
+
if (explicitDefault.length > 0) {
|
|
254
|
+
for (const key of Object.keys(providerPolicy.modelDefaultReasoningEfforts ?? {})) {
|
|
255
|
+
if (key.toLowerCase() === directModel.toLowerCase()) delete providerPolicy.modelDefaultReasoningEfforts![key];
|
|
256
|
+
}
|
|
257
|
+
for (const [key, value] of explicitDefault) (providerPolicy.modelDefaultReasoningEfforts ??= {})[key] = value;
|
|
258
|
+
} else if (entry?.modelDefaultReasoningEfforts?.[directModel] !== undefined) {
|
|
259
|
+
(providerPolicy.modelDefaultReasoningEfforts ??= {})[directModel] = entry.modelDefaultReasoningEfforts[directModel]!;
|
|
260
|
+
}
|
|
261
|
+
const explicitMap = Object.entries(provider.modelReasoningEffortMap ?? {})
|
|
262
|
+
.filter(([key]) => key.toLowerCase() === directModel.toLowerCase());
|
|
263
|
+
for (const key of Object.keys(providerPolicy.modelReasoningEffortMap ?? {})) {
|
|
264
|
+
if (key.toLowerCase() === directModel.toLowerCase()) delete providerPolicy.modelReasoningEffortMap![key];
|
|
265
|
+
}
|
|
266
|
+
if (explicitMap.length > 0) {
|
|
267
|
+
for (const [key, value] of explicitMap) (providerPolicy.modelReasoningEffortMap ??= {})[key] = { ...value };
|
|
268
|
+
} else {
|
|
269
|
+
(providerPolicy.modelReasoningEffortMap ??= {})[directModel] = {};
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const modelValue = <T>(record: Readonly<Record<string, T>> | undefined): T | undefined =>
|
|
273
|
+
legacyModelValue(record, input.modelId);
|
|
274
|
+
const modelSource = <T>(operator: Readonly<Record<string, T>> | undefined, registry: Readonly<Record<string, T>> | undefined): StaticPolicySource =>
|
|
275
|
+
legacyModelSource(operator, registry, input.modelId);
|
|
276
|
+
const modelOrProviderSource = <T>(
|
|
277
|
+
operatorMap: Readonly<Record<string, T>> | undefined, registryMap: Readonly<Record<string, T>> | undefined,
|
|
278
|
+
operatorDefault: T | undefined, registryDefault: T | undefined,
|
|
279
|
+
): StaticPolicySource => {
|
|
280
|
+
const exact = modelSource(operatorMap, registryMap);
|
|
281
|
+
return exact !== "unknown" ? exact : operatorDefault !== undefined ? "operator" : registryDefault !== undefined ? "registry" : "unknown";
|
|
282
|
+
};
|
|
283
|
+
const configuredAdapter = provider.modelAdapters?.[input.modelId];
|
|
284
|
+
const pin = pinnedWireAdapter(input.providerName, input.modelId);
|
|
285
|
+
const normalizedModelId = input.modelId.trim().toLowerCase();
|
|
286
|
+
const registryWire = wireDefault(entry?.modelWireDefaults?.[normalizedModelId], provider, entry,
|
|
287
|
+
input.inboundWire ?? "responses", resolvedAuthMode);
|
|
288
|
+
const explicitWire = configuredAdapter && MODEL_ADAPTER_OVERRIDE_ALLOWED.has(configuredAdapter)
|
|
289
|
+
? configuredAdapter
|
|
290
|
+
: undefined;
|
|
291
|
+
const providerAdapter = providerPolicy.adapter ?? provider.adapter;
|
|
292
|
+
const effectiveProviderForWire = resolvedAuthMode === provider.authMode
|
|
293
|
+
? provider
|
|
294
|
+
: { ...provider, authMode: resolvedAuthMode };
|
|
295
|
+
const canonicalForward = isCanonicalOpenAiForwardProvider(effectiveProviderForWire);
|
|
296
|
+
const adapter = pin
|
|
297
|
+
?? (!canonicalForward ? explicitWire ?? registryWire : undefined)
|
|
298
|
+
?? providerAdapter;
|
|
299
|
+
const adapterSource: StaticPolicySource = pin
|
|
300
|
+
? "hard-pin"
|
|
301
|
+
: explicitWire && !canonicalForward
|
|
302
|
+
? "operator"
|
|
303
|
+
: registryWire && !canonicalForward
|
|
304
|
+
? "registry"
|
|
305
|
+
: "provider-default";
|
|
306
|
+
const declaredModalities = input.modelCapabilities?.inputModalities;
|
|
307
|
+
const capabilityModalities = Array.isArray(declaredModalities) && declaredModalities.length > 0
|
|
308
|
+
? [...declaredModalities]
|
|
309
|
+
: undefined;
|
|
310
|
+
const exactContextWindow = modelValue(providerPolicy.modelContextWindows);
|
|
311
|
+
const familyContextWindow = providerPolicy.adapter === "anthropic"
|
|
312
|
+
? anthropicFamilyContextWindow(providerPolicy.modelContextWindows, input.modelId)
|
|
313
|
+
: undefined;
|
|
314
|
+
const modelContextWindow = exactContextWindow ?? familyContextWindow ?? providerPolicy.contextWindow;
|
|
315
|
+
const exactMaxInputTokens = modelValue(providerPolicy.modelMaxInputTokens);
|
|
316
|
+
const modelMaxInputTokens = exactMaxInputTokens !== undefined && modelContextWindow !== undefined
|
|
317
|
+
? Math.min(exactMaxInputTokens, modelContextWindow)
|
|
318
|
+
: exactMaxInputTokens;
|
|
319
|
+
const exactMaxOutputTokens = modelValue(providerPolicy.modelMaxOutputTokens);
|
|
320
|
+
const modelMaxOutputTokens = exactMaxOutputTokens ?? providerPolicy.defaultMaxOutputTokens;
|
|
321
|
+
const modelReasoningEfforts = modelValue(providerPolicy.modelReasoningEfforts) ?? providerPolicy.reasoningEfforts;
|
|
322
|
+
const modelSupportsReasoningSummaries = modelValue(providerPolicy.modelSupportsReasoningSummaries)
|
|
323
|
+
?? (modelValue(providerPolicy.modelReasoningSummaryDelivery) !== undefined ? true : undefined);
|
|
324
|
+
const modelSupportsVerbosity = modelValue(providerPolicy.modelSupportsVerbosity) ?? providerPolicy.supportsVerbosity;
|
|
325
|
+
const modelSupportsServiceTier = modelValue(providerPolicy.modelSupportsServiceTier) ?? providerPolicy.supportsServiceTier;
|
|
326
|
+
const model: ResolvedPerModelStaticPolicy = {
|
|
327
|
+
adapter,
|
|
328
|
+
...(modelContextWindow !== undefined ? { contextWindow: modelContextWindow } : {}),
|
|
329
|
+
...(capabilityModalities ?? modelValue(providerPolicy.modelInputModalities) !== undefined
|
|
330
|
+
? { inputModalities: capabilityModalities ?? modelValue(providerPolicy.modelInputModalities) }
|
|
331
|
+
: {}),
|
|
332
|
+
...(modelMaxInputTokens !== undefined ? { maxInputTokens: modelMaxInputTokens } : {}),
|
|
333
|
+
...(modelMaxOutputTokens !== undefined ? { maxOutputTokens: modelMaxOutputTokens } : {}),
|
|
334
|
+
...(modelReasoningEfforts !== undefined ? { reasoningEfforts: modelReasoningEfforts } : {}),
|
|
335
|
+
...(modelValue(providerPolicy.modelDefaultReasoningEfforts) !== undefined ? { defaultReasoningEffort: modelValue(providerPolicy.modelDefaultReasoningEfforts) } : {}),
|
|
336
|
+
...(modelSupportsReasoningSummaries !== undefined ? { supportsReasoningSummaries: modelSupportsReasoningSummaries } : {}),
|
|
337
|
+
...(modelSupportsVerbosity !== undefined ? { supportsVerbosity: modelSupportsVerbosity } : {}),
|
|
338
|
+
...(modelSupportsServiceTier !== undefined ? { supportsServiceTier: modelSupportsServiceTier } : {}),
|
|
339
|
+
...(modelSupportsServiceTier === true && entry?.fastTierDescription !== undefined
|
|
340
|
+
? { fastTierDescription: entry.fastTierDescription }
|
|
341
|
+
: {}),
|
|
342
|
+
...(entry?.directReasoningEffortModels?.includes(input.modelId) ? { directReasoningEffort: true } : {}),
|
|
343
|
+
...(entry?.modelResponsesUpstreamStreaming?.[normalizedModelId] !== undefined
|
|
344
|
+
? { responsesUpstreamStreaming: entry.modelResponsesUpstreamStreaming[normalizedModelId] }
|
|
345
|
+
: {}),
|
|
346
|
+
...(entry?.modelResponsesTerminalRepair?.[normalizedModelId] !== undefined
|
|
347
|
+
? { responsesTerminalRepair: entry.modelResponsesTerminalRepair[normalizedModelId] }
|
|
348
|
+
: {}),
|
|
349
|
+
};
|
|
350
|
+
const modelProvenance: Partial<Record<keyof ResolvedPerModelStaticPolicy, StaticPolicySource>> = {
|
|
351
|
+
adapter: adapterSource,
|
|
352
|
+
};
|
|
353
|
+
modelProvenance.contextWindow = modelOrProviderSource(provider.modelContextWindows, entry?.modelContextWindows, provider.contextWindow, entry?.contextWindow);
|
|
354
|
+
if (modelProvenance.contextWindow === "unknown" && familyContextWindow !== undefined) {
|
|
355
|
+
modelProvenance.contextWindow = anthropicFamilyContextWindow(provider.modelContextWindows, input.modelId) !== undefined
|
|
356
|
+
? "operator"
|
|
357
|
+
: "registry";
|
|
358
|
+
}
|
|
359
|
+
modelProvenance.inputModalities = capabilityModalities
|
|
360
|
+
? "operator-capability"
|
|
361
|
+
: modelSource(provider.modelInputModalities, entry?.modelInputModalities);
|
|
362
|
+
modelProvenance.maxInputTokens = modelSource(provider.modelMaxInputTokens, entry?.modelMaxInputTokens);
|
|
363
|
+
modelProvenance.maxOutputTokens = modelOrProviderSource(
|
|
364
|
+
provider.modelMaxOutputTokens,
|
|
365
|
+
entry?.modelMaxOutputTokens,
|
|
366
|
+
provider.defaultMaxOutputTokens,
|
|
367
|
+
entry?.defaultMaxOutputTokens,
|
|
368
|
+
);
|
|
369
|
+
modelProvenance.reasoningEfforts = modelOrProviderSource(provider.modelReasoningEfforts, entry?.modelReasoningEfforts, provider.reasoningEfforts, entry?.reasoningEfforts);
|
|
370
|
+
modelProvenance.defaultReasoningEffort = modelSource(
|
|
371
|
+
provider.modelDefaultReasoningEfforts,
|
|
372
|
+
entry?.modelDefaultReasoningEfforts,
|
|
373
|
+
);
|
|
374
|
+
modelProvenance.supportsReasoningSummaries = modelSource(provider.modelSupportsReasoningSummaries, entry?.modelSupportsReasoningSummaries);
|
|
375
|
+
if (modelProvenance.supportsReasoningSummaries === "unknown" && modelValue(provider.modelReasoningSummaryDelivery) !== undefined) {
|
|
376
|
+
modelProvenance.supportsReasoningSummaries = "operator";
|
|
377
|
+
}
|
|
378
|
+
modelProvenance.supportsVerbosity = modelOrProviderSource(provider.modelSupportsVerbosity, entry?.modelSupportsVerbosity, provider.supportsVerbosity, entry?.supportsVerbosity);
|
|
379
|
+
const exactServiceTierSource = modelSource(provider.modelSupportsServiceTier, registryServiceTierDefaults);
|
|
380
|
+
modelProvenance.supportsServiceTier = exactServiceTierSource !== "unknown" ? exactServiceTierSource
|
|
381
|
+
: providerProvenance.supportsServiceTier ?? "unknown";
|
|
382
|
+
modelProvenance.responsesUpstreamStreaming = model.responsesUpstreamStreaming === undefined ? "unknown" : "registry";
|
|
383
|
+
modelProvenance.responsesTerminalRepair = model.responsesTerminalRepair === undefined ? "unknown" : "registry";
|
|
384
|
+
modelProvenance.fastTierDescription = model.fastTierDescription === undefined ? "unknown" : "registry";
|
|
385
|
+
modelProvenance.directReasoningEffort = model.directReasoningEffort === undefined ? "unknown" : "registry";
|
|
386
|
+
const alias = input.effectiveAlias !== undefined
|
|
387
|
+
? input.effectiveAlias
|
|
388
|
+
: provider.alias !== undefined
|
|
389
|
+
? provider.alias
|
|
390
|
+
: entry?.alias;
|
|
391
|
+
const aliasSource: StaticPolicySource = input.effectiveAlias !== undefined
|
|
392
|
+
? input.effectiveAliasSource ?? "operator"
|
|
393
|
+
: provider.alias !== undefined
|
|
394
|
+
? "operator"
|
|
395
|
+
: entry?.alias !== undefined ? "registry" : "unknown";
|
|
396
|
+
return recursivelyFreeze(detachedClone({
|
|
397
|
+
version: 1 as const,
|
|
398
|
+
providerName: input.providerName,
|
|
399
|
+
modelId: input.modelId,
|
|
400
|
+
transportMatchedRegistry: entry !== undefined,
|
|
401
|
+
...(alias !== undefined ? { effectiveAlias: alias } : {}),
|
|
402
|
+
provider: providerPolicy as ResolvedProviderStaticPolicy,
|
|
403
|
+
model,
|
|
404
|
+
provenance: { alias: aliasSource, provider: providerProvenance, model: modelProvenance },
|
|
405
|
+
}));
|
|
406
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repair vision classifications a saved config inherited from a stale registry seed.
|
|
3
|
+
*
|
|
4
|
+
* `enrichProviderFromRegistry` is fill-only and asymmetric, which is right for a hand-tuned
|
|
5
|
+
* value but freezes a wrong one:
|
|
6
|
+
*
|
|
7
|
+
* - `noVisionModels` is filled ALL-OR-NOTHING (`if (!prov.noVisionModels && seed.noVisionModels)`),
|
|
8
|
+
* so a config saved while the list was current keeps that list forever.
|
|
9
|
+
* - `modelInputModalities` is filled per-key BENEATH the saved value, so a saved `["text"]` for
|
|
10
|
+
* one id survives every later registry correction.
|
|
11
|
+
*
|
|
12
|
+
* Correcting the registry therefore fixes new installs only. This projection is the other half.
|
|
13
|
+
* It repairs the two saved values a stale seed left behind, in both states that reach a running
|
|
14
|
+
* process:
|
|
15
|
+
*
|
|
16
|
+
* - the full pair: modalities still exactly the stale declaration, and the id in the list. Both
|
|
17
|
+
* are rewritten.
|
|
18
|
+
* - the half-repaired row: modalities already corrected but the id still in the list. The
|
|
19
|
+
* sidecar predicate reads `noVisionModels` BEFORE the modality list, so that row keeps
|
|
20
|
+
* stripping images until the name goes as well; this projection removes it and leaves the
|
|
21
|
+
* modalities untouched.
|
|
22
|
+
*
|
|
23
|
+
* The paired modalities value is the guard in both states, which is also why it has to be
|
|
24
|
+
* readable: a name in the list with no modality declaration beside it is ambiguous — either a
|
|
25
|
+
* half-finished repair or an entry the operator added on purpose — and this projection does not
|
|
26
|
+
* guess which. It leaves that row alone.
|
|
27
|
+
*
|
|
28
|
+
* Identity is the same rule enrichment uses, read from the same helper: the registry must still
|
|
29
|
+
* own the row (`providerMatchesRegistryTransport`, the check `enrichProviderFromRegistry` makes
|
|
30
|
+
* before it writes registry metadata) and the row must still be on the entry's adapter.
|
|
31
|
+
* `opencode-go` is an existing key preset without `preserveCustomDestination`, so the id alone
|
|
32
|
+
* claims a row — exactly as it does for enrichment — while an entry that opts into destination
|
|
33
|
+
* preservation narrows this projection for free.
|
|
34
|
+
*
|
|
35
|
+
* Nothing here writes `modelCapabilities`. That is the dedicated per-model axis, it outranks
|
|
36
|
+
* every source this file touches, and it is where a deliberate text-only override belongs
|
|
37
|
+
* (`ocx provider edit <provider> --model <id> --text-only` writes it).
|
|
38
|
+
*
|
|
39
|
+
* Scope is deliberately one entry. The Go gateway's `deepseek-v4.1-flash` was declared text-only
|
|
40
|
+
* from jawcode metadata and was measured natively multimodal on 2026-09-19 (see the note at that
|
|
41
|
+
* entry in `registry/entries-core.ts`). Its sibling `deepseek-v4-flash` still rejects images
|
|
42
|
+
* upstream, and the sibling Zen tiers (`opencode-zen`, `opencode-free`) could not be probed at
|
|
43
|
+
* all — an unverified tier is not evidence, so neither is touched here.
|
|
44
|
+
*/
|
|
45
|
+
import { PROVIDER_REGISTRY, providerMatchesRegistryTransport } from "./registry";
|
|
46
|
+
import type { OcxConfig } from "../types";
|
|
47
|
+
|
|
48
|
+
export interface StaleVisionClassification {
|
|
49
|
+
/** Registry provider id whose saved rows may carry the wrong classification. */
|
|
50
|
+
provider: string;
|
|
51
|
+
model: string;
|
|
52
|
+
/** The stale saved modalities this migration is allowed to replace, and nothing else. */
|
|
53
|
+
fromModalities: string[];
|
|
54
|
+
toModalities: string[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface StaleVisionClassificationProjection {
|
|
58
|
+
config: OcxConfig;
|
|
59
|
+
changed: boolean;
|
|
60
|
+
warnings: string[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const STALE_VISION_CLASSIFICATIONS: readonly StaleVisionClassification[] = [
|
|
64
|
+
{
|
|
65
|
+
provider: "opencode-go",
|
|
66
|
+
model: "deepseek-v4.1-flash",
|
|
67
|
+
fromModalities: ["text"],
|
|
68
|
+
toModalities: ["text", "image"],
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Whether the registry still owns the row whose saved values would be rewritten.
|
|
74
|
+
*
|
|
75
|
+
* `providerMatchesRegistryTransport` is the rule enrichment applies, so this projection and the
|
|
76
|
+
* fill that put the stale values there answer identity the same way. Adapter equality stays as
|
|
77
|
+
* an additional tightening: a row moved onto another wire is not the row the seed described,
|
|
78
|
+
* however the registry entry is pinned.
|
|
79
|
+
*/
|
|
80
|
+
function providerStillMatchesRegistry(id: string, prov: OcxConfig["providers"][string]): boolean {
|
|
81
|
+
const entry = PROVIDER_REGISTRY.find(row => row.id === id);
|
|
82
|
+
if (entry === undefined || entry.adapter !== prov.adapter) return false;
|
|
83
|
+
return providerMatchesRegistryTransport(id, prov);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function sameModalities(current: unknown, expected: readonly string[]): boolean {
|
|
87
|
+
return Array.isArray(current)
|
|
88
|
+
&& current.length === expected.length
|
|
89
|
+
&& current.every((value, index) => value === expected[index]);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Pure projection. The caller decides whether to persist. */
|
|
93
|
+
export function projectStaleVisionClassifications(
|
|
94
|
+
config: OcxConfig,
|
|
95
|
+
entries: readonly StaleVisionClassification[] = STALE_VISION_CLASSIFICATIONS,
|
|
96
|
+
): StaleVisionClassificationProjection {
|
|
97
|
+
const warnings: string[] = [];
|
|
98
|
+
const repaired = new Map<string, string[]>();
|
|
99
|
+
|
|
100
|
+
for (const entry of entries) {
|
|
101
|
+
const prov = config.providers?.[entry.provider];
|
|
102
|
+
if (!prov) continue;
|
|
103
|
+
if (!providerStillMatchesRegistry(entry.provider, prov)) continue;
|
|
104
|
+
const modalities = prov.modelInputModalities;
|
|
105
|
+
if (!modalities) continue;
|
|
106
|
+
// The paired modalities value is the guard, and it decides which of the two states above this
|
|
107
|
+
// row is in. Anything else is a declaration this file does not own.
|
|
108
|
+
const saved = modalities[entry.model];
|
|
109
|
+
const stale = sameModalities(saved, entry.fromModalities);
|
|
110
|
+
const alreadyMigrated = sameModalities(saved, entry.toModalities);
|
|
111
|
+
if (!stale && !alreadyMigrated) continue;
|
|
112
|
+
const visionList = Array.isArray(prov.noVisionModels) ? prov.noVisionModels : undefined;
|
|
113
|
+
const listed = visionList !== undefined && visionList.includes(entry.model);
|
|
114
|
+
// Half-repaired rows have nothing left to do once the name is gone; the full pair is rewritten
|
|
115
|
+
// whether or not the name was ever listed, because `derive.ts` fills the two fields
|
|
116
|
+
// independently and a per-key modality fill can land without the all-or-nothing list.
|
|
117
|
+
if (!stale && !listed) continue;
|
|
118
|
+
if (stale) modalities[entry.model] = [...entry.toModalities];
|
|
119
|
+
if (visionList !== undefined && listed) {
|
|
120
|
+
prov.noVisionModels = visionList.filter(id => id !== entry.model);
|
|
121
|
+
}
|
|
122
|
+
const repairedList = repaired.get(entry.provider) ?? [];
|
|
123
|
+
repairedList.push(stale
|
|
124
|
+
? `${entry.model} ${entry.fromModalities.join("+")} -> ${entry.toModalities.join("+")}`
|
|
125
|
+
: `${entry.model} dropped from noVisionModels (modalities already ${entry.toModalities.join("+")})`);
|
|
126
|
+
repaired.set(entry.provider, repairedList);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (const [provider, list] of repaired) {
|
|
130
|
+
warnings.push(
|
|
131
|
+
`repaired the stale registry vision seed for ${list.length} model(s) on "${provider}": `
|
|
132
|
+
+ `${list.join(", ")}.`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { config, changed: repaired.size > 0, warnings };
|
|
137
|
+
}
|
|
@@ -34,18 +34,6 @@ function stripMarkdownCodeFence(text: string, toolName: string): string {
|
|
|
34
34
|
return match ? match[1] : text;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
/**
|
|
38
|
-
* The single-field wrappers `unwrapFreeformToolInput` accepts for one tool name, besides the
|
|
39
|
-
* canonical `input`.
|
|
40
|
-
*
|
|
41
|
-
* Exported so the streaming side can hold a buffer that is still turning into one of these.
|
|
42
|
-
* A second list of key names beside this one is how the streamed bytes and the completed item
|
|
43
|
-
* come to disagree, which is the defect it exists to prevent (#5047).
|
|
44
|
-
*/
|
|
45
|
-
export function freeformFallbackKeys(toolName: string): readonly string[] {
|
|
46
|
-
return FREEFORM_FALLBACK_KEYS[toolName] ?? [];
|
|
47
|
-
}
|
|
48
|
-
|
|
49
37
|
/** Unwrap the `{input:string}` function-call wrapper used for freeform tools. */
|
|
50
38
|
export function unwrapFreeformToolInput(argumentsText: unknown, toolName = ""): string {
|
|
51
39
|
if (typeof argumentsText !== "string") return "";
|