@bitkyc08/opencodex 2.10.0 → 2.10.1
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/AGENTS_INSTALL.md +77 -0
- package/README.md +4 -10
- package/bin/ocx.mjs +71 -18
- package/gui/dist/assets/index-Cd6_PBKn.css +1 -0
- package/gui/dist/assets/index-ChZQsmBY.js +70 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/alibaba-color.svg +1 -1
- package/gui/dist/provider-icons/antigravity-color.svg +1 -1
- package/gui/dist/provider-icons/claude-color.svg +1 -1
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/copilot-color.svg +1 -1
- package/gui/dist/provider-icons/cursor-color.svg +1 -1
- package/gui/dist/provider-icons/deepseek-color.svg +1 -1
- package/gui/dist/provider-icons/firepass-color.svg +1 -1
- package/gui/dist/provider-icons/fireworks-color.svg +1 -1
- package/gui/dist/provider-icons/gemini-color.svg +1 -1
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
- package/gui/dist/provider-icons/grok.svg +1 -1
- package/gui/dist/provider-icons/groq-color.svg +1 -1
- package/gui/dist/provider-icons/huggingface-color.svg +1 -1
- package/gui/dist/provider-icons/kimi-color.svg +1 -1
- package/gui/dist/provider-icons/kiro-color.svg +2 -2
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
- package/gui/dist/provider-icons/mistral-color.svg +1 -1
- package/gui/dist/provider-icons/moonshot-color.svg +1 -1
- package/gui/dist/provider-icons/nvidia-color.svg +1 -1
- package/gui/dist/provider-icons/ollama-color.svg +1 -1
- package/gui/dist/provider-icons/openai.svg +1 -1
- package/gui/dist/provider-icons/opencode.svg +2 -1
- package/gui/dist/provider-icons/openrouter-color.svg +1 -1
- package/gui/dist/provider-icons/pi.svg +2 -2
- package/gui/dist/provider-icons/qianfan-color.svg +1 -1
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/vllm-color.svg +1 -1
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
- package/package.json +8 -4
- package/src/adapters/anthropic.ts +208 -14
- package/src/adapters/base.ts +16 -5
- package/src/adapters/cursor/effort-map.ts +3 -2
- package/src/adapters/cursor/framing.ts +39 -0
- package/src/adapters/cursor/live-transport.ts +105 -95
- package/src/adapters/cursor/native-exec.ts +32 -6
- package/src/adapters/cursor/protobuf-request.ts +20 -15
- package/src/adapters/cursor/request-builder.ts +21 -7
- package/src/adapters/cursor/types.ts +7 -0
- package/src/adapters/google-antigravity-replay.ts +237 -21
- package/src/adapters/google-truncation.ts +11 -0
- package/src/adapters/google.ts +50 -9
- package/src/adapters/identity.ts +39 -6
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +10 -2
- package/src/adapters/kiro-tools.ts +10 -1
- package/src/adapters/kiro.ts +37 -11
- package/src/adapters/openai-chat.ts +284 -83
- package/src/adapters/openai-responses.ts +182 -24
- package/src/bridge.ts +177 -7
- package/src/chat/outbound.ts +78 -23
- package/src/claude/agents-inject.ts +27 -5
- package/src/claude/inbound.ts +11 -1
- package/src/claude/model-info.ts +13 -10
- package/src/claude/outbound.ts +17 -0
- package/src/cli/account-api.ts +24 -0
- package/src/cli/account-auth.ts +31 -6
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +5 -0
- package/src/cli/claude.ts +2 -1
- package/src/cli/doctor.ts +93 -22
- package/src/cli/export-command.ts +26 -12
- package/src/cli/help.ts +8 -6
- package/src/cli/index.ts +56 -22
- package/src/cli/integrations.ts +84 -1
- package/src/cli/observe.ts +54 -1
- package/src/cli/opencode.ts +2 -1
- package/src/cli/provider-runtime.ts +18 -1
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +6 -3
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +1 -1
- package/src/cli/v2.ts +36 -9
- package/src/clients/config-export.ts +687 -10
- package/src/codex/account-lifecycle.ts +30 -5
- package/src/codex/account-usability.ts +22 -2
- package/src/codex/admission.ts +255 -0
- package/src/codex/auth-api.ts +427 -140
- package/src/codex/auth-context.ts +155 -30
- package/src/codex/autostart-health.ts +8 -1
- package/src/codex/catalog/account-models.ts +62 -0
- package/src/codex/catalog/aggregation.ts +14 -1
- package/src/codex/catalog/bundled.ts +282 -32
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/metadata.ts +51 -6
- package/src/codex/catalog/parsing.ts +6 -3
- package/src/codex/catalog/provider-fetch.ts +576 -41
- package/src/codex/catalog/sync.ts +505 -66
- package/src/codex/catalog-admission.ts +197 -0
- package/src/codex/catalog-write-serialization.ts +241 -0
- package/src/codex/catalog.ts +2 -1
- package/src/codex/codex-write-lock.ts +372 -0
- package/src/codex/convergence-types.ts +593 -0
- package/src/codex/convergence.ts +441 -0
- package/src/codex/desired-state.ts +177 -0
- package/src/codex/features.ts +52 -8
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +257 -0
- package/src/codex/history-lock.ts +241 -0
- package/src/codex/history-migration-guardian.ts +18 -5
- package/src/codex/history-provider.ts +9 -2
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +176 -0
- package/src/codex/inject-coordination.ts +245 -0
- package/src/codex/inject.ts +605 -124
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +80 -0
- package/src/codex/journal.ts +10 -1
- package/src/codex/main-account-cache.ts +24 -0
- package/src/codex/management-convergence.ts +114 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +167 -0
- package/src/codex/native-main-lock-file.ts +158 -0
- package/src/codex/native-main-owner.ts +315 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1512 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +340 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +557 -0
- package/src/codex/project-config-warnings.ts +18 -4
- package/src/codex/prompt-journal.ts +311 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/quota-rejection.ts +224 -0
- package/src/codex/quota.ts +86 -3
- package/src/codex/routing.ts +299 -62
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +39 -13
- package/src/codex/subagent-model-fallback.ts +73 -12
- package/src/codex/transition-state.ts +604 -0
- package/src/codex/upstream-host-health.ts +70 -0
- package/src/codex/user-identity.ts +266 -0
- package/src/codex/write-coordination.ts +114 -0
- package/src/config.ts +562 -26
- package/src/generated/jawcode-model-metadata.ts +2 -2
- package/src/grok/inject.ts +15 -4
- package/src/grok/inspect.ts +45 -0
- package/src/images/loop.ts +113 -20
- package/src/integrations/config-io.ts +151 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/native/ownership-preflight.ts +165 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +101 -0
- package/src/integrations/serialize.ts +235 -0
- package/src/integrations/state.ts +290 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer.ts +492 -0
- package/src/lib/bounded-body.ts +46 -8
- package/src/lib/bun-runtime.ts +110 -1
- package/src/lib/bun-stream-caps.ts +2 -1
- package/src/lib/redact.ts +407 -2
- package/src/lib/shadow-call.ts +24 -0
- package/src/lib/translator-budget.ts +10 -0
- package/src/lib/upstream-reachability.ts +91 -0
- package/src/lib/upstream-retry.ts +154 -2
- package/src/lib/windows-secret-acl.ts +212 -11
- package/src/lib/winsw.ts +9 -3
- package/src/oauth/index.ts +61 -3
- package/src/oauth/key-providers.ts +4 -0
- package/src/oauth/kiro.ts +50 -6
- package/src/oauth/store.ts +31 -6
- package/src/oauth/token-guardian.ts +9 -3
- package/src/providers/codex-capacity.ts +288 -0
- package/src/providers/derive.ts +33 -1
- package/src/providers/free-directory.ts +3 -1
- package/src/providers/key-failover.ts +71 -3
- package/src/providers/openai-sidecar.ts +64 -4
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/quota.ts +334 -26
- package/src/providers/registry.ts +284 -16
- package/src/providers/xai-transport.ts +11 -4
- package/src/responses/compaction.ts +8 -1
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/parser.ts +44 -2
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/reasoning-replay-cache.ts +105 -0
- package/src/responses/spill-store.ts +45 -8
- package/src/responses/state.ts +161 -17
- package/src/router.ts +302 -16
- package/src/routing/analytics.ts +377 -0
- package/src/routing/capability.ts +204 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +444 -0
- package/src/routing/health.ts +401 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +590 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile.ts +423 -0
- package/src/routing/quota.ts +124 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +686 -0
- package/src/server/auth-cors.ts +46 -6
- package/src/server/chat-completions.ts +28 -13
- package/src/server/claude-messages.ts +23 -15
- package/src/server/gui-static.ts +39 -10
- package/src/server/images.ts +10 -1
- package/src/server/index.ts +238 -52
- package/src/server/lifecycle.ts +228 -9
- package/src/server/live.ts +14 -3
- package/src/server/management/agent-settings-routes.ts +64 -14
- package/src/server/management/combo-routes.ts +5 -5
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/context.ts +42 -2
- package/src/server/management/integration-routes.ts +538 -0
- package/src/server/management/logs-usage-routes.ts +1 -1
- package/src/server/management/model-routes.ts +32 -113
- package/src/server/management/model-rows.ts +117 -0
- package/src/server/management/native-integration-routes.ts +587 -0
- package/src/server/management/oauth-account-routes.ts +1 -1
- package/src/server/management/provider-routes.ts +218 -117
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +177 -0
- package/src/server/management/shared.ts +2 -2
- package/src/server/management/sidebar-routes.ts +47 -31
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +276 -24
- package/src/server/management/system-routes.ts +4 -0
- package/src/server/management-api.ts +84 -9
- package/src/server/management-auth.ts +43 -5
- package/src/server/relay-eager.ts +82 -42
- package/src/server/relay.ts +120 -6
- package/src/server/request-log.ts +26 -6
- package/src/server/responses/collaboration.ts +63 -8
- package/src/server/responses/compact.ts +272 -41
- package/src/server/responses/core.ts +730 -132
- package/src/server/responses/fetch-helpers.ts +15 -1
- package/src/server/responses-item-id-repair.ts +32 -3
- package/src/server/responses-json-events.ts +52 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/search.ts +51 -6
- package/src/server/sse-payload-rewrite.ts +89 -12
- package/src/server/startup-health-cache.ts +7 -1
- package/src/server/ws-bridge.ts +11 -17
- package/src/service-manager-probe.ts +297 -0
- package/src/service.ts +222 -32
- package/src/tray/windows-tray.ps1 +9 -0
- package/src/tray/windows.ts +15 -7
- package/src/types.ts +194 -14
- package/src/update/index.ts +13 -13
- package/src/update/job.ts +24 -21
- package/src/update/notify.ts +7 -3
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +129 -10
- package/src/usage/log.ts +50 -15
- package/src/usage/summary.ts +4 -4
- package/src/vision/index.ts +6 -1
- package/src/web-search/loop.ts +161 -34
- package/gui/dist/assets/index-OY43ubAq.css +0 -1
- package/gui/dist/assets/index-YwNnKZcL.js +0 -67
- package/gui/dist/provider-icons/antigravity.svg +0 -1
- package/gui/dist/provider-icons/claude.svg +0 -1
- package/gui/dist/provider-icons/copilot.svg +0 -1
- package/gui/dist/provider-icons/cursor.svg +0 -2
- package/gui/dist/provider-icons/gemini.svg +0 -1
- package/gui/dist/provider-icons/grok-color.svg +0 -1
- package/gui/dist/provider-icons/kiro.svg +0 -14
package/src/router.ts
CHANGED
|
@@ -1,19 +1,69 @@
|
|
|
1
1
|
import type { CodexAccountMode, OcxConfig, OcxProviderConfig } from "./types";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
getCombo,
|
|
4
|
+
isComboTargetInCooldown,
|
|
5
|
+
preservesPhysicalComboProvider,
|
|
6
|
+
targetKey,
|
|
7
|
+
tryPickComboModel,
|
|
8
|
+
type ComboPick,
|
|
9
|
+
} from "./combos";
|
|
10
|
+
import type { NormalizedComboConfig } from "./combos/types";
|
|
3
11
|
import { hasOwnProvider, resolveEnvValue } from "./config";
|
|
4
12
|
import { assertProviderDestinationAllowed } from "./lib/destination-policy";
|
|
5
13
|
import { redactSecretString, redactUrlForLog } from "./lib/redact";
|
|
6
14
|
import { PROVIDER_REGISTRY, providerCodexAccountMode, providerMatchesRegistryTransport } from "./providers/registry";
|
|
7
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
isCanonicalOpenAiForwardProvider,
|
|
17
|
+
LEGACY_CHATGPT_PROVIDER_ID,
|
|
18
|
+
LEGACY_OPENAI_MULTI_PROVIDER_ID,
|
|
19
|
+
OPENAI_API_PROVIDER_ID,
|
|
20
|
+
OPENAI_CODEX_PROVIDER_ID,
|
|
21
|
+
} from "./providers/openai-tiers";
|
|
8
22
|
import { decodeRoutedModelId, encodeRoutedModelId } from "./providers/slug-codec";
|
|
9
23
|
import { getStaleCached } from "./codex/model-cache";
|
|
24
|
+
import { codexAccountNamespaceEntries } from "./codex/account-namespaces";
|
|
25
|
+
import { getEffectiveActiveCodexAccountId } from "./codex/routing";
|
|
26
|
+
import { getAccountSet } from "./oauth/store";
|
|
27
|
+
import {
|
|
28
|
+
buildRouteDecisionTrace,
|
|
29
|
+
type RouteDecisionKind,
|
|
30
|
+
type RouteDecisionTraceV1,
|
|
31
|
+
type TraceCandidateInput,
|
|
32
|
+
} from "./routing/trace";
|
|
33
|
+
import { getRoutingProfile, resolvePolicyProfileId } from "./routing/profile";
|
|
34
|
+
import { evaluatePolicyProfile, type PolicyRequestEvidence } from "./routing/evaluator";
|
|
35
|
+
import { candidateCapabilityEvidence } from "./routing/capability";
|
|
36
|
+
import { policyCandidateHealthEvidence } from "./routing/health";
|
|
37
|
+
import { quotaEvidenceForCandidate } from "./routing/quota";
|
|
38
|
+
import { costEvidenceForCandidate } from "./routing/cost";
|
|
39
|
+
|
|
40
|
+
export class NoEligiblePolicyCandidateError extends Error {
|
|
41
|
+
/** Evaluation trace (with per-candidate exclusions) when nothing qualified. */
|
|
42
|
+
readonly trace?: RouteDecisionTraceV1;
|
|
43
|
+
|
|
44
|
+
constructor(readonly profileId: string, trace?: RouteDecisionTraceV1) {
|
|
45
|
+
super(`No eligible candidates for policy profile: ${profileId}`);
|
|
46
|
+
this.name = "NoEligiblePolicyCandidateError";
|
|
47
|
+
this.trace = trace;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
10
50
|
|
|
11
51
|
export interface RouteResult {
|
|
12
52
|
providerName: string;
|
|
13
53
|
provider: OcxProviderConfig;
|
|
14
54
|
modelId: string;
|
|
55
|
+
/** Which deterministic routing path produced this route (RI-01). */
|
|
56
|
+
routeKind: RouteDecisionKind;
|
|
57
|
+
/** Stable wire reason code for the selected route (RI-01). */
|
|
58
|
+
routeReason: string;
|
|
15
59
|
codexAccountMode?: CodexAccountMode;
|
|
60
|
+
/** Exact account selected by an account-qualified native model. */
|
|
61
|
+
codexAccountId?: string;
|
|
62
|
+
/** Public namespace used by the account-qualified selector. */
|
|
63
|
+
codexAccountNamespace?: string;
|
|
16
64
|
combo?: ComboPick;
|
|
65
|
+
/** Bounded route-decision trace (RI-01); never contains secrets. */
|
|
66
|
+
routeDecision?: RouteDecisionTraceV1;
|
|
17
67
|
}
|
|
18
68
|
|
|
19
69
|
const MODEL_PROVIDER_PATTERNS: Array<{ providerNames: string[]; prefixes: string[] }> = [
|
|
@@ -259,6 +309,29 @@ function routedProviderConfig(providerName: string, provider: OcxProviderConfig)
|
|
|
259
309
|
...(provider.responsesPath === undefined && registryEntry.responsesPath !== undefined
|
|
260
310
|
? { responsesPath: registryEntry.responsesPath }
|
|
261
311
|
: {}),
|
|
312
|
+
...(provider.supportsServiceTier === undefined && registryEntry.supportsServiceTier !== undefined
|
|
313
|
+
? { supportsServiceTier: registryEntry.supportsServiceTier }
|
|
314
|
+
: {}),
|
|
315
|
+
...(provider.preserveResponsesReasoningContent === undefined && registryEntry.preserveResponsesReasoningContent !== undefined
|
|
316
|
+
? { preserveResponsesReasoningContent: registryEntry.preserveResponsesReasoningContent }
|
|
317
|
+
: {}),
|
|
318
|
+
// Registry-only client-facing repair policy (#938): fill only when the
|
|
319
|
+
// saved provider has no explicit policy; clone so runtime never aliases
|
|
320
|
+
// the registry constant.
|
|
321
|
+
...(provider.responsesItemIdRepair === undefined && registryEntry.responsesItemIdRepair
|
|
322
|
+
? {
|
|
323
|
+
responsesItemIdRepair: {
|
|
324
|
+
...(registryEntry.responsesItemIdRepair.message ? { message: [...registryEntry.responsesItemIdRepair.message] } : {}),
|
|
325
|
+
...(registryEntry.responsesItemIdRepair.reasoning ? { reasoning: [...registryEntry.responsesItemIdRepair.reasoning] } : {}),
|
|
326
|
+
...(registryEntry.responsesItemIdRepair.repairMissingTerminalIds !== undefined
|
|
327
|
+
? { repairMissingTerminalIds: registryEntry.responsesItemIdRepair.repairMissingTerminalIds }
|
|
328
|
+
: {}),
|
|
329
|
+
...(registryEntry.responsesItemIdRepair.repairInvalidIds !== undefined
|
|
330
|
+
? { repairInvalidIds: registryEntry.responsesItemIdRepair.repairInvalidIds }
|
|
331
|
+
: {}),
|
|
332
|
+
},
|
|
333
|
+
}
|
|
334
|
+
: {}),
|
|
262
335
|
authMode: canonicalAuthMode,
|
|
263
336
|
apiKey: resolvedApiKey,
|
|
264
337
|
// Backfill the Google wire mode + Vertex project/location from the registry when the user
|
|
@@ -276,6 +349,9 @@ function routedProviderConfig(providerName: string, provider: OcxProviderConfig)
|
|
|
276
349
|
// opt-in, while an explicit user `false` keeps overriding registry `true`.
|
|
277
350
|
...(provider.parallelToolCalls === undefined && registryEntry.parallelToolCalls !== undefined ? { parallelToolCalls: registryEntry.parallelToolCalls } : {}),
|
|
278
351
|
...(provider.promptCacheKey === undefined && registryEntry.promptCacheKey !== undefined ? { promptCacheKey: registryEntry.promptCacheKey } : {}),
|
|
352
|
+
...(provider.reasoningWireFormat === undefined && registryEntry.reasoningWireFormat !== undefined
|
|
353
|
+
? { reasoningWireFormat: registryEntry.reasoningWireFormat }
|
|
354
|
+
: {}),
|
|
279
355
|
...(provider.defaultMaxOutputTokens === undefined && registryEntry.defaultMaxOutputTokens !== undefined
|
|
280
356
|
? { defaultMaxOutputTokens: registryEntry.defaultMaxOutputTokens }
|
|
281
357
|
: {}),
|
|
@@ -315,6 +391,34 @@ export class NoEnabledOpenAiProviderError extends Error {
|
|
|
315
391
|
}
|
|
316
392
|
}
|
|
317
393
|
|
|
394
|
+
/**
|
|
395
|
+
* One immutable selection trace for a combo request: built once from the
|
|
396
|
+
* initial pick, before any child dispatch. Fallback execution stays in the
|
|
397
|
+
* usage entry's `attempts[]`; the trace never changes after selection.
|
|
398
|
+
*/
|
|
399
|
+
export function comboRouteDecisionTrace(
|
|
400
|
+
config: OcxConfig,
|
|
401
|
+
comboId: string,
|
|
402
|
+
pick: ComboPick,
|
|
403
|
+
requestedModel: string,
|
|
404
|
+
): RouteDecisionTraceV1 {
|
|
405
|
+
const combo = getCombo(config, comboId);
|
|
406
|
+
return buildRouteDecisionTrace({
|
|
407
|
+
requestedModel,
|
|
408
|
+
routeKind: "combo",
|
|
409
|
+
selected: {
|
|
410
|
+
provider: pick.target.provider,
|
|
411
|
+
model: pick.target.model,
|
|
412
|
+
reason: "combo-pick",
|
|
413
|
+
candidateIndex: pick.targetIndex,
|
|
414
|
+
...(combo
|
|
415
|
+
? { tieBreak: combo.strategy === "round-robin" ? "round-robin" : "failover" }
|
|
416
|
+
: {}),
|
|
417
|
+
},
|
|
418
|
+
candidates: combo ? comboRouteCandidates(config, pick, combo) : undefined,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
318
422
|
// Codex uses a small number of control-plane model ids that are not part of the public GPT/o
|
|
319
423
|
// naming families. Keep this exact: a broad `codex-*` rule could capture a third-party model.
|
|
320
424
|
const CODEX_INTERNAL_OPENAI_MODELS = new Set(["codex-auto-review"]);
|
|
@@ -324,25 +428,170 @@ function isBareOpenAiFamilyModel(modelId: string): boolean {
|
|
|
324
428
|
&& (/^(?:gpt-|o1-|o3-|o4-)/.test(modelId) || CODEX_INTERNAL_OPENAI_MODELS.has(modelId));
|
|
325
429
|
}
|
|
326
430
|
|
|
327
|
-
function routeResult(
|
|
431
|
+
function routeResult(
|
|
432
|
+
providerName: string,
|
|
433
|
+
provider: OcxProviderConfig,
|
|
434
|
+
modelId: string,
|
|
435
|
+
routeKind: RouteDecisionKind,
|
|
436
|
+
routeReason: string,
|
|
437
|
+
): RouteResult {
|
|
328
438
|
const codexAccountMode = providerCodexAccountMode(providerName, provider);
|
|
329
439
|
return {
|
|
330
440
|
providerName,
|
|
331
441
|
provider: routedProviderConfig(providerName, provider),
|
|
332
442
|
modelId,
|
|
443
|
+
routeKind,
|
|
444
|
+
routeReason,
|
|
333
445
|
...(codexAccountMode ? { codexAccountMode } : {}),
|
|
334
446
|
};
|
|
335
447
|
}
|
|
336
448
|
|
|
337
|
-
|
|
449
|
+
/**
|
|
450
|
+
* Candidate evidence for a combo route: every configured target with its
|
|
451
|
+
* selection-time eligibility and exclusion reasons. Purely observational; the
|
|
452
|
+
* pick already happened and this never re-selects.
|
|
453
|
+
*/
|
|
454
|
+
function comboRouteCandidates(
|
|
455
|
+
config: OcxConfig,
|
|
456
|
+
pick: NonNullable<RouteResult["combo"]>,
|
|
457
|
+
combo: NormalizedComboConfig,
|
|
458
|
+
): TraceCandidateInput[] {
|
|
459
|
+
const now = Date.now();
|
|
460
|
+
return combo.targets.map((target, index) => {
|
|
461
|
+
const key = targetKey(target);
|
|
462
|
+
const provider = config.providers[target.provider];
|
|
463
|
+
const configured = provider !== undefined;
|
|
464
|
+
const enabled = configured && provider.disabled !== true;
|
|
465
|
+
const inCooldown = isComboTargetInCooldown(pick.comboId, target, now);
|
|
466
|
+
const isSelected = index === pick.targetIndex;
|
|
467
|
+
// The pick's `attempted` list includes the winner itself; only non-selected
|
|
468
|
+
// targets can be "already-attempted" (fallback picks exclude earlier tries).
|
|
469
|
+
const alreadyAttempted = !isSelected && pick.attempted.includes(key);
|
|
470
|
+
const exclusions: TraceCandidateInput["exclusions"] = [];
|
|
471
|
+
if (!configured) exclusions.push({ code: "unconfigured" });
|
|
472
|
+
if (configured && !enabled) exclusions.push({ code: "disabled" });
|
|
473
|
+
if (inCooldown) exclusions.push({ code: "cooldown" });
|
|
474
|
+
if (isSelected && inCooldown) exclusions.push({ code: "selected-despite-cooldown" });
|
|
475
|
+
if (!isSelected && alreadyAttempted && exclusions.length === 0) {
|
|
476
|
+
exclusions.push({ code: "already-attempted" });
|
|
477
|
+
}
|
|
478
|
+
if (!isSelected && exclusions.length === 0) exclusions.push({ code: "not-selected" });
|
|
479
|
+
return {
|
|
480
|
+
provider: target.provider,
|
|
481
|
+
model: target.model,
|
|
482
|
+
eligible: enabled && !inCooldown && !alreadyAttempted,
|
|
483
|
+
exclusions,
|
|
484
|
+
};
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function routeModelInternal(
|
|
489
|
+
config: OcxConfig,
|
|
490
|
+
modelId: string,
|
|
491
|
+
bypassCombos: boolean,
|
|
492
|
+
policyEvidence?: PolicyRequestEvidence,
|
|
493
|
+
): RouteResult {
|
|
494
|
+
const slash = modelId.indexOf("/");
|
|
495
|
+
// Policy namespace is system-reserved: an explicit `policy/<id>` or a
|
|
496
|
+
// configured profile alias executes the policy evaluator and routes the
|
|
497
|
+
// selected candidate. Only explicit requests reach this branch; concrete
|
|
498
|
+
// recursive targets skip policy resolution entirely (bypassCombos) so an
|
|
499
|
+
// alias matching a selected candidate can never recurse, and a
|
|
500
|
+
// `policy/<id>` without a configured profile falls through to normal
|
|
501
|
+
// provider/default resolution instead of failing.
|
|
502
|
+
const policyId = !bypassCombos ? resolvePolicyProfileId(config, modelId) : null;
|
|
503
|
+
const profile = policyId ? getRoutingProfile(config, policyId) : undefined;
|
|
504
|
+
if (profile && policyId) {
|
|
505
|
+
// One clock read per decision keeps candidate evidence, exclusions, and
|
|
506
|
+
// scores mutually consistent and reproducible.
|
|
507
|
+
const now = Date.now();
|
|
508
|
+
const candidateEvidence = profile.candidates.map(candidate => ({
|
|
509
|
+
provider: candidate.provider,
|
|
510
|
+
model: candidate.model,
|
|
511
|
+
capability: candidateCapabilityEvidence(config, candidate.provider, candidate.model),
|
|
512
|
+
health: policyCandidateHealthEvidence(config, candidate, now),
|
|
513
|
+
quota: quotaEvidenceForCandidate({
|
|
514
|
+
provider: candidate.provider,
|
|
515
|
+
model: candidate.model,
|
|
516
|
+
...(candidate.provider === OPENAI_CODEX_PROVIDER_ID
|
|
517
|
+
&& providerCodexAccountMode(
|
|
518
|
+
OPENAI_CODEX_PROVIDER_ID,
|
|
519
|
+
config.providers[OPENAI_CODEX_PROVIDER_ID],
|
|
520
|
+
) === "pool"
|
|
521
|
+
? (() => {
|
|
522
|
+
const codexAccountId = getEffectiveActiveCodexAccountId(config);
|
|
523
|
+
return {
|
|
524
|
+
codexAccountId,
|
|
525
|
+
codexAccountPlan: codexAccountId
|
|
526
|
+
? config.codexAccounts?.find(account => account.id === codexAccountId)?.plan
|
|
527
|
+
: undefined,
|
|
528
|
+
};
|
|
529
|
+
})()
|
|
530
|
+
: {}),
|
|
531
|
+
accountRef: candidate.provider === "anthropic"
|
|
532
|
+
? getAccountSet("anthropic")?.activeAccountId
|
|
533
|
+
: undefined,
|
|
534
|
+
}),
|
|
535
|
+
cost: costEvidenceForCandidate({
|
|
536
|
+
provider: candidate.provider,
|
|
537
|
+
model: candidate.model,
|
|
538
|
+
limitUsd: profile.limits.maxEstimatedCostUsd,
|
|
539
|
+
}),
|
|
540
|
+
}));
|
|
541
|
+
const evaluation = evaluatePolicyProfile(config, policyId, policyEvidence ?? {}, candidateEvidence, now);
|
|
542
|
+
if (evaluation.selectedIndex === null) {
|
|
543
|
+
throw new NoEligiblePolicyCandidateError(policyId, evaluation.trace);
|
|
544
|
+
}
|
|
545
|
+
const selected = evaluation.candidates[evaluation.selectedIndex]!;
|
|
546
|
+
const concrete = `${selected.provider}/${selected.model}`;
|
|
547
|
+
const routed = routeModelInternal(config, concrete, true);
|
|
548
|
+
return {
|
|
549
|
+
...routed,
|
|
550
|
+
routeKind: "policy" as const,
|
|
551
|
+
routeReason: "policy-selected",
|
|
552
|
+
routeDecision: evaluation.trace,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
if (slash > 0) {
|
|
556
|
+
const namespace = modelId.slice(0, slash);
|
|
557
|
+
const binding = codexAccountNamespaceEntries(config)
|
|
558
|
+
.find(([candidate]) => candidate === namespace);
|
|
559
|
+
if (binding) {
|
|
560
|
+
const nativeModelId = modelId.slice(slash + 1);
|
|
561
|
+
if (!isBareOpenAiFamilyModel(nativeModelId)) {
|
|
562
|
+
throw new Error(`Codex account namespace ${namespace} only supports native OpenAI model ids`);
|
|
563
|
+
}
|
|
564
|
+
const provider = config.providers[OPENAI_CODEX_PROVIDER_ID];
|
|
565
|
+
if (!provider || provider.disabled === true) {
|
|
566
|
+
throw new NoEnabledOpenAiProviderError(nativeModelId);
|
|
567
|
+
}
|
|
568
|
+
// Registry routing backfills an omitted authMode on the built-in OpenAI row to forward.
|
|
569
|
+
// Mirror only that default here; explicit non-forward modes still fail closed.
|
|
570
|
+
const providerForCanonicalCheck = provider.authMode === undefined
|
|
571
|
+
? { ...provider, authMode: "forward" as const }
|
|
572
|
+
: provider;
|
|
573
|
+
if (!isCanonicalOpenAiForwardProvider(providerForCanonicalCheck)) {
|
|
574
|
+
throw new NoEnabledOpenAiProviderError(nativeModelId);
|
|
575
|
+
}
|
|
576
|
+
return {
|
|
577
|
+
...routeResult(OPENAI_CODEX_PROVIDER_ID, provider, nativeModelId, "explicit-account", "account-namespace"),
|
|
578
|
+
// Exact account injection uses the pool credential machinery even when the canonical
|
|
579
|
+
// provider is globally Direct. The fixed id bypasses pool selection entirely.
|
|
580
|
+
codexAccountMode: "pool",
|
|
581
|
+
codexAccountId: binding[1],
|
|
582
|
+
codexAccountNamespace: namespace,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
338
587
|
if (!bypassCombos && !preservesPhysicalComboProvider(config)) {
|
|
339
588
|
const combo = tryPickComboModel(config, modelId);
|
|
340
589
|
if (combo) {
|
|
341
590
|
const concrete = `${combo.target.provider}/${combo.target.model}`;
|
|
342
591
|
// The selected target is already a concrete provider/model reference. Resolve it without
|
|
343
592
|
// consulting combo aliases again, otherwise an alias that shadows the target can recurse.
|
|
344
|
-
const routed = routeModelInternal(config, concrete, true);
|
|
345
|
-
return { ...routed, combo };
|
|
593
|
+
const routed = routeModelInternal(config, concrete, true, undefined);
|
|
594
|
+
return { ...routed, combo, routeKind: "combo" as const, routeReason: "combo-pick" };
|
|
346
595
|
}
|
|
347
596
|
}
|
|
348
597
|
|
|
@@ -350,7 +599,6 @@ function routeModelInternal(config: OcxConfig, modelId: string, bypassCombos: bo
|
|
|
350
599
|
// Only triggers when the prefix matches a CONFIGURED provider, so genuine
|
|
351
600
|
// slash-containing model ids (e.g. "anthropic/claude-...") fall through when
|
|
352
601
|
// no such provider exists.
|
|
353
|
-
const slash = modelId.indexOf("/");
|
|
354
602
|
if (slash > 0) {
|
|
355
603
|
const provName = modelId.slice(0, slash);
|
|
356
604
|
if (provName === LEGACY_CHATGPT_PROVIDER_ID || provName === LEGACY_OPENAI_MULTI_PROVIDER_ID) {
|
|
@@ -363,23 +611,33 @@ function routeModelInternal(config: OcxConfig, modelId: string, bypassCombos: bo
|
|
|
363
611
|
// Self-namespaced native id — the vendor segment equals the provider id, so the FULL ref is
|
|
364
612
|
// itself a known model (e.g. orcarouter/auto). Route it whole instead of stripping to the
|
|
365
613
|
// remainder, which would send a bare `auto` the upstream cannot resolve.
|
|
366
|
-
if (known.includes(modelId))
|
|
614
|
+
if (known.includes(modelId)) {
|
|
615
|
+
return routeResult(provName, prov, modelId, "explicit-provider", "explicit-provider-namespace");
|
|
616
|
+
}
|
|
367
617
|
// Codex-facing alias ids (`provider/vendor-model`) decode back to the native
|
|
368
618
|
// slash id via an exact known-id lookup; raw full-slash selectors keep working.
|
|
369
|
-
return routeResult(
|
|
619
|
+
return routeResult(
|
|
620
|
+
provName,
|
|
621
|
+
prov,
|
|
622
|
+
decodeRoutedModelId(modelId.slice(slash + 1), known),
|
|
623
|
+
"explicit-provider",
|
|
624
|
+
"explicit-provider-namespace",
|
|
625
|
+
);
|
|
370
626
|
}
|
|
371
627
|
}
|
|
372
628
|
|
|
373
629
|
if (isBareOpenAiFamilyModel(modelId)) {
|
|
374
630
|
const provider = config.providers[OPENAI_CODEX_PROVIDER_ID];
|
|
375
|
-
if (provider && provider.disabled !== true)
|
|
631
|
+
if (provider && provider.disabled !== true) {
|
|
632
|
+
return routeResult(OPENAI_CODEX_PROVIDER_ID, provider, modelId, "native", "native-family");
|
|
633
|
+
}
|
|
376
634
|
throw new NoEnabledOpenAiProviderError(modelId);
|
|
377
635
|
}
|
|
378
636
|
|
|
379
637
|
for (const [provName, prov] of activeProviderEntries(config)) {
|
|
380
638
|
if (prov.defaultModel === modelId
|
|
381
639
|
|| (typeof prov.defaultModel === "string" && encodeRoutedModelId(prov.defaultModel) === modelId)) {
|
|
382
|
-
return routeResult(provName, prov, prov.defaultModel as string);
|
|
640
|
+
return routeResult(provName, prov, prov.defaultModel as string, "explicit-provider", "configured-default-model");
|
|
383
641
|
}
|
|
384
642
|
}
|
|
385
643
|
|
|
@@ -389,7 +647,9 @@ function routeModelInternal(config: OcxConfig, modelId: string, bypassCombos: bo
|
|
|
389
647
|
for (const [provName, prov] of activeProviderEntries(config)) {
|
|
390
648
|
if (prov.models && Array.isArray(prov.models)) {
|
|
391
649
|
const hit = (prov.models as string[]).find(id => id === modelId || encodeRoutedModelId(id) === modelId);
|
|
392
|
-
if (hit !== undefined)
|
|
650
|
+
if (hit !== undefined) {
|
|
651
|
+
return routeResult(provName, prov, hit, "explicit-provider", "configured-model-list");
|
|
652
|
+
}
|
|
393
653
|
}
|
|
394
654
|
}
|
|
395
655
|
|
|
@@ -399,14 +659,40 @@ function routeModelInternal(config: OcxConfig, modelId: string, bypassCombos: bo
|
|
|
399
659
|
if (hasOwnProvider(config.providers, config.defaultProvider)) {
|
|
400
660
|
const defaultProv = config.providers[config.defaultProvider];
|
|
401
661
|
if (defaultProv.disabled === true) throw new Error(`Default provider is disabled: ${config.defaultProvider}`);
|
|
402
|
-
return routeResult(config.defaultProvider, defaultProv, modelId);
|
|
662
|
+
return routeResult(config.defaultProvider, defaultProv, modelId, "default-provider", "default-provider");
|
|
403
663
|
}
|
|
404
664
|
|
|
405
665
|
throw new Error(`No provider configured for model: ${modelId}`);
|
|
406
666
|
}
|
|
407
667
|
|
|
408
|
-
export function routeModel(
|
|
409
|
-
|
|
668
|
+
export function routeModel(
|
|
669
|
+
config: OcxConfig,
|
|
670
|
+
modelId: string,
|
|
671
|
+
policyEvidence?: PolicyRequestEvidence,
|
|
672
|
+
): RouteResult {
|
|
673
|
+
const route = routeModelInternal(config, modelId, false, policyEvidence);
|
|
674
|
+
// Policy routes carry a full evaluation trace already; never rebuild it.
|
|
675
|
+
if (route.routeDecision) return route;
|
|
676
|
+
const accountRef = route.codexAccountNamespace;
|
|
677
|
+
const combo = route.combo ? getCombo(config, route.combo.comboId) : undefined;
|
|
678
|
+
route.routeDecision = buildRouteDecisionTrace({
|
|
679
|
+
requestedModel: modelId,
|
|
680
|
+
routeKind: route.routeKind,
|
|
681
|
+
selected: {
|
|
682
|
+
provider: route.providerName,
|
|
683
|
+
model: route.modelId,
|
|
684
|
+
...(accountRef ? { accountRef } : {}),
|
|
685
|
+
reason: route.routeReason,
|
|
686
|
+
...(route.combo ? { candidateIndex: route.combo.targetIndex } : {}),
|
|
687
|
+
...(combo
|
|
688
|
+
? { tieBreak: combo.strategy === "round-robin" ? "round-robin" : "failover" }
|
|
689
|
+
: {}),
|
|
690
|
+
},
|
|
691
|
+
candidates: route.routeKind === "combo" && route.combo && combo
|
|
692
|
+
? comboRouteCandidates(config, route.combo, combo)
|
|
693
|
+
: undefined,
|
|
694
|
+
});
|
|
695
|
+
return route;
|
|
410
696
|
}
|
|
411
697
|
|
|
412
698
|
function routeByKnownModelPattern(config: OcxConfig, modelId: string): RouteResult | undefined {
|
|
@@ -417,7 +703,7 @@ function routeByKnownModelPattern(config: OcxConfig, modelId: string): RouteResu
|
|
|
417
703
|
);
|
|
418
704
|
if (matchingProvider) {
|
|
419
705
|
const [provName, prov] = matchingProvider;
|
|
420
|
-
return routeResult(provName, prov, modelId);
|
|
706
|
+
return routeResult(provName, prov, modelId, "explicit-provider", "model-pattern");
|
|
421
707
|
}
|
|
422
708
|
}
|
|
423
709
|
}
|