@bitkyc08/opencodex 2.55.0 → 2.57.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/bin/ocx.mjs +10 -0
- package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
- package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
- package/gui/dist/index.html +2 -2
- package/package.json +4 -3
- package/src/adapters/base.ts +21 -0
- package/src/adapters/codebuddy/adapter.ts +2 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
- package/src/adapters/command-code.ts +1 -1
- package/src/adapters/cursor/envelope-echo.ts +8 -2
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/google.ts +7 -7
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro/payload.ts +17 -3
- package/src/adapters/kiro/reasoning.ts +70 -7
- package/src/adapters/kiro/stream.ts +8 -2
- package/src/adapters/kiro/wire.ts +2 -1
- package/src/adapters/kiro-events.ts +21 -13
- 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-name-registry.ts +166 -0
- package/src/adapters/openai-chat/tool-schema.ts +495 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +40 -1452
- 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 +642 -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 +58 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +630 -0
- package/src/bridge/sse.ts +1462 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/claude/desktop-profile.ts +66 -9
- package/src/claude/outbound.ts +18 -0
- package/src/cli/account-main.ts +1 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/combo.ts +10 -1
- package/src/cli/index.ts +48 -5
- package/src/cli/registry.ts +2 -1
- package/src/cli/system-command.ts +4 -4
- package/src/clients/config-export.ts +7 -3
- package/src/codex/account-label.ts +14 -3
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +184 -35
- package/src/codex/account-usability.ts +21 -0
- 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 +566 -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 +431 -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 +252 -35
- package/src/codex/catalog/aggregation.ts +80 -1
- 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 +305 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/remote.ts +30 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +714 -0
- package/src/codex/catalog/routed-gather.ts +895 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/cli-install-provenance.ts +7 -1
- package/src/codex/convergence.ts +7 -2
- package/src/codex/desktop-app/types.ts +11 -2
- package/src/codex/desktop-app/windows.ts +5 -5
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +567 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +89 -1444
- package/src/codex/lineage.ts +458 -0
- package/src/codex/model-entitlements.ts +152 -15
- package/src/codex/pool-refresh-backoff.ts +161 -0
- package/src/codex/quota-rejection.ts +104 -15
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cache-affinity.ts +70 -0
- package/src/codex/routing/cooldown-math.ts +285 -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 +780 -0
- package/src/codex/routing/thread-affinity.ts +586 -0
- package/src/codex/routing/transient-hold-dispatch.ts +141 -0
- package/src/codex/routing.ts +370 -2271
- 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/codex/warmup.ts +1 -1
- package/src/combos/failover.ts +85 -0
- package/src/combos/request.ts +17 -10
- package/src/combos/types.ts +23 -2
- 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/pending-teardown.ts +31 -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 +649 -121
- package/src/images/loop.ts +1 -1
- package/src/lib/errors.ts +17 -0
- package/src/lib/request-execution-budget.ts +198 -23
- package/src/lib/spend-reservation-ledger.ts +958 -0
- package/src/lib/state-store-registrations.ts +6 -2
- package/src/lib/test-home-guard.ts +85 -1
- package/src/lib/upstream-retry.ts +132 -21
- package/src/lib/windows-elevation.ts +76 -14
- package/src/lib/workflow-budget.ts +553 -30
- package/src/oauth/index.ts +2 -2
- package/src/oauth/key-providers.ts +2 -2
- package/src/providers/kiro-models.ts +4 -3
- package/src/providers/label.ts +19 -1
- package/src/providers/model-discovery.ts +16 -0
- 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 +1228 -0
- package/src/providers/registry/entries-extended.ts +1213 -0
- package/src/providers/registry/model-seeds.ts +912 -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/reasoning-envelope.ts +6 -3
- 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 +456 -0
- package/src/routing/probe-lease.ts +613 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/chat-native.ts +37 -9
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +601 -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 +339 -0
- package/src/server/index.ts +45 -2552
- package/src/server/inspection-tee.ts +107 -0
- package/src/server/live.ts +46 -1
- package/src/server/management/combo-routes.ts +10 -1
- 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/relay-eager.ts +2 -0
- package/src/server/relay.ts +14 -19
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +372 -4
- package/src/server/response-log-body.ts +153 -0
- package/src/server/responses/account-change-state.ts +307 -0
- package/src/server/responses/adapter-continuation.ts +540 -0
- package/src/server/responses/adapter-delivery.ts +208 -0
- package/src/server/responses/adapter-dispatch.ts +1042 -0
- package/src/server/responses/codex-ws-wire.ts +5 -0
- package/src/server/responses/collaboration.ts +74 -4
- package/src/server/responses/combo-session-recall.ts +68 -8
- package/src/server/responses/compact.ts +113 -17
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +529 -0
- package/src/server/responses/core-codex-account.ts +907 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +787 -0
- package/src/server/responses/core-errors.ts +170 -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 +298 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/encrypted-payload.ts +0 -1
- package/src/server/responses/input-admission.ts +126 -6
- package/src/server/responses/passthrough-delivery.ts +869 -0
- package/src/server/responses/passthrough-dispatch.ts +1494 -0
- package/src/server/responses/passthrough-error.ts +38 -2
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +1080 -0
- package/src/server/responses/request-send-budget.ts +259 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-spend.ts +147 -0
- package/src/server/responses/request-transport.ts +803 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +476 -0
- package/src/server/responses/sidecar-execution.ts +463 -0
- package/src/server/responses/terminal-guard.ts +65 -4
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/responses-undeclared-tool-guard.ts +9 -5
- package/src/server/workflow-refusal.ts +84 -0
- package/src/service/windows-ops.ts +210 -16
- package/src/service/windows-scheduler.ts +28 -21
- package/src/service.ts +1 -1
- package/src/types/config.ts +34 -1
- package/src/types/request.ts +8 -5
- package/src/types/tools.ts +24 -0
- package/src/types.ts +2 -0
- package/src/update/index.ts +10 -0
- package/src/update/stop-contract.d.mts +1 -0
- package/src/update/stop-contract.mjs +19 -0
- package/src/update/stop-decision.d.mts +1 -1
- package/src/update/stop-decision.mjs +12 -3
- package/src/usage/log.ts +147 -1
- package/src/usage/summary.ts +171 -21
- package/src/vision/anthropic-describe.ts +1 -1
- package/src/vision/describe.ts +5 -5
- package/src/web-search/anthropic-executor.ts +1 -1
- package/src/web-search/exa-executor.ts +1 -1
- package/src/web-search/executor.ts +1 -1
- package/src/web-search/gemini-executor.ts +1 -1
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/ollama-executor.ts +1 -1
- package/src/web-search/parse.ts +67 -14
- package/src/web-search/passthrough-bridge.ts +64 -31
- package/src/web-search/xai-executor.ts +1 -1
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { OcxConfig } from "../../types";
|
|
2
|
+
import { codexModelEntitlementStateForAccount, type CodexModelEntitlementSnapshot } from "../model-entitlements";
|
|
3
|
+
import { codexAccountLogLabel, fallbackCodexAccountLogLabel } from "../account-label";
|
|
4
|
+
import { MAIN_CODEX_ACCOUNT_ID } from "../main-account";
|
|
5
|
+
|
|
6
|
+
export function gatedNativeReauthSuppressionReason(args: {
|
|
7
|
+
snapshot: CodexModelEntitlementSnapshot;
|
|
8
|
+
slug: string;
|
|
9
|
+
eligibleAccountIds?: ReadonlySet<string>;
|
|
10
|
+
needsReauth: (accountId: string) => boolean;
|
|
11
|
+
label: (accountId: string) => string;
|
|
12
|
+
}): string | undefined {
|
|
13
|
+
const observed = [...args.snapshot.modelsByAccount.keys()]
|
|
14
|
+
.filter(accountId => !args.eligibleAccountIds || args.eligibleAccountIds.has(accountId))
|
|
15
|
+
// Only accounts that could actually have served THIS model. An account upstream positively
|
|
16
|
+
// denied is not why the model is missing, and blaming it would send the operator to repair a
|
|
17
|
+
// credential that was never going to help. `unknown` has to stay in: an account whose roster
|
|
18
|
+
// could not be confirmed reports `unknown` rather than `granted`, and a credential stuck on
|
|
19
|
+
// a failed refresh is exactly that account.
|
|
20
|
+
.filter(accountId => (
|
|
21
|
+
codexModelEntitlementStateForAccount(args.snapshot, accountId, args.slug) !== "denied"
|
|
22
|
+
));
|
|
23
|
+
const stuck = observed.filter(accountId => args.needsReauth(accountId));
|
|
24
|
+
if (stuck.length === 0) return undefined;
|
|
25
|
+
const names = stuck.map(accountId => args.label(accountId)).sort().join(", ");
|
|
26
|
+
return stuck.length === observed.length
|
|
27
|
+
? `every Codex account that could serve it needs reauthentication (${names})`
|
|
28
|
+
: `${stuck.length} of ${observed.length} Codex accounts that could serve it need reauthentication (${names})`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Durable, operator-facing label for a pool account id; never the raw id or the email. */
|
|
32
|
+
export function gatedNativeAccountLabel(config: OcxConfig, accountId: string): string {
|
|
33
|
+
// Direct mode narrows eligibility to the native main credential, so this is the account most
|
|
34
|
+
// likely to be named here. `codexAuthContextLogLabel` calls it "main" everywhere else; hashing
|
|
35
|
+
// it into a `p`-prefixed digest would name the one account the operator cannot look up.
|
|
36
|
+
if (accountId === MAIN_CODEX_ACCOUNT_ID) return "main";
|
|
37
|
+
const account = (config.codexAccounts ?? []).find(candidate => candidate.id === accountId);
|
|
38
|
+
return account ? codexAccountLogLabel(account) : fallbackCodexAccountLogLabel(accountId);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const warnedGatedNativeSuppression = new Set<string>();
|
|
42
|
+
|
|
43
|
+
/** Test seam: the warn-once memory is process-global, so a case needs to be able to clear it. */
|
|
44
|
+
export function resetGatedNativeSuppressionWarningsForTests(): void {
|
|
45
|
+
warnedGatedNativeSuppression.clear();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function warnGatedNativeSuppressedOnce(slug: string, reason: string): void {
|
|
49
|
+
const signature = `${slug}\u0000${reason}`;
|
|
50
|
+
if (warnedGatedNativeSuppression.has(signature)) return;
|
|
51
|
+
warnedGatedNativeSuppression.add(signature);
|
|
52
|
+
console.warn(
|
|
53
|
+
`[opencodex] catalog sync: ${slug} is not being offered because ${reason}. `
|
|
54
|
+
+ "Sign in again to restore it.",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Mescla o catálogo retido com os modelos visíveis e as configurações atuais,
|
|
60
|
+
* incluindo os nomes nativos. Tenta preservar o backup original e usa a permissão
|
|
61
|
+
* de escrita para publicar o resultado apenas se os bytes mudarem, retornando
|
|
62
|
+
* a contagem de entradas roteadas e por conta, o caminho e o estado da gravação.
|
|
63
|
+
*/
|
|
@@ -0,0 +1,533 @@
|
|
|
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 { applyRegistryCapabilitySeedFill, modelCapabilities, modelInputModalities } from "./model-hints";
|
|
104
|
+
import { configuredComboTargetModelsByProvider } from "./combo-member";
|
|
105
|
+
|
|
106
|
+
/** Concurrent gatherRoutedModels callers with the same catalog identity share one live discovery.
|
|
107
|
+
* Keyed by gatherFlightKey so a different config cannot join or evict the wrong flight. */
|
|
108
|
+
export interface CatalogGatherProviderAuthOutcome {
|
|
109
|
+
readonly provider: string;
|
|
110
|
+
readonly state: OAuthActiveTokenObservation["kind"];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface CatalogGatherProviderModelOutcome {
|
|
114
|
+
readonly provider: string;
|
|
115
|
+
readonly state: "authoritative" | "degraded";
|
|
116
|
+
}
|
|
117
|
+
export interface ModelsAuthResolution {
|
|
118
|
+
readonly apiKey: string | undefined;
|
|
119
|
+
readonly observed: boolean;
|
|
120
|
+
readonly oauthApiBaseUrl?: string;
|
|
121
|
+
readonly oauthProjectId?: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export type ModelsAuthResolver =
|
|
125
|
+
| { readonly kind: "refreshing" }
|
|
126
|
+
| {
|
|
127
|
+
readonly kind: "observed";
|
|
128
|
+
readonly resolve: (name: string, provider: OcxProviderConfig) => ModelsAuthResolution;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export type ModelsAuthResolverFactory = (
|
|
132
|
+
outcomes: CatalogGatherProviderAuthOutcome[],
|
|
133
|
+
) => ModelsAuthResolver;
|
|
134
|
+
|
|
135
|
+
export interface CapturedModelsRequest {
|
|
136
|
+
readonly method: "GET" | "POST";
|
|
137
|
+
readonly url: string;
|
|
138
|
+
readonly headersWithoutCredential: Readonly<Record<string, string>>;
|
|
139
|
+
readonly headersWithCredential: Readonly<Record<string, string>>;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface CapturedProviderGather {
|
|
143
|
+
readonly name: string;
|
|
144
|
+
readonly provider: OcxProviderConfig;
|
|
145
|
+
readonly discovery: ResolvedProviderModelDiscovery;
|
|
146
|
+
readonly policy: CatalogProviderDiscoveryPolicySnapshot;
|
|
147
|
+
readonly request: CapturedModelsRequest;
|
|
148
|
+
readonly fastPolicyAuthority: FastPolicyAuthority;
|
|
149
|
+
readonly metadataModelIdCaseFold: boolean;
|
|
150
|
+
readonly effectiveAlias?: string | null;
|
|
151
|
+
readonly observedAuth?: ModelsAuthResolution;
|
|
152
|
+
/**
|
|
153
|
+
* Configured model ids this provider must keep even when live discovery omits
|
|
154
|
+
* them — combo targets that are also listed in providers.*.models (OCX-111).
|
|
155
|
+
* Combo-only ids (not in models[]) stay out of the public catalog and are
|
|
156
|
+
* synthesized for combo derivation instead (#1305).
|
|
157
|
+
*/
|
|
158
|
+
readonly retainConfiguredModelIds?: ReadonlySet<string>;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface GatherFlightCapture {
|
|
162
|
+
readonly discoveryPolicyIdentity: string;
|
|
163
|
+
readonly authIdentity: string;
|
|
164
|
+
readonly providerGraphIdentity: string;
|
|
165
|
+
readonly discoveryPolicySnapshots: readonly CatalogProviderDiscoveryPolicySnapshot[];
|
|
166
|
+
readonly providers: readonly CapturedProviderGather[];
|
|
167
|
+
readonly authResolver: ModelsAuthResolver;
|
|
168
|
+
readonly providerAuthOutcomes: readonly CatalogGatherProviderAuthOutcome[];
|
|
169
|
+
readonly openAiApiPolicy: CatalogTrustedOpenAiApiPolicySnapshot;
|
|
170
|
+
}
|
|
171
|
+
export function withCanonicalOpenAiForwardAuthDefault(
|
|
172
|
+
name: string,
|
|
173
|
+
provider: OcxProviderConfig,
|
|
174
|
+
): OcxProviderConfig {
|
|
175
|
+
if (name !== OPENAI_CODEX_PROVIDER_ID || provider.authMode !== undefined) return provider;
|
|
176
|
+
const candidate = { ...provider, authMode: "forward" as const };
|
|
177
|
+
return isCanonicalOpenAiForwardProvider(candidate) ? candidate : provider;
|
|
178
|
+
}
|
|
179
|
+
const CATALOG_GATHER_AUTHORITY_KEY = randomBytes(32);
|
|
180
|
+
const REQUEST_CREDENTIAL_SENTINEL = `ocx-catalog-credential-${randomBytes(16).toString("hex")}`;
|
|
181
|
+
function stableJson(value: unknown): string {
|
|
182
|
+
return JSON.stringify(value, (_key, nested) => {
|
|
183
|
+
if (nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|
184
|
+
return Object.fromEntries(Object.entries(nested as Record<string, unknown>).sort(([a], [b]) => a.localeCompare(b)));
|
|
185
|
+
}
|
|
186
|
+
return nested;
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function framed(value: string): string {
|
|
191
|
+
return `${Buffer.byteLength(value, "utf8")}:${value}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function canonicalAuthorityEncoding(value: unknown): string {
|
|
195
|
+
if (value === null) return "null";
|
|
196
|
+
if (value === undefined) return "undefined";
|
|
197
|
+
if (typeof value === "string") return `string${framed(value)}`;
|
|
198
|
+
if (typeof value === "boolean") return value ? "boolean1" : "boolean0";
|
|
199
|
+
if (typeof value === "number") {
|
|
200
|
+
if (!Number.isFinite(value)) throw new TypeError("Catalog authority cannot encode a non-finite number.");
|
|
201
|
+
const encoded = Object.is(value, -0) ? "-0" : String(value);
|
|
202
|
+
return `number${framed(encoded)}`;
|
|
203
|
+
}
|
|
204
|
+
if (Array.isArray(value)) {
|
|
205
|
+
return `array${value.length}:${value.map(item => framed(canonicalAuthorityEncoding(item))).join("")}`;
|
|
206
|
+
}
|
|
207
|
+
if (typeof value === "object") {
|
|
208
|
+
const record = value as Record<string, unknown>;
|
|
209
|
+
const keys = Object.keys(record).sort((left, right) => left.localeCompare(right));
|
|
210
|
+
return `object${keys.length}:${keys.map(key => (
|
|
211
|
+
`${framed(key)}${framed(canonicalAuthorityEncoding(record[key]))}`
|
|
212
|
+
)).join("")}`;
|
|
213
|
+
}
|
|
214
|
+
throw new TypeError(`Catalog authority cannot encode ${typeof value}.`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function keyedGatherIdentity(domain: string, value: unknown): string {
|
|
218
|
+
return createHmac("sha256", CATALOG_GATHER_AUTHORITY_KEY)
|
|
219
|
+
.update(framed(domain))
|
|
220
|
+
.update(framed(canonicalAuthorityEncoding(value)))
|
|
221
|
+
.digest("hex");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function keyedGatherBytesIdentity(domain: string, value: Uint8Array): string {
|
|
225
|
+
return createHmac("sha256", CATALOG_GATHER_AUTHORITY_KEY)
|
|
226
|
+
.update(framed(domain))
|
|
227
|
+
.update(`${value.byteLength}:`)
|
|
228
|
+
.update(value)
|
|
229
|
+
.digest("hex");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function createCatalogGatherAuthorityIdentity(
|
|
233
|
+
snapshot: CatalogAdmissionSnapshot,
|
|
234
|
+
sourceEvidence: CatalogSourceEvidence,
|
|
235
|
+
processLocal: CatalogProcessLocalEvidence,
|
|
236
|
+
discoveryPolicies: readonly CatalogProviderDiscoveryPolicySnapshot[],
|
|
237
|
+
): CatalogGatherAuthorityIdentity {
|
|
238
|
+
const sourceEvidenceIdentity = keyedGatherIdentity("catalog-source-evidence-v1", sourceEvidence);
|
|
239
|
+
const processLocalEvidenceIdentity = keyedGatherIdentity("catalog-process-local-v1", processLocal);
|
|
240
|
+
const discoveryPolicyIdentity = keyedGatherIdentity("catalog-discovery-policy-v1", discoveryPolicies);
|
|
241
|
+
return Object.freeze({
|
|
242
|
+
version: 1 as const,
|
|
243
|
+
authorityId: keyedGatherIdentity("catalog-authority-v1", {
|
|
244
|
+
admittedConfig: snapshot.configIdentity,
|
|
245
|
+
discoveryPolicyIdentity,
|
|
246
|
+
sourceEvidenceIdentity,
|
|
247
|
+
processLocalEvidenceIdentity,
|
|
248
|
+
}),
|
|
249
|
+
admittedConfig: Object.freeze({
|
|
250
|
+
...snapshot.configIdentity,
|
|
251
|
+
generation: Object.freeze({ ...snapshot.configIdentity.generation }),
|
|
252
|
+
}),
|
|
253
|
+
authSnapshotIdentity: keyedGatherIdentity(
|
|
254
|
+
"catalog-auth-v1",
|
|
255
|
+
sourceEvidence.conditional["provider-auth-selection"],
|
|
256
|
+
),
|
|
257
|
+
discoveryPolicyIdentity,
|
|
258
|
+
nativeCatalogSourceIdentity: keyedGatherIdentity(
|
|
259
|
+
"catalog-native-v1",
|
|
260
|
+
sourceEvidence.conditional["native-catalog-selection"],
|
|
261
|
+
),
|
|
262
|
+
sourceEvidenceIdentity,
|
|
263
|
+
processLocalEvidenceIdentity,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function detachedClone<T>(value: T): T {
|
|
268
|
+
if (Array.isArray(value)) return value.map(item => detachedClone(item)) as T;
|
|
269
|
+
if (value && typeof value === "object") {
|
|
270
|
+
const clone: Record<string, unknown> = {};
|
|
271
|
+
for (const key of Object.keys(value)) {
|
|
272
|
+
clone[key] = detachedClone((value as Record<string, unknown>)[key]);
|
|
273
|
+
}
|
|
274
|
+
return clone as T;
|
|
275
|
+
}
|
|
276
|
+
return value;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function recursivelyFreeze<T>(value: T): T {
|
|
280
|
+
if (!value || typeof value !== "object" || Object.isFrozen(value)) return value;
|
|
281
|
+
for (const nested of Object.values(value as Record<string, unknown>)) recursivelyFreeze(nested);
|
|
282
|
+
return Object.freeze(value);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function detachedFrozen<T>(value: T): T {
|
|
286
|
+
return recursivelyFreeze(detachedClone(value));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function capturedField<T extends object, K extends keyof T>(
|
|
290
|
+
value: T | undefined,
|
|
291
|
+
key: K,
|
|
292
|
+
): CatalogDiscoveryPolicyField<T[K]> {
|
|
293
|
+
if (!value || !Object.hasOwn(value, key)) return Object.freeze({ state: "absent" });
|
|
294
|
+
return detachedFrozen({ state: "present" as const, value: value[key] });
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function captureTrustedOpenAiApiPolicy(
|
|
298
|
+
name: string,
|
|
299
|
+
registryTransportMatch: boolean,
|
|
300
|
+
): CatalogTrustedOpenAiApiPolicySnapshot {
|
|
301
|
+
if (name !== OPENAI_API_PROVIDER_ID) return Object.freeze({ state: "unused" });
|
|
302
|
+
if (!registryTransportMatch) return Object.freeze({ state: "transport-mismatch" });
|
|
303
|
+
const entry = getProviderRegistryEntry(name);
|
|
304
|
+
if (!entry?.models) return Object.freeze({ state: "registry-models-absent" });
|
|
305
|
+
return detachedFrozen({
|
|
306
|
+
state: "captured" as const,
|
|
307
|
+
models: entry.models,
|
|
308
|
+
...(entry.modelContextWindows ? { modelContextWindows: entry.modelContextWindows } : {}),
|
|
309
|
+
...(entry.modelMaxInputTokens ? { modelMaxInputTokens: entry.modelMaxInputTokens } : {}),
|
|
310
|
+
...(entry.modelMaxOutputTokens ? { modelMaxOutputTokens: entry.modelMaxOutputTokens } : {}),
|
|
311
|
+
...(entry.virtualModels ? { virtualModels: entry.virtualModels } : {}),
|
|
312
|
+
...(entry.modelInputModalities ? { modelInputModalities: entry.modelInputModalities } : {}),
|
|
313
|
+
...(entry.modelReasoningEfforts ? { modelReasoningEfforts: entry.modelReasoningEfforts } : {}),
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function captureModelsRequest(
|
|
318
|
+
name: string,
|
|
319
|
+
provider: OcxProviderConfig,
|
|
320
|
+
observedAuth: ModelsAuthResolution | undefined,
|
|
321
|
+
): CapturedModelsRequest {
|
|
322
|
+
const observed = observedAuth
|
|
323
|
+
? { oauthApiBaseUrl: observedAuth.oauthApiBaseUrl }
|
|
324
|
+
: undefined;
|
|
325
|
+
const withoutCredential = buildModelsRequest(provider, undefined, name, observed);
|
|
326
|
+
const withCredential = buildModelsRequest(provider, REQUEST_CREDENTIAL_SENTINEL, name, observed);
|
|
327
|
+
const method = withoutCredential.method ?? "GET";
|
|
328
|
+
if (withoutCredential.url !== withCredential.url || method !== (withCredential.method ?? "GET")) {
|
|
329
|
+
throw new TypeError(`Provider model discovery URL for ${name} depends on credential bytes.`);
|
|
330
|
+
}
|
|
331
|
+
return detachedFrozen({
|
|
332
|
+
method,
|
|
333
|
+
url: withoutCredential.url,
|
|
334
|
+
headersWithoutCredential: withoutCredential.headers,
|
|
335
|
+
headersWithCredential: withCredential.headers,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
export function captureProviderGather(
|
|
339
|
+
name: string,
|
|
340
|
+
configured: OcxProviderConfig,
|
|
341
|
+
authResolver: ModelsAuthResolver,
|
|
342
|
+
retainConfiguredModelIds?: ReadonlySet<string>,
|
|
343
|
+
config?: Pick<OcxConfig, "providers">,
|
|
344
|
+
): CapturedProviderGather {
|
|
345
|
+
const enriched = detachedClone(withCanonicalOpenAiForwardAuthDefault(name, configured));
|
|
346
|
+
enrichProviderFromRegistry(name, enriched);
|
|
347
|
+
applyRegistryCapabilitySeedFill(name, enriched);
|
|
348
|
+
const registryTransportMatch = providerMatchesRegistryTransport(name, enriched);
|
|
349
|
+
const provider = recursivelyFreeze(enriched);
|
|
350
|
+
const fastPolicyAuthority = captureFastPolicyAuthority(
|
|
351
|
+
name,
|
|
352
|
+
provider,
|
|
353
|
+
registryTransportMatch,
|
|
354
|
+
configured,
|
|
355
|
+
);
|
|
356
|
+
const metadataModelIdCaseFold = shouldCaseFoldMetadataModelId(name);
|
|
357
|
+
const observedAuth = authResolver.kind === "observed"
|
|
358
|
+
&& provider.authMode !== "forward"
|
|
359
|
+
&& provider.liveModels !== false
|
|
360
|
+
? authResolver.resolve(name, provider)
|
|
361
|
+
: undefined;
|
|
362
|
+
const request = captureModelsRequest(name, provider, observedAuth);
|
|
363
|
+
const resolved = resolveProviderModelDiscovery(name, provider);
|
|
364
|
+
const discovery = detachedFrozen({
|
|
365
|
+
...(resolved.spec ? { spec: resolved.spec } : {}),
|
|
366
|
+
maxResponseBytes: resolved.maxResponseBytes,
|
|
367
|
+
maxModels: resolved.maxModels,
|
|
368
|
+
});
|
|
369
|
+
const trustedOpenAiApi = captureTrustedOpenAiApiPolicy(name, registryTransportMatch);
|
|
370
|
+
const policy = detachedFrozen({
|
|
371
|
+
provider: name,
|
|
372
|
+
registryTransportMatch,
|
|
373
|
+
location: {
|
|
374
|
+
spec: discovery.spec ? "present" as const : "absent" as const,
|
|
375
|
+
url: capturedField(discovery.spec, "url"),
|
|
376
|
+
path: capturedField(discovery.spec, "path"),
|
|
377
|
+
query: capturedField(discovery.spec, "query"),
|
|
378
|
+
},
|
|
379
|
+
finalMethod: request.method,
|
|
380
|
+
finalUrl: request.url,
|
|
381
|
+
filter: capturedField(discovery.spec, "filter"),
|
|
382
|
+
maxResponseBytes: discovery.maxResponseBytes,
|
|
383
|
+
maxModels: discovery.maxModels,
|
|
384
|
+
trustedOpenAiApi,
|
|
385
|
+
});
|
|
386
|
+
const effectiveAlias = effectiveProviderAliasDecision(name, configured, config);
|
|
387
|
+
return Object.freeze({
|
|
388
|
+
name,
|
|
389
|
+
provider,
|
|
390
|
+
discovery,
|
|
391
|
+
policy,
|
|
392
|
+
request,
|
|
393
|
+
fastPolicyAuthority,
|
|
394
|
+
metadataModelIdCaseFold,
|
|
395
|
+
effectiveAlias,
|
|
396
|
+
...(observedAuth ? { observedAuth: Object.freeze({ ...observedAuth }) } : {}),
|
|
397
|
+
...(retainConfiguredModelIds && retainConfiguredModelIds.size > 0
|
|
398
|
+
? { retainConfiguredModelIds }
|
|
399
|
+
: {}),
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
export function captureGatherFlight(
|
|
403
|
+
config: OcxConfig,
|
|
404
|
+
createAuthResolver: ModelsAuthResolverFactory,
|
|
405
|
+
): GatherFlightCapture {
|
|
406
|
+
const providerAuthOutcomes: CatalogGatherProviderAuthOutcome[] = [];
|
|
407
|
+
const authResolver = createAuthResolver(providerAuthOutcomes);
|
|
408
|
+
const comboTargetsByProvider = configuredComboTargetModelsByProvider(config);
|
|
409
|
+
const providers = Object.entries(config.providers)
|
|
410
|
+
.filter(([, provider]) => provider.disabled !== true)
|
|
411
|
+
.map(([name, provider]) => captureProviderGather(
|
|
412
|
+
name,
|
|
413
|
+
provider,
|
|
414
|
+
authResolver,
|
|
415
|
+
comboTargetsByProvider.get(name),
|
|
416
|
+
config,
|
|
417
|
+
));
|
|
418
|
+
const discoveryPolicySnapshots = Object.freeze(providers.map(provider => provider.policy));
|
|
419
|
+
return Object.freeze({
|
|
420
|
+
discoveryPolicyIdentity: keyedGatherIdentity("catalog-discovery-policy-v1", discoveryPolicySnapshots),
|
|
421
|
+
// Credentials are hashed under the same unexported per-process key, never
|
|
422
|
+
// stored or compared in the clear: this value can reach a map key and must
|
|
423
|
+
// not disclose a token. The final headers are included because a static
|
|
424
|
+
// header can carry authority just as an `apiKey` can.
|
|
425
|
+
authIdentity: keyedGatherIdentity("catalog-gather-auth-v1", providers.map(provider => ({
|
|
426
|
+
name: provider.name,
|
|
427
|
+
authMode: provider.provider.authMode ?? null,
|
|
428
|
+
liveModels: provider.provider.liveModels ?? null,
|
|
429
|
+
credential: provider.provider.apiKey ?? null,
|
|
430
|
+
observedAuth: provider.observedAuth ?? null,
|
|
431
|
+
headers: provider.request.headersWithCredential,
|
|
432
|
+
url: provider.request.url,
|
|
433
|
+
}))),
|
|
434
|
+
// Every enriched provider row the flight will gather from, in admission order.
|
|
435
|
+
// Anything that can change a catalog row lives in here by construction.
|
|
436
|
+
providerGraphIdentity: keyedGatherIdentity("catalog-gather-provider-graph-v1",
|
|
437
|
+
providers.map(provider => ({
|
|
438
|
+
name: provider.name,
|
|
439
|
+
// `fetch` is a caller-owned transport executor, not admitted state: the
|
|
440
|
+
// outbound transport honors it so a caller can supply its own HTTP path.
|
|
441
|
+
// It is the one member of a provider row that is legitimately a function,
|
|
442
|
+
// so it is dropped here rather than allowed to break every encode.
|
|
443
|
+
provider: omitProviderTransportExecutor(provider.provider),
|
|
444
|
+
fastPolicyAuthority: provider.fastPolicyAuthority,
|
|
445
|
+
// Combo retention is capture-time state, not a provider-row field. Two
|
|
446
|
+
// gathers that share providers but differ in combo targets must not join.
|
|
447
|
+
retainConfiguredModelIds: [...(provider.retainConfiguredModelIds ?? [])].sort(),
|
|
448
|
+
}))),
|
|
449
|
+
discoveryPolicySnapshots,
|
|
450
|
+
providers: Object.freeze(providers),
|
|
451
|
+
authResolver,
|
|
452
|
+
providerAuthOutcomes: Object.freeze([...providerAuthOutcomes]),
|
|
453
|
+
openAiApiPolicy: providers.find(provider => provider.name === OPENAI_API_PROVIDER_ID)?.policy.trustedOpenAiApi
|
|
454
|
+
?? Object.freeze({ state: "unused" as const }),
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Drop the caller-owned transport executor before hashing a provider row.
|
|
460
|
+
*
|
|
461
|
+
* Fails closed on anything ELSE that cannot be encoded: the point of hashing the
|
|
462
|
+
* whole row is that no field escapes the comparison, so a second function member
|
|
463
|
+
* must surface as an encode error rather than being quietly skipped here.
|
|
464
|
+
*/
|
|
465
|
+
function omitProviderTransportExecutor(provider: OcxProviderConfig): Record<string, unknown> {
|
|
466
|
+
const entries = Object.entries(provider).filter(([key]) => key !== "fetch");
|
|
467
|
+
return Object.fromEntries(entries);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export function materializeCapturedHeaders(
|
|
471
|
+
request: CapturedModelsRequest,
|
|
472
|
+
apiKey: string | undefined,
|
|
473
|
+
): Record<string, string> {
|
|
474
|
+
const source = apiKey ? request.headersWithCredential : request.headersWithoutCredential;
|
|
475
|
+
return Object.fromEntries(Object.entries(source).map(([name, value]) => [
|
|
476
|
+
name,
|
|
477
|
+
apiKey ? value.split(REQUEST_CREDENTIAL_SENTINEL).join(apiKey) : value,
|
|
478
|
+
]));
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function providerCatalogFingerprint(name: string, prov: OcxProviderConfig): Record<string, unknown> {
|
|
482
|
+
return {
|
|
483
|
+
n: name,
|
|
484
|
+
// Preserve the persisted tri-state. Registry enrichment may turn an omitted value into
|
|
485
|
+
// `false` while an explicit `true` stays live, so those callers must not share a flight.
|
|
486
|
+
live: prov.liveModels ?? null,
|
|
487
|
+
base: prov.baseUrl ?? "",
|
|
488
|
+
adapter: prov.adapter ?? "",
|
|
489
|
+
models: [...(prov.models ?? [])].sort(),
|
|
490
|
+
retain: [...(prov.retainModels ?? [])].sort(),
|
|
491
|
+
selected: [...(prov.selectedModels ?? [])].sort(),
|
|
492
|
+
displayNames: prov.modelDisplayNames ?? null,
|
|
493
|
+
defaultModel: prov.defaultModel ?? null,
|
|
494
|
+
ctx: prov.contextWindow ?? null,
|
|
495
|
+
ctxW: prov.modelContextWindows ?? null,
|
|
496
|
+
maxIn: prov.modelMaxInputTokens ?? null,
|
|
497
|
+
maxOut: prov.modelMaxOutputTokens ?? null,
|
|
498
|
+
autoCompact: prov.modelAutoCompactTokenLimits ?? null,
|
|
499
|
+
inMod: prov.modelInputModalities ?? null,
|
|
500
|
+
capabilities: prov.modelCapabilities ?? null,
|
|
501
|
+
re: prov.modelReasoningEfforts ?? null,
|
|
502
|
+
defRe: prov.modelDefaultReasoningEfforts ?? null,
|
|
503
|
+
rsSum: prov.modelSupportsReasoningSummaries ?? null,
|
|
504
|
+
verbosity: prov.modelSupportsVerbosity ?? null,
|
|
505
|
+
rsDel: prov.modelReasoningSummaryDelivery ?? null,
|
|
506
|
+
serviceTier: prov.modelSupportsServiceTier ?? null,
|
|
507
|
+
noVis: [...(prov.noVisionModels ?? [])].sort(),
|
|
508
|
+
ptc: prov.parallelToolCalls ?? null,
|
|
509
|
+
gMode: prov.googleMode ?? null,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export function gatherFlightKey(config: OcxConfig): string {
|
|
514
|
+
const providers = Object.entries(config.providers)
|
|
515
|
+
.filter(([, prov]) => prov.disabled !== true)
|
|
516
|
+
.map(([name, prov]) => providerCatalogFingerprint(name, prov))
|
|
517
|
+
.sort((a, b) => String(a.n).localeCompare(String(b.n)));
|
|
518
|
+
const assembly = stableJson({
|
|
519
|
+
providers,
|
|
520
|
+
disabledModels: [...(config.disabledModels ?? [])].sort(),
|
|
521
|
+
combos: config.combos ?? {},
|
|
522
|
+
customModels: (config.customModels ?? []).map((cm) => ({
|
|
523
|
+
p: cm.provider,
|
|
524
|
+
m: cm.modelId,
|
|
525
|
+
d: cm.displayName ?? null,
|
|
526
|
+
cw: cm.contextWindow ?? null,
|
|
527
|
+
im: cm.inputModalities ?? null,
|
|
528
|
+
})),
|
|
529
|
+
caps: config.providerContextCaps ?? null,
|
|
530
|
+
});
|
|
531
|
+
const digest = createHash("sha256").update(assembly).digest("hex").slice(0, 16);
|
|
532
|
+
return `${digest}#${config.modelCacheTtlMs ?? DEFAULT_MODEL_CACHE_TTL_MS}`;
|
|
533
|
+
}
|