@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,858 @@
|
|
|
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
|
+
import type { CatalogGatherProviderAuthOutcome, CatalogGatherProviderModelOutcome, GatherFlightCapture, ModelsAuthResolverFactory } from "./gather-capture";
|
|
104
|
+
import { applyProviderConfigHints, configuredAutoCompactTokenLimit, configuredMaxInputTokens, configuredReasoningSummarySupport, modelInputModalities, routedMaxOutputTokens } from "./model-hints";
|
|
105
|
+
import { resolveComboCatalogMember } from "./combo-member";
|
|
106
|
+
import { captureGatherFlight, captureTrustedOpenAiApiPolicy, gatherFlightKey, keyedGatherBytesIdentity, withCanonicalOpenAiForwardAuthDefault } from "./gather-capture";
|
|
107
|
+
import { fetchProviderModelsWithAuth, observedModelsAuthResolver, refreshingModelsAuthResolver } from "./provider-models";
|
|
108
|
+
|
|
109
|
+
export interface GatherRoutedModelsOptions {
|
|
110
|
+
comboOmissions?: ComboCatalogOmission[];
|
|
111
|
+
providerAuthOutcomes?: CatalogGatherProviderAuthOutcome[];
|
|
112
|
+
/** Flight-local authority of each provider's returned model rows. */
|
|
113
|
+
providerModelOutcomes?: CatalogGatherProviderModelOutcome[];
|
|
114
|
+
/** Internal convergence sink for the immutable policy that produced the returned rows. */
|
|
115
|
+
discoveryPolicySnapshots?: CatalogProviderDiscoveryPolicySnapshot[];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface GatherFlightResult {
|
|
119
|
+
models: CatalogModel[];
|
|
120
|
+
comboOmissions: ComboCatalogOmission[];
|
|
121
|
+
providerAuthOutcomes: readonly CatalogGatherProviderAuthOutcome[];
|
|
122
|
+
providerModelOutcomes: readonly CatalogGatherProviderModelOutcome[];
|
|
123
|
+
discoveryPolicySnapshots: readonly CatalogProviderDiscoveryPolicySnapshot[];
|
|
124
|
+
}
|
|
125
|
+
interface GatherInflightEntry {
|
|
126
|
+
readonly discoveryPolicyIdentity: string;
|
|
127
|
+
/**
|
|
128
|
+
* The credential half of the join decision.
|
|
129
|
+
*
|
|
130
|
+
* `gatherFlightKey`'s fingerprint carries endpoints and model lists but no
|
|
131
|
+
* `authMode`, key or headers, and discovery policy does not carry them either.
|
|
132
|
+
* Two admissions differing ONLY in credential therefore produced the same key
|
|
133
|
+
* and the same policy, so the second joined the first and published rows the
|
|
134
|
+
* old key had fetched — reproduced against the real routes by rotating a key
|
|
135
|
+
* through `/api/providers/keys` mid-flight.
|
|
136
|
+
*
|
|
137
|
+
* Now REDUNDANT with `providerGraphIdentity`, which hashes the whole provider
|
|
138
|
+
* row and therefore covers `apiKey` too: removing this term alone leaves the
|
|
139
|
+
* credential regression green. It is kept deliberately, for two reasons. It
|
|
140
|
+
* covers what the graph cannot — the RESOLVED auth (`observedAuth`) and the
|
|
141
|
+
* final materialized headers, which are derived rather than stored, so an
|
|
142
|
+
* OAuth token that changes while the row is byte-identical still separates
|
|
143
|
+
* admissions. And it states the credential rule where a reader looks for it,
|
|
144
|
+
* instead of leaving it as an emergent property of hashing everything.
|
|
145
|
+
*/
|
|
146
|
+
readonly authIdentity: string;
|
|
147
|
+
/**
|
|
148
|
+
* The whole admitted provider graph, not a chosen subset.
|
|
149
|
+
*
|
|
150
|
+
* `providerCatalogFingerprint` is an ALLOW-LIST, so every field it forgot was
|
|
151
|
+
* silently treated as equivalence: credentials leaked a flight until
|
|
152
|
+
* `authIdentity` landed, and `reasoningEfforts` leaked one after that — both
|
|
153
|
+
* reproduced against real routes. Enumerating fields cannot converge, because
|
|
154
|
+
* the next field added to a provider row inherits the same defect. This
|
|
155
|
+
* identity therefore covers the enriched, frozen provider objects the flight
|
|
156
|
+
* actually gathered from, so a join is refused unless the admissions agree on
|
|
157
|
+
* everything rather than on everything somebody remembered to list.
|
|
158
|
+
*/
|
|
159
|
+
readonly providerGraphIdentity: string;
|
|
160
|
+
readonly promise: Promise<GatherFlightResult>;
|
|
161
|
+
}
|
|
162
|
+
const gatherInflight = new Map<string, GatherInflightEntry[]>();
|
|
163
|
+
const MAX_CONCURRENT_CATALOG_GATHERS = 8;
|
|
164
|
+
const gatherGate = createAdmissionGate("catalog_gathers", MAX_CONCURRENT_CATALOG_GATHERS);
|
|
165
|
+
|
|
166
|
+
export class CatalogGatherBusyError extends ResourceAdmissionError {
|
|
167
|
+
override readonly code = "catalog_busy";
|
|
168
|
+
readonly retryAfterSeconds = 1;
|
|
169
|
+
constructor() {
|
|
170
|
+
super("catalog_gathers", MAX_CONCURRENT_CATALOG_GATHERS);
|
|
171
|
+
this.name = "CatalogGatherBusyError";
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function catalogGatherAdmissionMetrics(): AdmissionMetrics {
|
|
176
|
+
return gatherGate.metrics();
|
|
177
|
+
}
|
|
178
|
+
/** Drop in-flight gather so tests / full cache clears do not reuse a stale promise. */
|
|
179
|
+
export function clearGatherRoutedModelsInflight(): void {
|
|
180
|
+
gatherInflight.clear();
|
|
181
|
+
}
|
|
182
|
+
export async function gatherRoutedModels(
|
|
183
|
+
config: OcxConfig,
|
|
184
|
+
options?: GatherRoutedModelsOptions,
|
|
185
|
+
): Promise<CatalogModel[]> {
|
|
186
|
+
return gatherRoutedModelsWithAuth(
|
|
187
|
+
config,
|
|
188
|
+
`refreshing:${gatherFlightKey(config)}`,
|
|
189
|
+
() => refreshingModelsAuthResolver,
|
|
190
|
+
options,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Catalog-gather model discovery using only auth-store bytes already captured by the
|
|
196
|
+
* filesystem-evidence owner. This entry point never reaches the refreshing resolver.
|
|
197
|
+
*/
|
|
198
|
+
export async function gatherRoutedModelsForCatalogGather(
|
|
199
|
+
config: OcxConfig,
|
|
200
|
+
evidence: CatalogGatherProviderAuthEvidence,
|
|
201
|
+
options?: GatherRoutedModelsOptions,
|
|
202
|
+
): Promise<CatalogModel[]> {
|
|
203
|
+
const authStoreBuffer = evidence.authStoreBuffer === null
|
|
204
|
+
? null
|
|
205
|
+
: Uint8Array.from(evidence.authStoreBuffer);
|
|
206
|
+
const authIdentity = authStoreBuffer === null
|
|
207
|
+
? "absent"
|
|
208
|
+
: keyedGatherBytesIdentity("catalog-observed-auth-v1", authStoreBuffer);
|
|
209
|
+
return gatherRoutedModelsWithAuth(
|
|
210
|
+
config,
|
|
211
|
+
`observed:${authIdentity}:${gatherFlightKey(config)}`,
|
|
212
|
+
outcomes => observedModelsAuthResolver(authStoreBuffer, outcomes),
|
|
213
|
+
options,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function gatherRoutedModelsWithAuth(
|
|
218
|
+
config: OcxConfig,
|
|
219
|
+
key: string,
|
|
220
|
+
createAuthResolver: ModelsAuthResolverFactory,
|
|
221
|
+
options?: GatherRoutedModelsOptions,
|
|
222
|
+
): Promise<CatalogModel[]> {
|
|
223
|
+
const capture = captureGatherFlight(config, createAuthResolver);
|
|
224
|
+
const bucket = gatherInflight.get(key) ?? [];
|
|
225
|
+
let entry = bucket.find(candidate => (
|
|
226
|
+
candidate.discoveryPolicyIdentity === capture.discoveryPolicyIdentity
|
|
227
|
+
&& candidate.authIdentity === capture.authIdentity
|
|
228
|
+
&& candidate.providerGraphIdentity === capture.providerGraphIdentity
|
|
229
|
+
));
|
|
230
|
+
if (!entry) {
|
|
231
|
+
const lease = gatherGate.tryAcquire();
|
|
232
|
+
if (!lease) throw new CatalogGatherBusyError();
|
|
233
|
+
// Claim the slot synchronously before any await so same-key callers join this flight.
|
|
234
|
+
// Distinct authorities retain separate entries even when their legacy bucket matches.
|
|
235
|
+
let ownedEntry!: GatherInflightEntry;
|
|
236
|
+
const flight = gatherRoutedModelsUncached(config, capture).finally(() => {
|
|
237
|
+
const current = gatherInflight.get(key);
|
|
238
|
+
const index = current?.indexOf(ownedEntry) ?? -1;
|
|
239
|
+
if (current && index >= 0) current.splice(index, 1);
|
|
240
|
+
if (current?.length === 0) gatherInflight.delete(key);
|
|
241
|
+
lease.release();
|
|
242
|
+
});
|
|
243
|
+
ownedEntry = Object.freeze({
|
|
244
|
+
discoveryPolicyIdentity: capture.discoveryPolicyIdentity,
|
|
245
|
+
authIdentity: capture.authIdentity,
|
|
246
|
+
providerGraphIdentity: capture.providerGraphIdentity,
|
|
247
|
+
promise: flight,
|
|
248
|
+
});
|
|
249
|
+
bucket.push(ownedEntry);
|
|
250
|
+
gatherInflight.set(key, bucket);
|
|
251
|
+
entry = ownedEntry;
|
|
252
|
+
}
|
|
253
|
+
const {
|
|
254
|
+
models,
|
|
255
|
+
comboOmissions,
|
|
256
|
+
providerAuthOutcomes,
|
|
257
|
+
providerModelOutcomes,
|
|
258
|
+
discoveryPolicySnapshots,
|
|
259
|
+
} = await entry.promise;
|
|
260
|
+
if (options?.comboOmissions) {
|
|
261
|
+
options.comboOmissions.length = 0;
|
|
262
|
+
options.comboOmissions.push(...comboOmissions);
|
|
263
|
+
}
|
|
264
|
+
if (options?.providerAuthOutcomes) {
|
|
265
|
+
options.providerAuthOutcomes.length = 0;
|
|
266
|
+
options.providerAuthOutcomes.push(...providerAuthOutcomes);
|
|
267
|
+
}
|
|
268
|
+
if (options?.providerModelOutcomes) {
|
|
269
|
+
options.providerModelOutcomes.length = 0;
|
|
270
|
+
options.providerModelOutcomes.push(...providerModelOutcomes);
|
|
271
|
+
}
|
|
272
|
+
if (options?.discoveryPolicySnapshots) {
|
|
273
|
+
options.discoveryPolicySnapshots.length = 0;
|
|
274
|
+
options.discoveryPolicySnapshots.push(...discoveryPolicySnapshots);
|
|
275
|
+
}
|
|
276
|
+
return models;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Bound a custom row whose model id has pinned native Codex metadata, without changing stored configuration. */
|
|
280
|
+
function boundCustomNativeReasoning(
|
|
281
|
+
model: CatalogModel,
|
|
282
|
+
allowed: readonly string[],
|
|
283
|
+
nativeDefault: string | undefined,
|
|
284
|
+
): CatalogModel {
|
|
285
|
+
if (allowed.length === 0 || model.reasoningEfforts === undefined) return model;
|
|
286
|
+
const bounded = { ...model };
|
|
287
|
+
if (model.reasoningEfforts.length === 0) {
|
|
288
|
+
bounded.reasoningEfforts = [];
|
|
289
|
+
delete bounded.defaultReasoningEffort;
|
|
290
|
+
return bounded;
|
|
291
|
+
}
|
|
292
|
+
const declared = new Set(model.reasoningEfforts);
|
|
293
|
+
const surviving = [...new Set(allowed)].filter(effort => declared.has(effort));
|
|
294
|
+
const fallback = nativeDefault && allowed.includes(nativeDefault) ? nativeDefault : allowed[0]!;
|
|
295
|
+
// A nonempty but incompatible declaration is not an explicit no-reasoning setting.
|
|
296
|
+
bounded.reasoningEfforts = surviving.length > 0 ? surviving : [fallback];
|
|
297
|
+
bounded.defaultReasoningEffort = model.defaultReasoningEffort
|
|
298
|
+
&& bounded.reasoningEfforts.includes(model.defaultReasoningEffort)
|
|
299
|
+
? model.defaultReasoningEffort
|
|
300
|
+
: bounded.reasoningEfforts.includes(fallback) ? fallback : bounded.reasoningEfforts[0]!;
|
|
301
|
+
return bounded;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function gatherRoutedModelsUncached(
|
|
305
|
+
config: OcxConfig,
|
|
306
|
+
capture: GatherFlightCapture,
|
|
307
|
+
): Promise<GatherFlightResult> {
|
|
308
|
+
// Flight-local list: joiners copy from the resolved promise, not a process-global last write.
|
|
309
|
+
const localOmissions: ComboCatalogOmission[] = [];
|
|
310
|
+
const localProviderAuthOutcomes = capture.providerAuthOutcomes;
|
|
311
|
+
const resolveAuth = capture.authResolver;
|
|
312
|
+
const ttlMs = config.modelCacheTtlMs ?? DEFAULT_MODEL_CACHE_TTL_MS;
|
|
313
|
+
// Persisted provider entries can predate newer registry fields (noVisionModels,
|
|
314
|
+
// modelInputModalities, ...). The ROUTER merges registry seeds at request time
|
|
315
|
+
// (routedProviderConfig), so the proxy behaves correctly — the catalog listing must see the
|
|
316
|
+
// same merged view or its advertisements drift from actual proxy behavior (e.g. a
|
|
317
|
+
// vision-sidecar model advertised text-only, blocking image attachments app-side).
|
|
318
|
+
// Enrich a CLONE: hydrated defaults must never leak into the persisted config.
|
|
319
|
+
const activeProviders = capture.providers;
|
|
320
|
+
const providerResults = await Promise.all(
|
|
321
|
+
activeProviders.map(provider => fetchProviderModelsWithAuth(
|
|
322
|
+
provider,
|
|
323
|
+
ttlMs,
|
|
324
|
+
providerContextCap(config, provider.name),
|
|
325
|
+
resolveAuth,
|
|
326
|
+
)),
|
|
327
|
+
);
|
|
328
|
+
const lists = providerResults.map(result => result.models);
|
|
329
|
+
const apiAugmented = augmentRoutedModelsWithCapturedOpenAiApiRows(
|
|
330
|
+
lists.flat(),
|
|
331
|
+
config,
|
|
332
|
+
capture.openAiApiPolicy,
|
|
333
|
+
);
|
|
334
|
+
const apiProvider = activeProviders.find(provider => provider.name === OPENAI_API_PROVIDER_ID);
|
|
335
|
+
// Trusted reconstruction replaces whole rows, including the earlier Fast hints.
|
|
336
|
+
// Restore only that capability from the same captured authority used by discovery.
|
|
337
|
+
if (apiProvider) {
|
|
338
|
+
for (const model of apiAugmented) {
|
|
339
|
+
if (model.provider !== OPENAI_API_PROVIDER_ID) continue;
|
|
340
|
+
const policy = fastPolicyForModel(apiProvider.provider, model.id, apiProvider.name);
|
|
341
|
+
const supported = serviceTierSupportFromPolicy(policy);
|
|
342
|
+
if (supported !== undefined) model.supportsServiceTier = supported;
|
|
343
|
+
if (supported === true && policy.fastTierDescription !== undefined) model.fastTierDescription = policy.fastTierDescription;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const metadataModelIdCaseFoldByProvider = new Map(
|
|
347
|
+
activeProviders.map(provider => [provider.name, provider.metadataModelIdCaseFold]),
|
|
348
|
+
);
|
|
349
|
+
const all = augmentRoutedModelsWithMetadata(
|
|
350
|
+
apiAugmented,
|
|
351
|
+
activeProviders.map(provider => provider.name),
|
|
352
|
+
config.providers,
|
|
353
|
+
config,
|
|
354
|
+
metadataModelIdCaseFoldByProvider,
|
|
355
|
+
)
|
|
356
|
+
// Drop image/video generation models (e.g. Grok image/video) by default. Cursor's static catalog
|
|
357
|
+
// intentionally mirrors Cursor's public model table, including Gemini image preview, so the
|
|
358
|
+
// exposure decision goes through shouldExposeRoutedModel (single choke point).
|
|
359
|
+
.filter(shouldExposeRoutedModel);
|
|
360
|
+
const memberByKey = new Map(all.map(model => [`${model.provider}/${model.id}`, model]));
|
|
361
|
+
// [Decision Log]
|
|
362
|
+
// - 목적과 의도: 콤보 타겟에 native OpenAI(Codex login) 모델이 포함될 때 카탈로그에서
|
|
363
|
+
// 누락되는 버그(issue #268)를 수정. "openai" provider는 forward-auth(Codex login
|
|
364
|
+
// passthrough)이므로 fetchProviderModels가 항상 []를 반환하고, native slugs는
|
|
365
|
+
// 별도 정적 경로(nativeOpenAiSlugs)로만 노출됨. 따라서 memberByKey에
|
|
366
|
+
// openai/<slug> 키가 존재하지 않아 콤보가 조용히 drop됨.
|
|
367
|
+
// - 기존 구현 및 제약 조건: memberByKey는 routed provider /models fetch 결과로만 구성.
|
|
368
|
+
// - 검토한 주요 대안: (A) native slugs를 all 배열에 직접 push — /v1/models와 온디스크
|
|
369
|
+
// 카탈로그에서 native 모델이 중복 노출되는 부작용 발생. (B) memberByKey에만 synthetic
|
|
370
|
+
// CatalogModel을 주입 — 콤보 멤버 해석에만 사용하고 all에는 추가하지 않으므로 기존
|
|
371
|
+
// 노출 경로에 영향 없음.
|
|
372
|
+
// - 선택한 방식: (B) — synthetic entries를 memberByKey에만 주입.
|
|
373
|
+
// - 다른 대안 대신 이 방식을 선택한 이유: 기존 native 모델 노출 경로(/v1/models, 온디스크
|
|
374
|
+
// 카탈로그 sync, management API)를 전혀 변경하지 않고 콤보 resolution만 수선하기 때문.
|
|
375
|
+
// - 장점, 단점 및 영향: 장점 — 최소 수정, 기존 경로 무변경. 단점 — synthetic entries의
|
|
376
|
+
// capability 데이터가 static/upstream snapshot 기반이므로, 사용자가 커스텀 config
|
|
377
|
+
// 힌트(modelContextWindows 등)로 native 모델의 context window를 오버라이드한 경우
|
|
378
|
+
// 반영되지 않음. 하지만 nativeOpenAiContextWindow가 이미 config 오버라이드를
|
|
379
|
+
// 우선시하므로 실제 충돌 가능성은 낮음.
|
|
380
|
+
if (!hasComboTargets(config)) {
|
|
381
|
+
// Skip the native slug injection entirely when no combos are configured — avoids
|
|
382
|
+
// calling nativeOpenAiSlugs() (which reads the live Codex catalog from disk) for
|
|
383
|
+
// configs that will never need it.
|
|
384
|
+
} else {
|
|
385
|
+
const disabled = disabledNativeSlugs(config);
|
|
386
|
+
const openaiContextCap = nativeContextLimits(config);
|
|
387
|
+
const requiredNativeComboTargets = new Set(listComboIds(config).flatMap(id => {
|
|
388
|
+
const combo = getCombo(config, id);
|
|
389
|
+
return combo?.targets.flatMap(target => (
|
|
390
|
+
target.provider === "openai" ? [target.model] : []
|
|
391
|
+
)) ?? [];
|
|
392
|
+
}));
|
|
393
|
+
for (const slug of nativeOpenAiSlugs()) {
|
|
394
|
+
// A bare native disable key hides the native row, not a combo that targets it.
|
|
395
|
+
// Keep synthetic native metadata available to those combos.
|
|
396
|
+
if (disabled.has(slug) && !requiredNativeComboTargets.has(slug)) continue;
|
|
397
|
+
const contextWindow = nativeOpenAiContextWindow(slug, openaiContextCap);
|
|
398
|
+
if (contextWindow === undefined) continue;
|
|
399
|
+
const synthetic: CatalogModel = {
|
|
400
|
+
provider: "openai",
|
|
401
|
+
id: slug,
|
|
402
|
+
owned_by: "openai",
|
|
403
|
+
contextWindow,
|
|
404
|
+
// Input limit, not the total window. These coincide for native GPT-5.6 today (the
|
|
405
|
+
// advertised 922,000 window is already capped at its measured ceiling), but the two
|
|
406
|
+
// stay separate fields because routed/API rows of the same family run a wider window.
|
|
407
|
+
// Falls back to the window for slugs with no separate ceiling.
|
|
408
|
+
maxInputTokens: Math.min(nativeOpenAiMaxInputTokens(slug, openaiContextCap) ?? contextWindow, contextWindow),
|
|
409
|
+
...(nativeOpenAiMaxOutputTokens(slug) !== undefined
|
|
410
|
+
? { maxOutputTokens: nativeOpenAiMaxOutputTokens(slug) }
|
|
411
|
+
: {}),
|
|
412
|
+
autoCompactTokenLimit: nativeOpenAiAutoCompactTokenLimit(slug, openaiContextCap),
|
|
413
|
+
inputModalities: nativeInputModalities(slug),
|
|
414
|
+
reasoningEfforts: nativeReasoningEfforts(slug),
|
|
415
|
+
...(nativeParallelToolCalls(slug) ? { parallelToolCalls: true } : {}),
|
|
416
|
+
};
|
|
417
|
+
const key = `openai/${slug}`;
|
|
418
|
+
// Only inject when not already present from a routed provider (an API-key
|
|
419
|
+
// "openai" provider could shadow the native one).
|
|
420
|
+
if (!memberByKey.has(key)) memberByKey.set(key, synthetic);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
// Enriched (registry-hydrated) provider clones — shared by combo member synthesis and
|
|
424
|
+
// custom-model vision-sidecar inheritance so both see the same merged registry view.
|
|
425
|
+
const enrichedByName = new Map(activeProviders.map(provider => [provider.name, provider.provider]));
|
|
426
|
+
for (const id of listComboIds(config)) {
|
|
427
|
+
const combo = getCombo(config, id);
|
|
428
|
+
if (!combo) continue;
|
|
429
|
+
const comboNativeLimits = nativeContextLimits(config);
|
|
430
|
+
const nativeContextWindow = combo.nativeAlias && combo.alias
|
|
431
|
+
? nativeOpenAiContextWindow(combo.alias, comboNativeLimits)
|
|
432
|
+
: undefined;
|
|
433
|
+
const nativeAliasMaxInput = combo.nativeAlias && combo.alias
|
|
434
|
+
? (combo.alias.startsWith("gpt-5.6-") || combo.alias.includes("daybreak")
|
|
435
|
+
? NATIVE_GPT56_MAX_INPUT_TOKENS
|
|
436
|
+
: nativeOpenAiMaxInputTokens(combo.alias, comboNativeLimits) ?? nativeOpenAiContextWindow(combo.alias, comboNativeLimits))
|
|
437
|
+
: undefined;
|
|
438
|
+
const nativeAliasAutoCompact = combo.nativeAlias && combo.alias
|
|
439
|
+
? nativeOpenAiAutoCompactTokenLimit(combo.alias, comboNativeLimits)
|
|
440
|
+
: undefined;
|
|
441
|
+
const nativeAliasFallback = combo.nativeAlias && combo.alias && nativeContextWindow !== undefined
|
|
442
|
+
? {
|
|
443
|
+
contextWindow: nativeContextWindow,
|
|
444
|
+
...(nativeAliasMaxInput !== undefined ? { maxInputTokens: nativeAliasMaxInput } : {}),
|
|
445
|
+
...(nativeOpenAiMaxOutputTokens(combo.alias) !== undefined
|
|
446
|
+
? { maxOutputTokens: nativeOpenAiMaxOutputTokens(combo.alias) }
|
|
447
|
+
: {}),
|
|
448
|
+
...(nativeAliasAutoCompact !== undefined ? { autoCompactTokenLimit: nativeAliasAutoCompact } : {}),
|
|
449
|
+
inputModalities: nativeInputModalities(combo.alias),
|
|
450
|
+
reasoningEfforts: nativeReasoningEfforts(combo.alias),
|
|
451
|
+
}
|
|
452
|
+
: undefined;
|
|
453
|
+
const members = combo.targets
|
|
454
|
+
.map(target => resolveComboCatalogMember(
|
|
455
|
+
target,
|
|
456
|
+
memberByKey,
|
|
457
|
+
enrichedByName,
|
|
458
|
+
providerContextCap(config, target.provider),
|
|
459
|
+
nativeAliasFallback,
|
|
460
|
+
metadataModelIdCaseFoldByProvider.get(target.provider),
|
|
461
|
+
))
|
|
462
|
+
.filter((member): member is CatalogModel => member !== undefined);
|
|
463
|
+
const derived = deriveComboCatalogModel(id, combo, members);
|
|
464
|
+
if (derived) {
|
|
465
|
+
const nativeDefault = combo.nativeAlias && combo.alias
|
|
466
|
+
? nativeDefaultReasoningEffort(combo.alias)
|
|
467
|
+
: undefined;
|
|
468
|
+
if (combo.defaultEffort === null
|
|
469
|
+
&& nativeDefault
|
|
470
|
+
&& derived.reasoningEfforts?.includes(nativeDefault)) {
|
|
471
|
+
derived.defaultReasoningEffort = nativeDefault;
|
|
472
|
+
}
|
|
473
|
+
all.push(derived);
|
|
474
|
+
}
|
|
475
|
+
else warnUncataloguedComboOnce(id, combo, members, localOmissions);
|
|
476
|
+
}
|
|
477
|
+
replaceLastComboCatalogOmissions(localOmissions);
|
|
478
|
+
all.sort((a, b) => (a.provider === b.provider ? a.id.localeCompare(b.id) : a.provider.localeCompare(b.provider)));
|
|
479
|
+
// Provider-derived rows keyed by their Codex-facing slug: a custom override replaces the row
|
|
480
|
+
// with the same slug below, so that row's provider capability metadata is the inheritance source.
|
|
481
|
+
const replacedByRoutedSlug = new Map(all.map(model => [routedSlug(model.provider, model.id), model]));
|
|
482
|
+
const customModels = (config.customModels ?? []).map(cm => {
|
|
483
|
+
const rawProvider = config.providers[cm.provider];
|
|
484
|
+
const effectiveProvider = enrichedByName.get(cm.provider) ?? rawProvider;
|
|
485
|
+
// Registry routing backfills an omitted authMode on the built-in OpenAI provider to
|
|
486
|
+
// forward. Keep the catalog projection on the same contract while still failing closed
|
|
487
|
+
// for every explicit non-forward mode and every non-canonical endpoint.
|
|
488
|
+
const providerForCanonicalCheck = rawProvider
|
|
489
|
+
? withCanonicalOpenAiForwardAuthDefault(cm.provider, rawProvider)
|
|
490
|
+
: undefined;
|
|
491
|
+
const codexForwardNativeCapabilityAlias = cm.provider === OPENAI_CODEX_PROVIDER_ID
|
|
492
|
+
&& providerForCanonicalCheck !== undefined
|
|
493
|
+
&& isCanonicalOpenAiForwardProvider(providerForCanonicalCheck)
|
|
494
|
+
&& hasNativeOpenAiCapabilityMetadata(cm.modelId);
|
|
495
|
+
const customNativeLimits = {
|
|
496
|
+
...nativeContextLimits(config),
|
|
497
|
+
...(typeof cm.contextWindow === "number" && cm.contextWindow > 0
|
|
498
|
+
? { modelWindows: { ...(nativeContextLimits(config).modelWindows ?? {}), [cm.modelId]: cm.contextWindow } }
|
|
499
|
+
: {}),
|
|
500
|
+
};
|
|
501
|
+
const nativeAliasContextWindow = codexForwardNativeCapabilityAlias
|
|
502
|
+
? nativeOpenAiContextWindow(cm.modelId, customNativeLimits)
|
|
503
|
+
: undefined;
|
|
504
|
+
const customContextWindow = cm.contextWindow
|
|
505
|
+
? nativeAliasContextWindow !== undefined
|
|
506
|
+
? nativeAliasContextWindow
|
|
507
|
+
: cm.contextWindow
|
|
508
|
+
: nativeAliasContextWindow;
|
|
509
|
+
const nativeAliasMaxInputTokens = codexForwardNativeCapabilityAlias
|
|
510
|
+
? nativeOpenAiMaxInputTokens(cm.modelId, customNativeLimits)
|
|
511
|
+
: undefined;
|
|
512
|
+
const nativeAliasMaxOutputTokens = codexForwardNativeCapabilityAlias
|
|
513
|
+
? nativeOpenAiMaxOutputTokens(cm.modelId)
|
|
514
|
+
: undefined;
|
|
515
|
+
const configuredMaxInput = rawProvider
|
|
516
|
+
? configuredMaxInputTokens(rawProvider, cm.modelId)
|
|
517
|
+
: undefined;
|
|
518
|
+
const hardMaxCandidates = [nativeAliasMaxInputTokens, configuredMaxInput]
|
|
519
|
+
.filter((value): value is number => typeof value === "number" && value > 0);
|
|
520
|
+
const customMaxInputTokens = hardMaxCandidates.length > 0
|
|
521
|
+
? Math.min(
|
|
522
|
+
...hardMaxCandidates,
|
|
523
|
+
...(customContextWindow !== undefined ? [customContextWindow] : []),
|
|
524
|
+
)
|
|
525
|
+
: undefined;
|
|
526
|
+
const customMaxOutputTokens = rawProvider
|
|
527
|
+
? routedMaxOutputTokens(cm.provider, rawProvider, {
|
|
528
|
+
id: cm.modelId,
|
|
529
|
+
provider: cm.provider,
|
|
530
|
+
...(nativeAliasMaxOutputTokens !== undefined ? { maxOutputTokens: nativeAliasMaxOutputTokens } : {}),
|
|
531
|
+
}, cm.modelId, metadataModelIdCaseFoldByProvider.get(cm.provider))
|
|
532
|
+
: nativeAliasMaxOutputTokens;
|
|
533
|
+
const configuredAutoCompact = configuredAutoCompactTokenLimit(rawProvider, cm.modelId);
|
|
534
|
+
const customAutoCompactTokenLimit = codexForwardNativeCapabilityAlias
|
|
535
|
+
? nativeOpenAiAutoCompactTokenLimit(cm.modelId, customNativeLimits)
|
|
536
|
+
: customContextWindow !== undefined && configuredAutoCompact !== undefined
|
|
537
|
+
? clampAutoCompactTokenLimit(customContextWindow, customMaxInputTokens, configuredAutoCompact)
|
|
538
|
+
: undefined;
|
|
539
|
+
const nativeAliasDefaultEffort = codexForwardNativeCapabilityAlias
|
|
540
|
+
? nativeDefaultReasoningEffort(cm.modelId)
|
|
541
|
+
: undefined;
|
|
542
|
+
const supportsReasoningSummaries = configuredReasoningSummarySupport(rawProvider, cm.modelId);
|
|
543
|
+
const fastPolicy = effectiveProvider
|
|
544
|
+
? fastPolicyForModel(effectiveProvider, cm.modelId, cm.provider)
|
|
545
|
+
: undefined;
|
|
546
|
+
const supportsServiceTier = fastPolicy
|
|
547
|
+
? serviceTierSupportFromPolicy(fastPolicy)
|
|
548
|
+
: undefined;
|
|
549
|
+
const base: CatalogModel = {
|
|
550
|
+
id: cm.modelId,
|
|
551
|
+
provider: cm.provider,
|
|
552
|
+
catalogKind: CODEX_CUSTOM_MODEL_CATALOG_KIND,
|
|
553
|
+
// Display-only label: never feeds routing (customModels are keyed by routedSlug below).
|
|
554
|
+
...(cm.displayName
|
|
555
|
+
? { displayName: cm.displayName }
|
|
556
|
+
: codexForwardNativeCapabilityAlias
|
|
557
|
+
? { displayName: nativeOpenAiCapabilityDisplayName(cm.modelId) ?? cm.modelId } : {}),
|
|
558
|
+
...(customContextWindow !== undefined ? { contextWindow: customContextWindow } : {}),
|
|
559
|
+
...(customMaxInputTokens !== undefined ? { maxInputTokens: customMaxInputTokens } : {}),
|
|
560
|
+
...(customMaxOutputTokens !== undefined ? { maxOutputTokens: customMaxOutputTokens } : {}),
|
|
561
|
+
...(customAutoCompactTokenLimit !== undefined ? { autoCompactTokenLimit: customAutoCompactTokenLimit } : {}),
|
|
562
|
+
...(cm.inputModalities
|
|
563
|
+
? { inputModalities: cm.inputModalities }
|
|
564
|
+
: codexForwardNativeCapabilityAlias ? { inputModalities: nativeInputModalities(cm.modelId) } : {}),
|
|
565
|
+
...(typeof supportsReasoningSummaries === "boolean" ? { supportsReasoningSummaries } : {}),
|
|
566
|
+
// Native-alias defaults apply only where the custom row declares nothing: the explicit
|
|
567
|
+
// spreads below must win (later in object order), so a stored `[]` stays empty and a
|
|
568
|
+
// declared ladder is narrowed to proven native capabilities after the merge below.
|
|
569
|
+
...(codexForwardNativeCapabilityAlias
|
|
570
|
+
? {
|
|
571
|
+
codexForwardNativeCapabilityAlias: true,
|
|
572
|
+
parallelToolCalls: nativeParallelToolCalls(cm.modelId),
|
|
573
|
+
...(Array.isArray(cm.reasoningEfforts)
|
|
574
|
+
? {}
|
|
575
|
+
: {
|
|
576
|
+
reasoningEfforts: nativeReasoningEfforts(cm.modelId),
|
|
577
|
+
...(nativeAliasDefaultEffort ? { defaultReasoningEffort: nativeAliasDefaultEffort } : {}),
|
|
578
|
+
}),
|
|
579
|
+
}
|
|
580
|
+
: {}),
|
|
581
|
+
// Explicit custom-row ladder wins over the inherited provider row below: the merge only
|
|
582
|
+
// gap-fills, so a stored `[]` (explicit "no reasoning") or a declared ladder is kept
|
|
583
|
+
// instead of being replaced by that row's metadata. Capability-backed native model ids
|
|
584
|
+
// are bounded against their own pinned ladder after the merge, including gateways.
|
|
585
|
+
...(Array.isArray(cm.reasoningEfforts) ? { reasoningEfforts: [...cm.reasoningEfforts] } : {}),
|
|
586
|
+
...(cm.defaultReasoningEffort ? { defaultReasoningEffort: cm.defaultReasoningEffort } : {}),
|
|
587
|
+
...(typeof supportsServiceTier === "boolean" ? { supportsServiceTier } : {}),
|
|
588
|
+
...(supportsServiceTier === true && fastPolicy?.fastTierDescription !== undefined
|
|
589
|
+
? { fastTierDescription: fastPolicy.fastTierDescription }
|
|
590
|
+
: {}),
|
|
591
|
+
...(cm.codexToolMode !== undefined
|
|
592
|
+
? { codexToolMode: cm.codexToolMode }
|
|
593
|
+
: effectiveProvider?.codexToolMode !== undefined
|
|
594
|
+
? { codexToolMode: effectiveProvider.codexToolMode }
|
|
595
|
+
: {}),
|
|
596
|
+
};
|
|
597
|
+
// #962: the dedupe below drops the provider-derived row this custom row replaces. Inherit that
|
|
598
|
+
// row's provider capability metadata (reasoning ladder, default effort, parallel tool calls,
|
|
599
|
+
// context, ...) so the generated catalog keeps advertising what the router actually provides.
|
|
600
|
+
// Explicit custom fields win by construction; this only fills gaps. Without it a
|
|
601
|
+
// noReasoningModels model loses its empty ladder and the catalog synthesizes the generic one,
|
|
602
|
+
// which Codex then rejects for spawn_agent with effort "none".
|
|
603
|
+
const replaced = replacedByRoutedSlug.get(routedSlug(cm.provider, cm.modelId));
|
|
604
|
+
// The final ladder is what the catalog will advertise; the inherited default only rides
|
|
605
|
+
// along when it is actually a member — otherwise a provider default like "xhigh" would
|
|
606
|
+
// re-apply onto a narrower custom ladder and override the fallback in applyReasoningLevels.
|
|
607
|
+
const effectiveLadder = base.reasoningEfforts ?? replaced?.reasoningEfforts;
|
|
608
|
+
const mergedMaxInputCandidates = [base.maxInputTokens, replaced?.maxInputTokens]
|
|
609
|
+
.filter((value): value is number => typeof value === "number" && value > 0);
|
|
610
|
+
const mergedMaxInput = mergedMaxInputCandidates.length > 0
|
|
611
|
+
? Math.min(...mergedMaxInputCandidates)
|
|
612
|
+
: undefined;
|
|
613
|
+
const mergedMaxOutputCandidates = [base.maxOutputTokens, replaced?.maxOutputTokens]
|
|
614
|
+
.filter((value): value is number => typeof value === "number" && value > 0);
|
|
615
|
+
const mergedMaxOutput = mergedMaxOutputCandidates.length > 0
|
|
616
|
+
? Math.min(...mergedMaxOutputCandidates)
|
|
617
|
+
: undefined;
|
|
618
|
+
const merged: CatalogModel = replaced ? {
|
|
619
|
+
...base,
|
|
620
|
+
...(base.contextWindow === undefined && replaced.contextWindow !== undefined ? { contextWindow: replaced.contextWindow } : {}),
|
|
621
|
+
...(mergedMaxInput !== undefined ? { maxInputTokens: mergedMaxInput } : {}),
|
|
622
|
+
...(mergedMaxOutput !== undefined ? { maxOutputTokens: mergedMaxOutput } : {}),
|
|
623
|
+
...(base.autoCompactTokenLimit === undefined && replaced.autoCompactTokenLimit !== undefined
|
|
624
|
+
? { autoCompactTokenLimit: replaced.autoCompactTokenLimit }
|
|
625
|
+
: {}),
|
|
626
|
+
...(base.inputModalities === undefined && replaced.inputModalities !== undefined ? { inputModalities: replaced.inputModalities } : {}),
|
|
627
|
+
...(base.reasoningEfforts === undefined && replaced.reasoningEfforts !== undefined ? { reasoningEfforts: replaced.reasoningEfforts } : {}),
|
|
628
|
+
...(base.defaultReasoningEffort === undefined && replaced.defaultReasoningEffort !== undefined
|
|
629
|
+
&& Array.isArray(effectiveLadder) && effectiveLadder.includes(replaced.defaultReasoningEffort)
|
|
630
|
+
? { defaultReasoningEffort: replaced.defaultReasoningEffort } : {}),
|
|
631
|
+
...(base.parallelToolCalls === undefined && replaced.parallelToolCalls !== undefined ? { parallelToolCalls: replaced.parallelToolCalls } : {}),
|
|
632
|
+
...(base.supportsVerbosity === undefined && replaced.supportsVerbosity !== undefined ? { supportsVerbosity: replaced.supportsVerbosity } : {}),
|
|
633
|
+
...(base.supportsReasoningSummaries === undefined && replaced.supportsReasoningSummaries !== undefined ? { supportsReasoningSummaries: replaced.supportsReasoningSummaries } : {}),
|
|
634
|
+
...(base.codexToolMode === undefined && replaced.codexToolMode !== undefined ? { codexToolMode: replaced.codexToolMode } : {}),
|
|
635
|
+
...(base.capabilities === undefined && replaced.capabilities !== undefined ? { capabilities: replaced.capabilities } : {}),
|
|
636
|
+
} : base;
|
|
637
|
+
// Catalog-advertised efforts are bounded whenever the model id is a pinned native
|
|
638
|
+
// slug. Desktop validates that id, so a gateway such as YYLJ/gpt-6-astra still cannot
|
|
639
|
+
// advertise none/minimal. Full native identity stays behind the alias predicate.
|
|
640
|
+
const nativeEffortSource = hasNativeOpenAiCapabilityMetadata(cm.modelId);
|
|
641
|
+
const reasoningBounded = nativeEffortSource
|
|
642
|
+
? boundCustomNativeReasoning(
|
|
643
|
+
merged,
|
|
644
|
+
nativeReasoningEfforts(cm.modelId),
|
|
645
|
+
nativeAliasDefaultEffort ?? nativeDefaultReasoningEffort(cm.modelId),
|
|
646
|
+
)
|
|
647
|
+
: merged;
|
|
648
|
+
// Vision-sidecar coverage only: when the enriched provider's shared predicate matches
|
|
649
|
+
// noVisionModels or text-without-image modelInputModalities, advertise image input so the
|
|
650
|
+
// Codex app lets images reach the sidecar (#349/#344). Deliberately NOT the full
|
|
651
|
+
// applyProviderConfigHints pass — custom rows are a
|
|
652
|
+
// user override, so their explicit contextWindow / inputModalities / reasoning fields must be
|
|
653
|
+
// preserved verbatim (the hint pass would cap context and overwrite modalities from registry).
|
|
654
|
+
const mergedContext = typeof reasoningBounded.contextWindow === "number" && reasoningBounded.contextWindow > 0
|
|
655
|
+
? reasoningBounded.contextWindow
|
|
656
|
+
: undefined;
|
|
657
|
+
const boundedMergedMaxInput = typeof reasoningBounded.maxInputTokens === "number" && reasoningBounded.maxInputTokens > 0
|
|
658
|
+
? (mergedContext !== undefined ? Math.min(reasoningBounded.maxInputTokens, mergedContext) : reasoningBounded.maxInputTokens)
|
|
659
|
+
: undefined;
|
|
660
|
+
const mergedWithHardBounds = boundedMergedMaxInput !== undefined
|
|
661
|
+
&& boundedMergedMaxInput !== reasoningBounded.maxInputTokens
|
|
662
|
+
? { ...reasoningBounded, maxInputTokens: boundedMergedMaxInput }
|
|
663
|
+
: reasoningBounded;
|
|
664
|
+
const mergedSoftCandidates = [mergedWithHardBounds.autoCompactTokenLimit, configuredAutoCompact]
|
|
665
|
+
.filter((value): value is number => typeof value === "number" && value > 0);
|
|
666
|
+
const mergedWithAutoCompact: CatalogModel = mergedContext !== undefined && mergedSoftCandidates.length > 0
|
|
667
|
+
? {
|
|
668
|
+
...mergedWithHardBounds,
|
|
669
|
+
autoCompactTokenLimit: clampAutoCompactTokenLimit(
|
|
670
|
+
mergedContext,
|
|
671
|
+
boundedMergedMaxInput,
|
|
672
|
+
Math.min(...mergedSoftCandidates),
|
|
673
|
+
),
|
|
674
|
+
}
|
|
675
|
+
: mergedWithHardBounds;
|
|
676
|
+
const enrichedProvider = enrichedByName.get(cm.provider) ?? rawProvider;
|
|
677
|
+
// Reuse the request-time consumer predicate so custom rows cannot drift from catalog hints.
|
|
678
|
+
if (enrichedProvider && isModelVisionSidecarConsumer(enrichedProvider, mergedWithAutoCompact.id)) {
|
|
679
|
+
const current = mergedWithAutoCompact.inputModalities ?? ["text"];
|
|
680
|
+
if (!current.includes("image")) {
|
|
681
|
+
return { ...mergedWithAutoCompact, inputModalities: [...current, "image"] };
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return mergedWithAutoCompact;
|
|
685
|
+
});
|
|
686
|
+
// Custom rows override discovered rows that encode to the same Codex-facing slug.
|
|
687
|
+
const customKeys = new Set(customModels.map(c => routedSlug(c.provider, c.id)));
|
|
688
|
+
const deduped = all.filter(m => !customKeys.has(routedSlug(m.provider, m.id)));
|
|
689
|
+
const models = [...deduped, ...customModels];
|
|
690
|
+
// ponytail: catalog-scale scan; index ids by provider if catalog growth makes this measurable.
|
|
691
|
+
const aliasDisplayNames = new Map(activeProviders.flatMap(({ name, provider }) => {
|
|
692
|
+
const providerModels = models.filter(model => model.provider === name);
|
|
693
|
+
const aliases = [...effectiveModelAliases(config, provider, providerModels.map(model => model.id))];
|
|
694
|
+
return aliases.flatMap(([id, { alias }]) => {
|
|
695
|
+
const exact = providerModels.filter(model => model.id === id);
|
|
696
|
+
const matches = exact.length > 0
|
|
697
|
+
? exact
|
|
698
|
+
: providerModels.filter(model => model.id.toLowerCase() === id.toLowerCase());
|
|
699
|
+
return matches.length === 1
|
|
700
|
+
? [[`${name}/${matches[0]!.id}`, `${provider.alias || name}/${alias}`] as const]
|
|
701
|
+
: [];
|
|
702
|
+
});
|
|
703
|
+
}));
|
|
704
|
+
const providerModelOutcomes = providerResults.map(result => (
|
|
705
|
+
result.outcome.provider === OPENAI_API_PROVIDER_ID
|
|
706
|
+
&& capture.openAiApiPolicy.state === "captured"
|
|
707
|
+
&& capture.openAiApiPolicy.models !== undefined
|
|
708
|
+
? { provider: result.outcome.provider, state: "authoritative" as const }
|
|
709
|
+
: result.outcome
|
|
710
|
+
));
|
|
711
|
+
return {
|
|
712
|
+
models: models.map(model => {
|
|
713
|
+
const displayName = aliasDisplayNames.get(`${model.provider}/${model.id}`);
|
|
714
|
+
// #1711: one stamping point for every row this gather produces — routed, combo, and custom
|
|
715
|
+
// alike — because it is the only place that has both the finished list and the config the
|
|
716
|
+
// quota rules need. A combo votes over its own targets; anything else votes over the single
|
|
717
|
+
// provider that would serve it.
|
|
718
|
+
const targets = model.provider === COMBO_NAMESPACE
|
|
719
|
+
? config.combos?.[model.id]?.targets ?? []
|
|
720
|
+
: [{ provider: model.provider }];
|
|
721
|
+
const inactive = quotaInactiveReason(config, targets);
|
|
722
|
+
const named = displayName && !model.displayName ? { ...model, displayName } : model;
|
|
723
|
+
return inactive ? { ...named, quotaInactiveReason: inactive } : named;
|
|
724
|
+
}),
|
|
725
|
+
comboOmissions: localOmissions,
|
|
726
|
+
providerAuthOutcomes: localProviderAuthOutcomes,
|
|
727
|
+
providerModelOutcomes,
|
|
728
|
+
discoveryPolicySnapshots: capture.discoveryPolicySnapshots,
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
export function augmentRoutedModelsWithRegistryOpenAiApiRows(
|
|
733
|
+
models: CatalogModel[],
|
|
734
|
+
config: OcxConfig,
|
|
735
|
+
): CatalogModel[] {
|
|
736
|
+
const configured = config.providers[OPENAI_API_PROVIDER_ID];
|
|
737
|
+
if (!configured || configured.disabled === true || !providerMatchesRegistryTransport(OPENAI_API_PROVIDER_ID, configured)) return models;
|
|
738
|
+
return augmentRoutedModelsWithCapturedOpenAiApiRows(
|
|
739
|
+
models,
|
|
740
|
+
config,
|
|
741
|
+
captureTrustedOpenAiApiPolicy(OPENAI_API_PROVIDER_ID, true),
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function augmentRoutedModelsWithCapturedOpenAiApiRows(
|
|
746
|
+
models: CatalogModel[],
|
|
747
|
+
config: OcxConfig,
|
|
748
|
+
policy: CatalogTrustedOpenAiApiPolicySnapshot,
|
|
749
|
+
): CatalogModel[] {
|
|
750
|
+
if (policy.state !== "captured" || !policy.models) return models;
|
|
751
|
+
const configured = config.providers[OPENAI_API_PROVIDER_ID];
|
|
752
|
+
if (!configured || configured.disabled === true) return models;
|
|
753
|
+
|
|
754
|
+
const existingById = new Map(
|
|
755
|
+
models.filter(model => model.provider === OPENAI_API_PROVIDER_ID).map(model => [model.id, model]),
|
|
756
|
+
);
|
|
757
|
+
const trustedRows = policy.models.map((id): CatalogModel => {
|
|
758
|
+
const officialContext = policy.modelContextWindows?.[id];
|
|
759
|
+
const officialMaxInput = policy.modelMaxInputTokens?.[id];
|
|
760
|
+
const userContext = configured.modelContextWindows?.[id] ?? configured.contextWindow;
|
|
761
|
+
const userMaxInput = configured.modelMaxInputTokens?.[id];
|
|
762
|
+
const providerCap = providerContextCap(config, OPENAI_API_PROVIDER_ID);
|
|
763
|
+
const contextWindow = typeof officialContext === "number"
|
|
764
|
+
? Math.min(officialContext, userContext ?? officialContext, providerCap ?? officialContext)
|
|
765
|
+
: undefined;
|
|
766
|
+
const maxInputTokens = typeof officialMaxInput === "number"
|
|
767
|
+
? Math.min(
|
|
768
|
+
officialMaxInput,
|
|
769
|
+
userMaxInput ?? officialMaxInput,
|
|
770
|
+
contextWindow ?? officialMaxInput,
|
|
771
|
+
)
|
|
772
|
+
: undefined;
|
|
773
|
+
const configuredAutoCompact = configuredAutoCompactTokenLimit(configured, id);
|
|
774
|
+
const autoCompactTokenLimit = contextWindow !== undefined && configuredAutoCompact !== undefined
|
|
775
|
+
? clampAutoCompactTokenLimit(contextWindow, maxInputTokens, configuredAutoCompact)
|
|
776
|
+
: undefined;
|
|
777
|
+
const maxOutputTokens = routedMaxOutputTokens(
|
|
778
|
+
OPENAI_API_PROVIDER_ID,
|
|
779
|
+
configured,
|
|
780
|
+
policy.modelMaxOutputTokens?.[id] !== undefined
|
|
781
|
+
? { provider: OPENAI_API_PROVIDER_ID, id, maxOutputTokens: policy.modelMaxOutputTokens[id] }
|
|
782
|
+
: existingById.get(id) ?? { provider: OPENAI_API_PROVIDER_ID, id },
|
|
783
|
+
policy.virtualModels?.[id]?.wireModelId ?? id,
|
|
784
|
+
);
|
|
785
|
+
return {
|
|
786
|
+
provider: OPENAI_API_PROVIDER_ID,
|
|
787
|
+
id,
|
|
788
|
+
owned_by: OPENAI_API_PROVIDER_ID,
|
|
789
|
+
...(contextWindow ? { contextWindow } : {}),
|
|
790
|
+
...(maxInputTokens ? { maxInputTokens } : {}),
|
|
791
|
+
...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
|
|
792
|
+
...(autoCompactTokenLimit !== undefined ? { autoCompactTokenLimit } : {}),
|
|
793
|
+
...(policy.modelInputModalities?.[id] ? { inputModalities: [...policy.modelInputModalities[id]!] } : {}),
|
|
794
|
+
...(policy.modelReasoningEfforts?.[id] ? { reasoningEfforts: [...policy.modelReasoningEfforts[id]!] } : {}),
|
|
795
|
+
};
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
for (const trusted of trustedRows) {
|
|
799
|
+
const live = existingById.get(trusted.id);
|
|
800
|
+
if (!live) continue;
|
|
801
|
+
const liveSignature = normalizedOpenAiApiSignature(live);
|
|
802
|
+
const trustedSignature = normalizedOpenAiApiSignature(trusted);
|
|
803
|
+
if (liveSignature === trustedSignature) continue;
|
|
804
|
+
const warningKey = `${trusted.provider}/${trusted.id}\n${liveSignature}\n${trustedSignature}`;
|
|
805
|
+
if (openAiApiCollisionWarnings.has(warningKey)) continue;
|
|
806
|
+
openAiApiCollisionWarnings.add(warningKey);
|
|
807
|
+
console.warn(`[opencodex] replacing conflicting live OpenAI API metadata for ${trusted.provider}/${trusted.id} with trusted registry metadata`);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
return [
|
|
811
|
+
...models.filter(model => model.provider !== OPENAI_API_PROVIDER_ID),
|
|
812
|
+
...trustedRows,
|
|
813
|
+
];
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
export function augmentRoutedModelsWithMetadata(
|
|
817
|
+
models: CatalogModel[],
|
|
818
|
+
providerNames: string[],
|
|
819
|
+
providers?: Record<string, OcxProviderConfig>,
|
|
820
|
+
caps?: Pick<OcxConfig, "providerContextCaps">,
|
|
821
|
+
metadataModelIdCaseFoldByProvider?: ReadonlyMap<string, boolean>,
|
|
822
|
+
): CatalogModel[] {
|
|
823
|
+
const out = [...models];
|
|
824
|
+
const seen = new Set(out.map(m => `${m.provider}/${m.id}`));
|
|
825
|
+
for (const provider of providerNames) {
|
|
826
|
+
if (!JAWCODE_CATALOG_AUGMENT_PROVIDERS.has(provider)) continue;
|
|
827
|
+
if (providers?.[provider]?.liveModels === false) continue;
|
|
828
|
+
const jawcodeProvider = resolveMetadataProvider(provider);
|
|
829
|
+
if (!jawcodeProvider) continue;
|
|
830
|
+
for (const meta of listModelMetadata(jawcodeProvider)) {
|
|
831
|
+
const key = `${provider}/${meta.id}`;
|
|
832
|
+
if (seen.has(key)) continue;
|
|
833
|
+
seen.add(key);
|
|
834
|
+
const contextCap = caps ? providerContextCap(caps, provider) : undefined;
|
|
835
|
+
const model: CatalogModel = {
|
|
836
|
+
provider,
|
|
837
|
+
id: meta.id,
|
|
838
|
+
owned_by: provider,
|
|
839
|
+
...(typeof meta.contextWindow === "number" && meta.contextWindow > 0 ? { contextWindow: meta.contextWindow } : {}),
|
|
840
|
+
...(typeof meta.maxTokens === "number" && meta.maxTokens > 0 ? { maxOutputTokens: meta.maxTokens } : {}),
|
|
841
|
+
...(Array.isArray(meta.input) && meta.input.length > 0 ? { inputModalities: [...meta.input] } : {}),
|
|
842
|
+
};
|
|
843
|
+
out.push({
|
|
844
|
+
...model,
|
|
845
|
+
...(providers?.[provider]
|
|
846
|
+
? applyProviderConfigHints(
|
|
847
|
+
provider,
|
|
848
|
+
providers[provider],
|
|
849
|
+
model,
|
|
850
|
+
contextCap,
|
|
851
|
+
metadataModelIdCaseFoldByProvider?.get(provider),
|
|
852
|
+
)
|
|
853
|
+
: {}),
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
return out;
|
|
858
|
+
}
|