@bitkyc08/opencodex 2.55.0-preview.20260914 → 2.56.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-DH2PUHqr.js → index-D4zuyIxQ.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +2 -1
- package/src/adapters/base.ts +21 -0
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-schema.ts +477 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +33 -1445
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +611 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +34 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +624 -0
- package/src/bridge/sse.ts +1444 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +71 -9
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +554 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +422 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +95 -28
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +304 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +706 -0
- package/src/codex/catalog/routed-gather.ts +858 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +540 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +81 -1436
- package/src/codex/lineage.ts +458 -0
- package/src/codex/pool-refresh-backoff.ts +152 -0
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cooldown-math.ts +275 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +703 -0
- package/src/codex/routing/thread-affinity.ts +538 -0
- package/src/codex/routing.ts +353 -2234
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +539 -39
- package/src/lib/request-execution-budget.ts +69 -20
- package/src/lib/spend-reservation-ledger.ts +940 -0
- package/src/lib/upstream-retry.ts +55 -11
- package/src/lib/workflow-budget.ts +553 -30
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1221 -0
- package/src/providers/registry/entries-extended.ts +1204 -0
- package/src/providers/registry/model-seeds.ts +908 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +449 -0
- package/src/routing/probe-lease.ts +511 -0
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +565 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +335 -0
- package/src/server/index.ts +40 -2547
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +245 -1
- package/src/server/responses/account-change-state.ts +233 -0
- package/src/server/responses/adapter-continuation.ts +514 -0
- package/src/server/responses/adapter-delivery.ts +214 -0
- package/src/server/responses/adapter-dispatch.ts +971 -0
- package/src/server/responses/compact.ts +59 -4
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +527 -0
- package/src/server/responses/core-codex-account.ts +859 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +707 -0
- package/src/server/responses/core-errors.ts +152 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +225 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/passthrough-delivery.ts +856 -0
- package/src/server/responses/passthrough-dispatch.ts +1476 -0
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +970 -0
- package/src/server/responses/request-send-budget.ts +164 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-transport.ts +744 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +448 -0
- package/src/server/responses/sidecar-execution.ts +469 -0
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/workflow-refusal.ts +84 -0
- package/src/types/config.ts +30 -0
- package/src/usage/log.ts +146 -0
- package/src/usage/summary.ts +171 -21
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
import { effectiveProviderAlias, effectiveProviderAliasDecision } from "../../providers/default-aliases";
|
|
2
|
+
import { initialModelSelectionPending } from "../../providers/initial-model-selection";
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { createHash, createHmac, randomBytes } from "node:crypto";
|
|
5
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
|
|
6
|
+
import { delimiter, dirname, join, resolve } from "node:path";
|
|
7
|
+
import { atomicWriteFile, expandUserPath, getConfigDir, websocketsEnabled } from "../../config";
|
|
8
|
+
import { resolveProviderApiKey } from "../../providers/key-store";
|
|
9
|
+
import { CODEX_CONFIG_PATH, CODEX_MODELS_CACHE_PATH, DEFAULT_CATALOG_PATH, readRootTomlString, resolveCodexConfigPath } from "../paths";
|
|
10
|
+
import {
|
|
11
|
+
clearModelCache,
|
|
12
|
+
clearProviderDiscoveryStatus,
|
|
13
|
+
captureModelCacheGeneration,
|
|
14
|
+
DEFAULT_MODEL_CACHE_TTL_MS,
|
|
15
|
+
getFreshCached,
|
|
16
|
+
getStaleCached,
|
|
17
|
+
isModelsFetchCoolingDown,
|
|
18
|
+
isModelCacheGenerationCurrent,
|
|
19
|
+
markModelsFetchFailure,
|
|
20
|
+
markProviderDiscoveryFailed,
|
|
21
|
+
markProviderDiscoveryOk,
|
|
22
|
+
shouldLogDiscoveryFailure,
|
|
23
|
+
setCached,
|
|
24
|
+
type ProviderModelDiscoveryFailure,
|
|
25
|
+
} from "../model-cache";
|
|
26
|
+
import {
|
|
27
|
+
buildModelsRequest,
|
|
28
|
+
getValidAccessTokenSnapshot,
|
|
29
|
+
observeActiveOAuthAccessToken,
|
|
30
|
+
resolveModelsAuthToken,
|
|
31
|
+
type OAuthActiveTokenObservation,
|
|
32
|
+
} from "../../oauth";
|
|
33
|
+
import type { OcxConfig, OcxProviderConfig } from "../../types";
|
|
34
|
+
import { modelInList } from "../../types";
|
|
35
|
+
import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
|
|
36
|
+
import { isModelVisionSidecarConsumer } from "../../vision/eligibility";
|
|
37
|
+
import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, resolveMetadataProvider, type ModelMetadata } from "../../generated/model-metadata";
|
|
38
|
+
import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
|
|
39
|
+
import {
|
|
40
|
+
captureFastPolicyAuthority,
|
|
41
|
+
fastPolicyForModel,
|
|
42
|
+
serviceTierSupportFromPolicy,
|
|
43
|
+
} from "../../providers/service-tier";
|
|
44
|
+
import type { FastPolicyAuthority } from "../../providers/fastwire";
|
|
45
|
+
import { effectiveGoogleMode, getProviderRegistryEntry, providerMatchesRegistryTransport, registryEntryForProviderDestination } from "../../providers/registry";
|
|
46
|
+
import { parseAntigravityAvailableModels, registerAntigravityDiscoveredWireModels } from "../../providers/antigravity-models";
|
|
47
|
+
import { applyProviderContextCap, providerContextCap, resolveUnknownRoutedContextWindow } from "../../providers/context-cap";
|
|
48
|
+
import { clampAutoCompactTokenLimit } from "../../providers/auto-compact-budget";
|
|
49
|
+
import { effectiveModelAliases } from "../../providers/default-aliases";
|
|
50
|
+
import { routedSlug, slugEquals, slugEquivalenceKey, slugsEquivalent } from "../../providers/slug-codec";
|
|
51
|
+
import { CODEX_GPT5_IDENTITY_LINE } from "../../adapters/identity";
|
|
52
|
+
import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery";
|
|
53
|
+
import { fetchCursorUsableModels } from "../../adapters/cursor/live-models";
|
|
54
|
+
import { recordLiveCursorClaudeModels, recordLiveCursorMaxModeModels } from "../../adapters/cursor/catalog";
|
|
55
|
+
import { fetchQoderModels } from "../../adapters/qoder/live-models";
|
|
56
|
+
import { resolveQoderProfile } from "../../adapters/qoder/profiles";
|
|
57
|
+
import { fetchDevinUsableModels } from "../../adapters/devin/live-models";
|
|
58
|
+
import { isCanonicalOpenAiForwardProvider, OPENAI_API_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
|
|
59
|
+
import {
|
|
60
|
+
COMBO_NAMESPACE,
|
|
61
|
+
comboModelId,
|
|
62
|
+
getCombo,
|
|
63
|
+
listComboIds,
|
|
64
|
+
quotaInactiveReason,
|
|
65
|
+
targetKey,
|
|
66
|
+
} from "../../combos";
|
|
67
|
+
import type { NormalizedComboConfig } from "../../combos/types";
|
|
68
|
+
import {
|
|
69
|
+
ProviderOutboundPolicyError,
|
|
70
|
+
providerOutboundGet,
|
|
71
|
+
providerOutboundPost,
|
|
72
|
+
providerRedirectError,
|
|
73
|
+
} from "../../lib/provider-outbound";
|
|
74
|
+
import { redactSecretString } from "../../lib/redact";
|
|
75
|
+
import {
|
|
76
|
+
extractProviderModelItems,
|
|
77
|
+
isRegistryModelDiscoveryUrl,
|
|
78
|
+
readBoundedDiscoveryJson,
|
|
79
|
+
resolveProviderModelDiscovery,
|
|
80
|
+
type ModelDiscoveryResponseFailure,
|
|
81
|
+
type ProviderModelsApiItem,
|
|
82
|
+
type ResolvedProviderModelDiscovery,
|
|
83
|
+
} from "../../providers/model-discovery";
|
|
84
|
+
import { extractGoogleAiStudioModelItems } from "../../providers/google-ai-studio-model-discovery";
|
|
85
|
+
import { applyConfiguredHeadersLast, fetchOllamaShowEnrichment, ollamaShowEnrichable } from "../../providers/ollama-show";
|
|
86
|
+
import upstreamModelsSnapshot from "../data/upstream-models.json";
|
|
87
|
+
import { createAdmissionGate, ResourceAdmissionError, type AdmissionMetrics } from "../../lib/admission";
|
|
88
|
+
import { CODEX_CUSTOM_MODEL_CATALOG_KIND, JAWCODE_CATALOG_AUGMENT_PROVIDERS, catalogModelSlug, shouldExposeRoutedModel } from "./parsing";
|
|
89
|
+
import type { CatalogModel } from "./parsing";
|
|
90
|
+
import { disabledNativeSlugs, hasComboTargets, hasNativeOpenAiCapabilityMetadata, NATIVE_GPT56_MAX_INPUT_TOKENS, nativeContextLimits, nativeOpenAiCapabilityDisplayName, nativeDefaultReasoningEffort, nativeInputModalities, nativeOpenAiAutoCompactTokenLimit, nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens, nativeOpenAiMaxOutputTokens, nativeOpenAiSlugs, nativeParallelToolCalls, nativeReasoningEfforts } from "./metadata";
|
|
91
|
+
import { deriveComboCatalogModel, normalizedOpenAiApiSignature, openAiApiCollisionWarnings, replaceLastComboCatalogOmissions, warnUncataloguedComboOnce } from "./aggregation";
|
|
92
|
+
import type { ComboCatalogOmission } from "./aggregation";
|
|
93
|
+
import type { CatalogGatherProviderAuthEvidence } from "./filesystem-evidence";
|
|
94
|
+
import type {
|
|
95
|
+
CatalogAdmissionSnapshot,
|
|
96
|
+
CatalogDiscoveryPolicyField,
|
|
97
|
+
CatalogGatherAuthorityIdentity,
|
|
98
|
+
CatalogProviderDiscoveryPolicySnapshot,
|
|
99
|
+
CatalogProcessLocalEvidence,
|
|
100
|
+
CatalogSourceEvidence,
|
|
101
|
+
CatalogTrustedOpenAiApiPolicySnapshot,
|
|
102
|
+
} from "../convergence-types";
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Fill the registry seed's per-model numeric capability maps beneath the provider's own
|
|
107
|
+
* values, mutating `prov` in place. The merge is per key — an operator's entry always
|
|
108
|
+
* wins; a model the persisted map never mentions picks up its seed value — matching
|
|
109
|
+
* `mergeRecordFill` in src/router.ts exactly.
|
|
110
|
+
*
|
|
111
|
+
* Routing already performs this fill at resolve time (routedProviderConfig in
|
|
112
|
+
* src/router.ts) and the catalog did not, and that divergence is #4570:
|
|
113
|
+
* zhipu-bigmodel-coding/glm-5.3-flash reached the live catalog with correct modalities
|
|
114
|
+
* but no context window, because an install persisted before Flash joined the seed map
|
|
115
|
+
* held a truthy partial `modelContextWindows` that shadowed the whole seed.
|
|
116
|
+
*
|
|
117
|
+
* This lives here and not in enrichProviderFromRegistry because enrichment output is
|
|
118
|
+
* persisted on a management POST, and #1409 (pinned by
|
|
119
|
+
* tests/server/management-provider-validation.test.ts) requires that a save never write
|
|
120
|
+
* registry seed keys into the operator's config. The gather clone is detached and
|
|
121
|
+
* frozen, never saved, so the catalog can see the seed without the config gaining it.
|
|
122
|
+
*/
|
|
123
|
+
export function applyRegistryCapabilitySeedFill(name: string, prov: OcxProviderConfig): void {
|
|
124
|
+
// router.ts resolves the canonical OpenAI API provider's token maps with
|
|
125
|
+
// mergePositiveNumberCaps (user values cap the seed rather than replace it), so a
|
|
126
|
+
// plain fill here would give that one provider catalog semantics routing never has.
|
|
127
|
+
if (name === OPENAI_API_PROVIDER_ID) return;
|
|
128
|
+
if (!providerMatchesRegistryTransport(name, prov)) return;
|
|
129
|
+
const entry = getProviderRegistryEntry(name);
|
|
130
|
+
if (!entry) return;
|
|
131
|
+
if (entry.modelContextWindows || prov.modelContextWindows) {
|
|
132
|
+
prov.modelContextWindows = { ...(entry.modelContextWindows ?? {}), ...(prov.modelContextWindows ?? {}) };
|
|
133
|
+
}
|
|
134
|
+
if (entry.modelMaxOutputTokens || prov.modelMaxOutputTokens) {
|
|
135
|
+
prov.modelMaxOutputTokens = { ...(entry.modelMaxOutputTokens ?? {}), ...(prov.modelMaxOutputTokens ?? {}) };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const NUMERIC_MODEL_ID_SEGMENT = /^\d+$/;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Resolve an unknown Claude point release or date pin from the nearest configured
|
|
142
|
+
* family row. Only numeric tail segments are removed so unrelated model families
|
|
143
|
+
* cannot inherit one another's limits.
|
|
144
|
+
*/
|
|
145
|
+
function anthropicFamilyContextWindow(
|
|
146
|
+
record: Record<string, number> | undefined,
|
|
147
|
+
id: string,
|
|
148
|
+
): number | undefined {
|
|
149
|
+
if (!record || !id.toLowerCase().startsWith("claude-")) return undefined;
|
|
150
|
+
let candidate = id;
|
|
151
|
+
while (true) {
|
|
152
|
+
const cut = candidate.lastIndexOf("-");
|
|
153
|
+
if (cut <= 0 || !NUMERIC_MODEL_ID_SEGMENT.test(candidate.slice(cut + 1))) return undefined;
|
|
154
|
+
candidate = candidate.slice(0, cut);
|
|
155
|
+
const value = modelRecordValue(record, candidate);
|
|
156
|
+
if (typeof value === "number" && value > 0) return value;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Resolve the configured context window in exact-model, Anthropic numeric-family,
|
|
162
|
+
* then provider-wide order. Return undefined when the selected value is not positive.
|
|
163
|
+
*/
|
|
164
|
+
export function configuredContextWindow(prov: OcxProviderConfig, id: string): number | undefined {
|
|
165
|
+
const configured = modelRecordValue(prov.modelContextWindows, id)
|
|
166
|
+
?? (prov.adapter === "anthropic" ? anthropicFamilyContextWindow(prov.modelContextWindows, id) : undefined)
|
|
167
|
+
?? prov.contextWindow;
|
|
168
|
+
return typeof configured === "number" && configured > 0 ? configured : undefined;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function configuredInputModalities(prov: OcxProviderConfig, id: string): string[] | undefined {
|
|
172
|
+
const declared = Object.hasOwn(prov.modelCapabilities ?? {}, id)
|
|
173
|
+
? prov.modelCapabilities?.[id]?.inputModalities : undefined;
|
|
174
|
+
const modalities = declared ?? modelRecordValue(prov.modelInputModalities, id);
|
|
175
|
+
return Array.isArray(modalities) && modalities.length > 0 ? [...modalities] : undefined;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Exact display-only override for one provider-native model id. */
|
|
179
|
+
export function configuredModelDisplayName(
|
|
180
|
+
prov: OcxProviderConfig,
|
|
181
|
+
id: string,
|
|
182
|
+
): string | undefined {
|
|
183
|
+
if (!prov.modelDisplayNames || !Object.hasOwn(prov.modelDisplayNames, id)) return undefined;
|
|
184
|
+
const value = prov.modelDisplayNames[id];
|
|
185
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function configuredMaxInputTokens(prov: OcxProviderConfig, id: string): number | undefined {
|
|
189
|
+
const configured = modelRecordValue(prov.modelMaxInputTokens, id);
|
|
190
|
+
return typeof configured === "number" && configured > 0 ? configured : undefined;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function generatedMaxOutputTokens(
|
|
194
|
+
providerName: string,
|
|
195
|
+
id: string,
|
|
196
|
+
metadataId = id,
|
|
197
|
+
metadataModelIdCaseFold?: boolean,
|
|
198
|
+
): number | undefined {
|
|
199
|
+
const metadataProvider = providerName === OPENAI_API_PROVIDER_ID || providerName === OPENAI_CODEX_PROVIDER_ID
|
|
200
|
+
? "openai"
|
|
201
|
+
: resolveMetadataProvider(providerName);
|
|
202
|
+
if (!metadataProvider) return undefined;
|
|
203
|
+
const metadata = getModelMetadata(metadataProvider, metadataId)
|
|
204
|
+
?? ((metadataModelIdCaseFold ?? (providerName === OPENAI_API_PROVIDER_ID || providerName === OPENAI_CODEX_PROVIDER_ID
|
|
205
|
+
? false
|
|
206
|
+
: shouldCaseFoldMetadataModelId(providerName)))
|
|
207
|
+
? getModelMetadataCaseInsensitive(metadataProvider, metadataId)
|
|
208
|
+
: undefined);
|
|
209
|
+
return positiveSafeInteger(metadata?.maxTokens);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function routedMaxOutputTokens(
|
|
213
|
+
providerName: string,
|
|
214
|
+
provider: OcxProviderConfig,
|
|
215
|
+
model: CatalogModel,
|
|
216
|
+
metadataId = model.id,
|
|
217
|
+
metadataModelIdCaseFold?: boolean,
|
|
218
|
+
): number | undefined {
|
|
219
|
+
const discovered = positiveSafeInteger(model.maxOutputTokens);
|
|
220
|
+
const generated = generatedMaxOutputTokens(providerName, model.id, metadataId, metadataModelIdCaseFold);
|
|
221
|
+
const configured = positiveSafeInteger(
|
|
222
|
+
modelRecordValue(provider.modelMaxOutputTokens, model.id),
|
|
223
|
+
);
|
|
224
|
+
const authoritative = discovered ?? generated;
|
|
225
|
+
if (configured === undefined) return authoritative;
|
|
226
|
+
return authoritative === undefined
|
|
227
|
+
? configured
|
|
228
|
+
: Math.min(authoritative, configured);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function configuredAutoCompactTokenLimit(
|
|
232
|
+
prov: OcxProviderConfig | undefined,
|
|
233
|
+
id: string,
|
|
234
|
+
): number | undefined {
|
|
235
|
+
if (!prov) return undefined;
|
|
236
|
+
const configured = modelRecordValue(prov.modelAutoCompactTokenLimits, id);
|
|
237
|
+
return typeof configured === "number" && Number.isSafeInteger(configured) && configured > 0
|
|
238
|
+
? configured
|
|
239
|
+
: undefined;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function configuredReasoningSummarySupport(prov: OcxProviderConfig | undefined, id: string): boolean | undefined {
|
|
243
|
+
if (!prov) return undefined;
|
|
244
|
+
const explicit = modelRecordValue(prov.modelSupportsReasoningSummaries, id);
|
|
245
|
+
if (explicit !== undefined) return explicit;
|
|
246
|
+
return modelRecordValue(prov.modelReasoningSummaryDelivery, id) !== undefined ? true : undefined;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function configuredVerbositySupport(name: string, prov: OcxProviderConfig | undefined, id: string): boolean | undefined {
|
|
250
|
+
const explicit = prov ? modelRecordValue(prov.modelSupportsVerbosity, id) : undefined;
|
|
251
|
+
if (explicit !== undefined) return explicit;
|
|
252
|
+
if (!prov) return undefined;
|
|
253
|
+
void name;
|
|
254
|
+
// Provider-wide fallback for ids the per-model map does not enumerate — a live-discovered
|
|
255
|
+
// model would otherwise re-advertise a control the upstream accepts and ignores.
|
|
256
|
+
//
|
|
257
|
+
// Read from the PROVIDER CONFIG, never from PROVIDER_REGISTRY. A gather flight captures its
|
|
258
|
+
// registry authority up front and forbids any later registry read, so consulting the registry
|
|
259
|
+
// here made a custom-destination flight fall back to "configured" instead of serving its own
|
|
260
|
+
// discovery result (tests/codex-integration/codex-gather-authority.test.ts). `applyVerbosityDefaults` in
|
|
261
|
+
// providers/derive.ts materializes the registry default into the config at seed/enrich time.
|
|
262
|
+
return prov.supportsVerbosity;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function applyProviderConfigHints(
|
|
266
|
+
name: string,
|
|
267
|
+
prov: OcxProviderConfig,
|
|
268
|
+
model: CatalogModel,
|
|
269
|
+
providerCap?: number,
|
|
270
|
+
metadataModelIdCaseFold?: boolean,
|
|
271
|
+
effectiveAlias?: string | null,
|
|
272
|
+
): CatalogModel {
|
|
273
|
+
const displayName = configuredModelDisplayName(prov, model.id);
|
|
274
|
+
// The alias decision is resolved once at flight admission (captureProviderGather) and threaded
|
|
275
|
+
// through as `effectiveAlias`. Re-deriving it here would read PROVIDER_REGISTRY after admission,
|
|
276
|
+
// which is exactly the authority leak tests/codex-integration/codex-gather-authority.test.ts
|
|
277
|
+
// forbids: a flight must not consult the live registry once its transport has been captured.
|
|
278
|
+
// When no decision was threaded in, carry whatever the row already resolved to instead.
|
|
279
|
+
const providerAlias = typeof effectiveAlias === "string" || effectiveAlias === null
|
|
280
|
+
? effectiveAlias
|
|
281
|
+
: model.providerAlias;
|
|
282
|
+
const configuredCap = configuredContextWindow(prov, model.id);
|
|
283
|
+
const configuredMaxInput = configuredMaxInputTokens(prov, model.id);
|
|
284
|
+
const maxOutputTokens = routedMaxOutputTokens(name, prov, model, model.id, metadataModelIdCaseFold);
|
|
285
|
+
const configuredAutoCompact = configuredAutoCompactTokenLimit(prov, model.id);
|
|
286
|
+
let inputModalities = configuredInputModalities(prov, model.id);
|
|
287
|
+
// The shared vision-sidecar consumer predicate keeps catalog advertisement and request-time
|
|
288
|
+
// planning aligned. The catalog must still advertise image input — the Codex app
|
|
289
|
+
// gates attachments client-side on input_modalities, and a text-only entry would block images
|
|
290
|
+
// before the sidecar ever runs ("This model does not support image inputs"). Discovery-derived
|
|
291
|
+
// text-only rows stay untouched: the runtime predicate only reads these two config sources, so
|
|
292
|
+
// it would not convert those.
|
|
293
|
+
const sidecarCovered = isModelVisionSidecarConsumer(prov, model.id);
|
|
294
|
+
if (sidecarCovered) {
|
|
295
|
+
const base = inputModalities ?? model.inputModalities ?? ["text"];
|
|
296
|
+
inputModalities = base.includes("image") ? [...base] : [...base, "image"];
|
|
297
|
+
}
|
|
298
|
+
const reasoningEfforts = configuredReasoningEfforts(prov, model.id);
|
|
299
|
+
const defaultReasoningEffort = modelRecordValue(prov.modelDefaultReasoningEfforts, model.id) ?? model.defaultReasoningEffort;
|
|
300
|
+
const supportsReasoningSummaries = configuredReasoningSummarySupport(prov, model.id);
|
|
301
|
+
const supportsVerbosity = configuredVerbositySupport(name, prov, model.id);
|
|
302
|
+
const fastPolicy = fastPolicyForModel(prov, model.id, name);
|
|
303
|
+
const supportsServiceTier = serviceTierSupportFromPolicy(fastPolicy);
|
|
304
|
+
const {
|
|
305
|
+
supportsServiceTier: _staleServiceTier,
|
|
306
|
+
fastTierDescription: _staleFastTierDescription,
|
|
307
|
+
providerAlias: _staleProviderAlias,
|
|
308
|
+
...modelWithoutServiceTier
|
|
309
|
+
} = model;
|
|
310
|
+
// 已发现窗口只允许被配置值压低;缺窗口时,已开的 Context cap 就是实际窗口。
|
|
311
|
+
const discoveredWindow = typeof model.contextWindow === "number" && model.contextWindow > 0
|
|
312
|
+
? model.contextWindow
|
|
313
|
+
: undefined;
|
|
314
|
+
const hintedWindow = discoveredWindow !== undefined
|
|
315
|
+
? (configuredCap !== undefined ? Math.min(discoveredWindow, configuredCap) : discoveredWindow)
|
|
316
|
+
: (configuredCap ?? (providerCap !== undefined ? resolveUnknownRoutedContextWindow(providerCap) : undefined));
|
|
317
|
+
const hinted = {
|
|
318
|
+
...modelWithoutServiceTier,
|
|
319
|
+
...(displayName !== undefined ? { displayName } : {}),
|
|
320
|
+
...(providerAlias !== undefined ? { providerAlias } : {}),
|
|
321
|
+
...(hintedWindow !== undefined ? { contextWindow: hintedWindow } : {}),
|
|
322
|
+
...(inputModalities ? { inputModalities } : {}),
|
|
323
|
+
...(reasoningEfforts !== undefined ? { reasoningEfforts } : {}),
|
|
324
|
+
...(configuredMaxInput !== undefined
|
|
325
|
+
? {
|
|
326
|
+
maxInputTokens: typeof model.maxInputTokens === "number" && model.maxInputTokens > 0
|
|
327
|
+
? Math.min(model.maxInputTokens, configuredMaxInput)
|
|
328
|
+
: configuredMaxInput,
|
|
329
|
+
}
|
|
330
|
+
: {}),
|
|
331
|
+
...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
|
|
332
|
+
...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
|
|
333
|
+
...(typeof supportsReasoningSummaries === "boolean" ? { supportsReasoningSummaries } : {}),
|
|
334
|
+
...(typeof supportsVerbosity === "boolean" ? { supportsVerbosity } : {}),
|
|
335
|
+
...(typeof supportsServiceTier === "boolean" ? { supportsServiceTier } : {}),
|
|
336
|
+
...(supportsServiceTier === true && fastPolicy.fastTierDescription !== undefined
|
|
337
|
+
? { fastTierDescription: fastPolicy.fastTierDescription }
|
|
338
|
+
: {}),
|
|
339
|
+
// Default-on for openai-chat providers (explicit false opts out); other adapters
|
|
340
|
+
// advertise only on explicit opt-in.
|
|
341
|
+
...(prov.parallelToolCalls === true || (prov.adapter === "openai-chat" && prov.parallelToolCalls !== false)
|
|
342
|
+
? { parallelToolCalls: true }
|
|
343
|
+
: {}),
|
|
344
|
+
...(prov.codexToolMode !== undefined ? { codexToolMode: prov.codexToolMode } : {}),
|
|
345
|
+
};
|
|
346
|
+
const capped = applyProviderContextCap(hinted.contextWindow, providerCap);
|
|
347
|
+
const withCap = providerCap !== undefined
|
|
348
|
+
? capped !== hinted.contextWindow
|
|
349
|
+
? { ...hinted, contextWindow: capped, contextCap: providerCap, contextCapped: true }
|
|
350
|
+
: { ...hinted, contextCap: providerCap, contextCapped: false }
|
|
351
|
+
: hinted;
|
|
352
|
+
const contextWindow = typeof withCap.contextWindow === "number" && withCap.contextWindow > 0
|
|
353
|
+
? withCap.contextWindow
|
|
354
|
+
: undefined;
|
|
355
|
+
const boundedMaxInput = typeof withCap.maxInputTokens === "number" && withCap.maxInputTokens > 0
|
|
356
|
+
? (contextWindow !== undefined ? Math.min(withCap.maxInputTokens, contextWindow) : withCap.maxInputTokens)
|
|
357
|
+
: undefined;
|
|
358
|
+
const withHardBounds = boundedMaxInput !== undefined && boundedMaxInput !== withCap.maxInputTokens
|
|
359
|
+
? { ...withCap, maxInputTokens: boundedMaxInput }
|
|
360
|
+
: withCap;
|
|
361
|
+
const softCandidates = [model.autoCompactTokenLimit, configuredAutoCompact]
|
|
362
|
+
.filter((value): value is number => typeof value === "number" && value > 0);
|
|
363
|
+
if (contextWindow === undefined || softCandidates.length === 0) return withHardBounds;
|
|
364
|
+
return {
|
|
365
|
+
...withHardBounds,
|
|
366
|
+
autoCompactTokenLimit: clampAutoCompactTokenLimit(
|
|
367
|
+
contextWindow,
|
|
368
|
+
boundedMaxInput,
|
|
369
|
+
Math.min(...softCandidates),
|
|
370
|
+
),
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export function catalogHintsFromProviderConfig(
|
|
375
|
+
name: string,
|
|
376
|
+
prov: OcxProviderConfig,
|
|
377
|
+
id: string,
|
|
378
|
+
contextCap?: number,
|
|
379
|
+
metadataModelIdCaseFold?: boolean,
|
|
380
|
+
effectiveAlias?: string | null,
|
|
381
|
+
): Partial<CatalogModel> {
|
|
382
|
+
const hinted = applyProviderConfigHints(name, prov, { id, provider: name }, contextCap, metadataModelIdCaseFold, effectiveAlias);
|
|
383
|
+
const { provider: _provider, id: _id, ...hints } = hinted;
|
|
384
|
+
return hints;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export function applyConfigHintsToCachedModels(
|
|
388
|
+
name: string,
|
|
389
|
+
prov: OcxProviderConfig,
|
|
390
|
+
models: CatalogModel[],
|
|
391
|
+
contextCap?: number,
|
|
392
|
+
metadataModelIdCaseFold?: boolean,
|
|
393
|
+
effectiveAlias?: string | null,
|
|
394
|
+
): CatalogModel[] {
|
|
395
|
+
return models.map(model => applyProviderConfigHints(name, prov, model, contextCap, metadataModelIdCaseFold, effectiveAlias));
|
|
396
|
+
}
|
|
397
|
+
export const QUIET_AUTHORITATIVE_CATALOG_PROVIDERS = new Set(["kimi", "xai"]);
|
|
398
|
+
|
|
399
|
+
export const CALLABLE_CONFIGURED_COMPATIBILITY_MODELS: Readonly<Record<string, ReadonlySet<string>>> = {
|
|
400
|
+
kimi: new Set([
|
|
401
|
+
"k3[1m]",
|
|
402
|
+
"kimi-k2.7-code",
|
|
403
|
+
"kimi-k2.7-code-highspeed",
|
|
404
|
+
"kimi-k2.6",
|
|
405
|
+
"kimi-k2.5",
|
|
406
|
+
]),
|
|
407
|
+
xai: new Set([
|
|
408
|
+
"grok-4.3",
|
|
409
|
+
"grok-4.20-multi-agent-0309",
|
|
410
|
+
"grok-4.20-0309-reasoning",
|
|
411
|
+
"grok-4.20-0309-non-reasoning",
|
|
412
|
+
"grok-build-0.1",
|
|
413
|
+
"grok-composer-2.5-fast",
|
|
414
|
+
]),
|
|
415
|
+
};
|
|
416
|
+
/**
|
|
417
|
+
* Z.AI and Neuralwatt advertise GLM reasoning as a bare boolean, which would otherwise
|
|
418
|
+
* collapse to the four-tier default ladder that omits `max`. These two helpers name the
|
|
419
|
+
* ladder each GLM generation actually honours on the wire.
|
|
420
|
+
*/
|
|
421
|
+
/** GLM-5.2 and its 1M alias: the full five-tier ladder including `max`. */
|
|
422
|
+
export function isGlm52ModelId(id: string): boolean {
|
|
423
|
+
const normalized = id.trim().toLowerCase();
|
|
424
|
+
return normalized === "glm-5.2" || normalized === "glm-5.2[1m]";
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* GLM-5.3 and its 1M alias. 260814: docs.z.ai/devpack/latest-model folds every incoming
|
|
428
|
+
* effort into three effective tiers (low/minimal/light -> low, medium/high -> high,
|
|
429
|
+
* xhigh/max/ultra -> max), so a boolean capability must not be expanded to five rows.
|
|
430
|
+
*/
|
|
431
|
+
export function isGlm53ModelId(id: string): boolean {
|
|
432
|
+
const normalized = id.trim().toLowerCase();
|
|
433
|
+
return normalized === "glm-5.3" || normalized === "glm-5.3[1m]";
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function plainRecord(value: unknown): Record<string, unknown> | undefined {
|
|
437
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
438
|
+
? value as Record<string, unknown>
|
|
439
|
+
: undefined;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const MODEL_DISCOVERY_METADATA_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/;
|
|
443
|
+
|
|
444
|
+
export function positiveSafeInteger(...values: unknown[]): number | undefined {
|
|
445
|
+
return values.find(value => typeof value === "number" && Number.isSafeInteger(value) && value > 0) as number | undefined;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function normalizedMetadataString(raw: string, maxLength: number): string | undefined {
|
|
449
|
+
if (raw.length > maxLength * 4 || MODEL_DISCOVERY_METADATA_CONTROL_CHARS.test(raw)) return undefined;
|
|
450
|
+
const normalized = raw.trim().toLowerCase().replace(/\s+/g, "-").slice(0, maxLength);
|
|
451
|
+
return normalized || undefined;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function normalizedStringList(value: unknown, maxItems = 32, maxLength = 64): string[] | undefined {
|
|
455
|
+
if (!Array.isArray(value)) return undefined;
|
|
456
|
+
const out: string[] = [];
|
|
457
|
+
const maxInspectedItems = Math.max(maxItems * 8, maxItems);
|
|
458
|
+
for (let i = 0; i < value.length && i < maxInspectedItems; i += 1) {
|
|
459
|
+
const raw = value[i];
|
|
460
|
+
if (typeof raw !== "string") continue;
|
|
461
|
+
const normalized = normalizedMetadataString(raw, maxLength);
|
|
462
|
+
if (normalized && !out.includes(normalized)) out.push(normalized);
|
|
463
|
+
if (out.length >= maxItems) break;
|
|
464
|
+
}
|
|
465
|
+
return out.length > 0 ? out : undefined;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export function modelCapabilities(item: ProviderModelsApiItem): string[] | undefined {
|
|
469
|
+
const metadata = plainRecord(item.metadata);
|
|
470
|
+
const metadataCapabilities = metadata?.capabilities;
|
|
471
|
+
const capabilityRecord = plainRecord(metadataCapabilities)
|
|
472
|
+
?? plainRecord(item.capabilities)
|
|
473
|
+
?? plainRecord(item.features);
|
|
474
|
+
const out = new Set<string>();
|
|
475
|
+
for (const list of [item.capabilities, item.features, item.supported_features, metadataCapabilities]) {
|
|
476
|
+
for (const capability of normalizedStringList(list) ?? []) out.add(capability);
|
|
477
|
+
}
|
|
478
|
+
const capabilityFields = capabilityRecord ?? {};
|
|
479
|
+
let inspectedCapabilityFields = 0;
|
|
480
|
+
for (const key in capabilityFields) {
|
|
481
|
+
if (!Object.hasOwn(capabilityFields, key)) continue;
|
|
482
|
+
inspectedCapabilityFields += 1;
|
|
483
|
+
if (inspectedCapabilityFields > 256 || out.size >= 32) break;
|
|
484
|
+
if (capabilityFields[key] === true) {
|
|
485
|
+
const normalized = normalizedMetadataString(key, 64);
|
|
486
|
+
if (normalized) out.add(normalized);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
for (const field of ["supports_tools", "supports_tool_calling", "supports_function_calling"] as const) {
|
|
490
|
+
if (item[field] === true) out.add("tools");
|
|
491
|
+
}
|
|
492
|
+
for (const field of ["supports_reasoning", "reasoning"] as const) {
|
|
493
|
+
if (item[field] === true) out.add("reasoning");
|
|
494
|
+
}
|
|
495
|
+
return out.size > 0 ? [...out].filter(Boolean).slice(0, 32) : undefined;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export function modelInputModalities(
|
|
499
|
+
item: ProviderModelsApiItem,
|
|
500
|
+
capabilities: readonly string[] | undefined,
|
|
501
|
+
): string[] | undefined {
|
|
502
|
+
const metadata = plainRecord(item.metadata);
|
|
503
|
+
const capabilityRecord = plainRecord(metadata?.capabilities)
|
|
504
|
+
?? plainRecord(item.capabilities)
|
|
505
|
+
?? plainRecord(item.features);
|
|
506
|
+
const explicit = normalizedStringList(
|
|
507
|
+
item.input_modalities
|
|
508
|
+
?? item.modalities
|
|
509
|
+
?? metadata?.input_modalities
|
|
510
|
+
?? capabilityRecord?.input_modalities
|
|
511
|
+
?? plainRecord(item.architecture)?.input_modalities,
|
|
512
|
+
8,
|
|
513
|
+
24,
|
|
514
|
+
)?.filter(value => (
|
|
515
|
+
// Codex parses `input_modalities` as a closed enum of text | image | audio. A provider that
|
|
516
|
+
// advertises anything else (zenmux reports "video") must not reach the catalog: Codex rejects
|
|
517
|
+
// the whole file, so plugins, apps and MCP servers all stop loading over one model's metadata.
|
|
518
|
+
value === "text" || value === "image" || value === "audio"
|
|
519
|
+
));
|
|
520
|
+
if (explicit && explicit.length > 0) return explicit;
|
|
521
|
+
const architecture = plainRecord(item.architecture);
|
|
522
|
+
const architectureModality = typeof architecture?.modality === "string"
|
|
523
|
+
? normalizedMetadataString(architecture.modality, 64)
|
|
524
|
+
: undefined;
|
|
525
|
+
if (architectureModality?.includes("->")) {
|
|
526
|
+
const [rawInput = ""] = architectureModality.split("->");
|
|
527
|
+
const inferred = rawInput
|
|
528
|
+
.split("+")
|
|
529
|
+
.filter(value => value === "text" || value === "image" || value === "audio");
|
|
530
|
+
if (inferred.length > 0) return [...new Set(inferred)];
|
|
531
|
+
}
|
|
532
|
+
// GitHub Copilot nests vision support one level down as `capabilities.supports.vision`, so the
|
|
533
|
+
// flat read alone finds nothing and every Copilot model falls through to `["text"]` — Codex then
|
|
534
|
+
// refuses image attachments on models that accept them (#2941). Precedence is by specificity:
|
|
535
|
+
// a flat boolean is authoritative when present, the nested boolean is consulted only otherwise,
|
|
536
|
+
// and a non-boolean at either level decides NOTHING so the signals below still apply. Two things
|
|
537
|
+
// this ordering deliberately avoids: a deny-wins rule across both levels would flip a provider
|
|
538
|
+
// reporting flat `true` with nested `false` from image-capable to text-only, changing behaviour
|
|
539
|
+
// that predates Copilot support; and a truthy test would let the string `"no"` advertise image
|
|
540
|
+
// input. The payload also carries a SECOND `vision` key under `limits` holding an image count,
|
|
541
|
+
// which is why this reads one exact path instead of searching `capabilities` for a vision-ish key.
|
|
542
|
+
const nestedSupports = plainRecord(capabilityRecord?.supports);
|
|
543
|
+
const explicitVisionSupport = typeof capabilityRecord?.vision === "boolean"
|
|
544
|
+
? capabilityRecord.vision
|
|
545
|
+
: typeof nestedSupports?.vision === "boolean"
|
|
546
|
+
? nestedSupports.vision
|
|
547
|
+
: undefined;
|
|
548
|
+
if (explicitVisionSupport === false) return ["text"];
|
|
549
|
+
if (explicitVisionSupport === true || capabilities?.some(value => (
|
|
550
|
+
value === "vision" || value === "image-input" || value === "image_input"
|
|
551
|
+
// llama.cpp and Ollama-compatible servers report vision as "multimodal" —
|
|
552
|
+
// it is the only image signal those servers emit (#1797). Mapped to the
|
|
553
|
+
// closed `text|image` enum rather than passed through: an out-of-enum
|
|
554
|
+
// modality makes Codex reject the entire catalog file.
|
|
555
|
+
|| value === "multimodal"
|
|
556
|
+
))) {
|
|
557
|
+
return ["text", "image"];
|
|
558
|
+
}
|
|
559
|
+
return undefined;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* A per-token rate exactly as a /models row publishes it, or undefined when the value is not a
|
|
564
|
+
* usable non-negative number. Providers ship these both as JSON numbers and as decimal strings —
|
|
565
|
+
* OpenRouter encodes free as the string `"0.00000000"` — so both shapes are accepted and nothing
|
|
566
|
+
* else is. The explicit numeric-shape test has to run BEFORE any coercion: `Number("")` and
|
|
567
|
+
* `Number(" ")` are both 0 and `Number(true)` is 1, so a bare `Number(value)` would classify a
|
|
568
|
+
* row with an empty price string as free.
|
|
569
|
+
*/
|
|
570
|
+
const DISCOVERED_PRICING_RATE_PATTERN = /^-?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?$/;
|
|
571
|
+
|
|
572
|
+
function discoveredPricingRate(value: unknown): number | undefined {
|
|
573
|
+
const numeric = typeof value === "number"
|
|
574
|
+
? value
|
|
575
|
+
: typeof value === "string" && DISCOVERED_PRICING_RATE_PATTERN.test(value.trim())
|
|
576
|
+
? Number(value.trim())
|
|
577
|
+
: undefined;
|
|
578
|
+
if (numeric === undefined || !Number.isFinite(numeric) || numeric < 0) return undefined;
|
|
579
|
+
return numeric;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Cost class for one discovered row, read from the provider's own `pricing` object (#3666).
|
|
584
|
+
*
|
|
585
|
+
* Fail closed. Only a complete pair of non-negative numeric rates classifies at all; a missing,
|
|
586
|
+
* one-sided, non-numeric, or negative rate is "unknown" and therefore excluded from a free-only
|
|
587
|
+
* filter. Showing a paid model under a Free filter spends the user's money, while hiding a free
|
|
588
|
+
* one costs a click.
|
|
589
|
+
*
|
|
590
|
+
* Two things that look like evidence and are not. A `:free` id suffix is an OpenRouter naming
|
|
591
|
+
* convention, not a price — Nous ships `:free` slugs on a provider whose `freeTier` is false on
|
|
592
|
+
* purpose. And the operator's own `modelCosts` overlay is an estimate they typed, not something
|
|
593
|
+
* the provider published, so a zeroed overlay never reaches this field either.
|
|
594
|
+
*
|
|
595
|
+
* Classification is on numeric zero and never on a unit conversion: OpenRouter quotes USD per
|
|
596
|
+
* token while the cost overlays and the jawcode bundle quote per 1M, and zero is zero in both.
|
|
597
|
+
*/
|
|
598
|
+
export function discoveredPricingStatus(item: ProviderModelsApiItem): "free" | "paid" | "unknown" {
|
|
599
|
+
const pricing = plainRecord(item.pricing) ?? plainRecord(plainRecord(item.metadata)?.pricing);
|
|
600
|
+
if (!pricing) return "unknown";
|
|
601
|
+
const prompt = discoveredPricingRate(pricing.prompt ?? pricing.input);
|
|
602
|
+
const completion = discoveredPricingRate(pricing.completion ?? pricing.output);
|
|
603
|
+
if (prompt === undefined || completion === undefined) return "unknown";
|
|
604
|
+
return prompt === 0 && completion === 0 ? "free" : "paid";
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
export function catalogHintsFromModelsApiItem(providerName: string, item: ProviderModelsApiItem): Partial<CatalogModel> {
|
|
608
|
+
const metadata = plainRecord(item.metadata);
|
|
609
|
+
const capabilityRecord = plainRecord(metadata?.capabilities) ?? plainRecord(item.capabilities);
|
|
610
|
+
const limits = plainRecord(metadata?.limits);
|
|
611
|
+
const capabilityLimits = plainRecord(plainRecord(item.capabilities)?.limits);
|
|
612
|
+
const contextWindow =
|
|
613
|
+
positiveSafeInteger(
|
|
614
|
+
limits?.max_context_length,
|
|
615
|
+
// GitHub Copilot reports the live context window here instead of in the metadata or
|
|
616
|
+
// top-level fields used by other OpenAI-compatible catalogs (#3156). Keep the existing
|
|
617
|
+
// metadata field authoritative when both are present: adding this provider-specific
|
|
618
|
+
// fallback must not change previously recognized providers.
|
|
619
|
+
capabilityLimits?.max_context_window_tokens,
|
|
620
|
+
metadata?.context_length,
|
|
621
|
+
item.context_length,
|
|
622
|
+
item.context_size,
|
|
623
|
+
item.max_model_len,
|
|
624
|
+
item.max_context_length,
|
|
625
|
+
// llama.cpp reports the served context under `meta`: `n_ctx` is what the
|
|
626
|
+
// server was actually started with, `n_ctx_train` the model's trained
|
|
627
|
+
// maximum. Prefer the served value — routing must not promise a window the
|
|
628
|
+
// running server will refuse. Both come LAST so no provider already
|
|
629
|
+
// supplying a recognized field changes behavior (#1797).
|
|
630
|
+
plainRecord(item.meta)?.n_ctx,
|
|
631
|
+
plainRecord(item.meta)?.n_ctx_train,
|
|
632
|
+
// A chained OpenCodex hub (and other re-serving gateways) reports the per-model
|
|
633
|
+
// window on the same capability record this function already reads for
|
|
634
|
+
// `max_output_tokens` below (#4032). Without it every routed row fell through to
|
|
635
|
+
// the 128k compatibility floor in parsing.ts while local forward rows kept their
|
|
636
|
+
// real values. Appended after the recognized fields for the same reason as the
|
|
637
|
+
// llama.cpp entries above: no provider that already resolves changes behavior.
|
|
638
|
+
capabilityRecord?.context_length,
|
|
639
|
+
);
|
|
640
|
+
const maxInputTokens = positiveSafeInteger(limits?.max_input_tokens, item.max_input_tokens);
|
|
641
|
+
const maxOutputTokens = positiveSafeInteger(
|
|
642
|
+
capabilityRecord?.max_output_tokens,
|
|
643
|
+
limits?.max_output_tokens,
|
|
644
|
+
metadata?.max_output_tokens,
|
|
645
|
+
item.max_output_tokens,
|
|
646
|
+
);
|
|
647
|
+
// Some OpenAI-compatible catalogs expose the selectable ladder under
|
|
648
|
+
// `reasoning_parameters.efforts` instead of the older `reasoning_efforts` key.
|
|
649
|
+
// Treat both as model metadata: otherwise a valid upstream capability disappears
|
|
650
|
+
// before client exporters (including omp) can advertise it.
|
|
651
|
+
const reasoningParameters = plainRecord(item.reasoning_parameters)
|
|
652
|
+
?? plainRecord(metadata?.reasoning_parameters)
|
|
653
|
+
?? plainRecord(capabilityRecord?.reasoning_parameters);
|
|
654
|
+
const rawReasoningEfforts = capabilityRecord?.reasoning_effort
|
|
655
|
+
?? item.reasoning_efforts
|
|
656
|
+
?? reasoningParameters?.efforts;
|
|
657
|
+
const listedReasoningEfforts = normalizedStringList(rawReasoningEfforts, 8, 24);
|
|
658
|
+
const reasoningEfforts = listedReasoningEfforts
|
|
659
|
+
? sanitizeCodexReasoningEfforts(listedReasoningEfforts)
|
|
660
|
+
: typeof rawReasoningEfforts === "boolean"
|
|
661
|
+
? (rawReasoningEfforts
|
|
662
|
+
? ((providerName === "neuralwatt" || providerName === "zai") && isGlm53ModelId(item.id)
|
|
663
|
+
? ["low", "high", "max"]
|
|
664
|
+
: (providerName === "neuralwatt" || providerName === "zai") && isGlm52ModelId(item.id)
|
|
665
|
+
? ["low", "medium", "high", "xhigh", "max"]
|
|
666
|
+
: ["low", "medium", "high", "xhigh"])
|
|
667
|
+
: [])
|
|
668
|
+
: undefined;
|
|
669
|
+
const capabilities = modelCapabilities(item);
|
|
670
|
+
const inputModalities = modelInputModalities(item, capabilities);
|
|
671
|
+
const pricingStatus = discoveredPricingStatus(item);
|
|
672
|
+
return {
|
|
673
|
+
...(contextWindow && contextWindow > 0 ? { contextWindow } : {}),
|
|
674
|
+
...(maxInputTokens && maxInputTokens > 0 ? { maxInputTokens } : {}),
|
|
675
|
+
...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
|
|
676
|
+
...(reasoningEfforts !== undefined ? { reasoningEfforts } : {}),
|
|
677
|
+
...(inputModalities ? { inputModalities } : {}),
|
|
678
|
+
...(capabilities ? { capabilities } : {}),
|
|
679
|
+
// Omitted when the classification is "unknown", following this function's existing
|
|
680
|
+
// contract that an unknown property is absent rather than present-and-empty. Callers
|
|
681
|
+
// that need to tell "provider published no prices" from "this build does not classify"
|
|
682
|
+
// call discoveredPricingStatus directly.
|
|
683
|
+
...(pricingStatus !== "unknown" ? { pricingStatus } : {}),
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
export function boundedOwnedBy(value: unknown): string | undefined {
|
|
688
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 256) return undefined;
|
|
689
|
+
if (MODEL_DISCOVERY_METADATA_CONTROL_CHARS.test(value)) return undefined;
|
|
690
|
+
return value;
|
|
691
|
+
}
|