@bitkyc08/opencodex 2.11.0 → 2.11.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/gui/dist/assets/{index-Bk-PN-70.css → index-BA1dgo4q.css} +1 -1
- package/gui/dist/assets/index-DDZpgzKk.js +70 -0
- package/gui/dist/index.html +2 -2
- package/package.json +6 -4
- package/src/adapters/anthropic.ts +10 -2
- package/src/adapters/cursor/native-exec-common.ts +6 -2
- package/src/adapters/google-antigravity-replay.ts +3 -2
- package/src/adapters/google-antigravity-wire.ts +38 -6
- package/src/adapters/google.ts +57 -10
- package/src/adapters/openai-chat.ts +19 -2
- package/src/claude/model-info.ts +1 -1
- package/src/cli/account-auth.ts +3 -1
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +15 -1
- package/src/cli/codex-shim-readiness.ts +69 -0
- package/src/cli/combo.ts +5 -0
- package/src/cli/config-command.ts +33 -4
- package/src/cli/doctor.ts +21 -0
- package/src/cli/export-command.ts +6 -6
- package/src/cli/help.ts +6 -6
- package/src/cli/index.ts +22 -5
- package/src/cli/models-runtime.ts +13 -1
- package/src/cli/provider.ts +7 -0
- package/src/clients/config-export.ts +67 -5
- package/src/codex/account-lifecycle.ts +99 -10
- package/src/codex/auth-api.ts +222 -34
- package/src/codex/catalog/account-models.ts +9 -4
- package/src/codex/catalog/aggregation.ts +41 -8
- package/src/codex/catalog/bundled.ts +54 -22
- package/src/codex/catalog/effort.ts +47 -20
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +70 -18
- package/src/codex/catalog/native-models.ts +7 -0
- package/src/codex/catalog/parsing.ts +29 -11
- package/src/codex/catalog/provider-fetch.ts +335 -50
- package/src/codex/catalog/sync.ts +588 -126
- package/src/codex/catalog-refresh-status.ts +87 -0
- package/src/codex/catalog-write-serialization.ts +2 -1
- package/src/codex/catalog.ts +4 -3
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/convergence.ts +190 -52
- package/src/codex/custom-model-catalog-migration.ts +176 -0
- package/src/codex/features.ts +79 -1
- package/src/codex/history-job.ts +81 -1
- package/src/codex/history-lock.ts +2 -1
- package/src/codex/history-provider.ts +4 -3
- package/src/codex/inject.ts +56 -12
- package/src/codex/model-cache.ts +50 -10
- package/src/codex/transition-state.ts +10 -2
- package/src/codex/user-identity.ts +110 -2
- package/src/combos/index.ts +3 -0
- package/src/combos/types.ts +75 -9
- package/src/config.ts +26 -12
- package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +12 -10
- package/src/integrations/registry.ts +7 -0
- package/src/integrations/writer.ts +1 -1
- package/src/lib/pinned-http.ts +40 -9
- package/src/lib/process-control.ts +4 -1
- package/src/lib/provider-outbound.ts +42 -9
- package/src/oauth/cursor.ts +25 -5
- package/src/oauth/index.ts +59 -45
- package/src/providers/antigravity-models.ts +115 -3
- package/src/providers/context-cap.ts +13 -5
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +14 -15
- package/src/providers/provider-id-rewrite.ts +29 -0
- package/src/providers/quota.ts +93 -19
- package/src/providers/registry.ts +16 -5
- package/src/providers/slug-codec.ts +13 -6
- package/src/reasoning-effort.ts +22 -0
- package/src/router.ts +0 -20
- package/src/routing/history/indexer.ts +54 -39
- package/src/routing/quota.ts +77 -56
- package/src/server/index.ts +32 -7
- package/src/server/management/combo-routes.ts +43 -19
- package/src/server/management/config-routes.ts +115 -20
- package/src/server/management/model-routes.ts +10 -7
- package/src/server/management/model-rows.ts +6 -2
- package/src/server/management/oauth-account-routes.ts +12 -0
- package/src/server/management/provider-routes.ts +114 -40
- package/src/server/management/routing-profile-routes.ts +0 -22
- package/src/server/management-api.ts +7 -25
- package/src/server/responses/core.ts +3 -3
- package/src/server/responses/policy-fallback.ts +152 -0
- package/src/server/responses.ts +3 -2
- package/src/types.ts +15 -2
- package/src/usage/cost.ts +0 -0
- package/src/vision/describe.ts +3 -1
- package/src/vision/index.ts +17 -6
- package/src/vision/reasoning.ts +55 -0
- package/src/web-search/parse.ts +15 -3
- package/gui/dist/assets/index-BynIEIV-.js +0 -70
|
@@ -25,10 +25,11 @@ import {
|
|
|
25
25
|
import { removeCredential } from "../../oauth/store";
|
|
26
26
|
import { providerDestinationResolvedError } from "../../lib/destination-policy";
|
|
27
27
|
import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
|
|
28
|
-
import { ProviderOutboundPolicyError, providerOutboundGet, providerRedirectError } from "../../lib/provider-outbound";
|
|
28
|
+
import { ProviderOutboundPolicyError, providerOutboundGet, providerOutboundPost, providerRedirectError } from "../../lib/provider-outbound";
|
|
29
|
+
import { parseAntigravityAvailableModels } from "../../providers/antigravity-models";
|
|
29
30
|
import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/key-providers";
|
|
30
31
|
import { deriveProviderPresets } from "../../providers/derive";
|
|
31
|
-
import { providerCodexAccountMode, providerMatchesRegistryTransport } from "../../providers/registry";
|
|
32
|
+
import { effectiveGoogleMode, providerCodexAccountMode, providerMatchesRegistryTransport } from "../../providers/registry";
|
|
32
33
|
import {
|
|
33
34
|
extractModelEnvelopeRows,
|
|
34
35
|
extractProviderModelItems,
|
|
@@ -506,19 +507,33 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
506
507
|
// credential resolution/network access for providers such as Antigravity.
|
|
507
508
|
return jsonResponse({ applicable: false, reason: "static_catalog", latencyMs: 0 });
|
|
508
509
|
}
|
|
509
|
-
const {
|
|
510
|
-
const
|
|
510
|
+
const { buildModelsRequest, getValidAccessTokenSnapshot, resolveModelsAuthToken } = await import("../../oauth");
|
|
511
|
+
const antigravity = effectiveGoogleMode(name, prov) === "cloud-code-assist";
|
|
512
|
+
const snapshot = antigravity
|
|
513
|
+
? await getValidAccessTokenSnapshot(name).catch(() => undefined)
|
|
514
|
+
: undefined;
|
|
515
|
+
const apiKey = snapshot?.accessToken ?? await resolveModelsAuthToken(name, prov);
|
|
511
516
|
if (prov.authMode === "oauth" && !apiKey) {
|
|
512
517
|
return jsonResponse({ ok: false, latencyMs: 0, error: "static catalog only — upstream not verified (not logged in)" });
|
|
513
518
|
}
|
|
514
|
-
const
|
|
519
|
+
const project = prov.project ?? snapshot?.projectId;
|
|
520
|
+
if (antigravity && !project) {
|
|
521
|
+
return jsonResponse({ ok: false, latencyMs: 0, error: "Antigravity project unavailable — re-run `ocx login google-antigravity`" });
|
|
522
|
+
}
|
|
523
|
+
const { method, url: modelsUrl, headers } = buildModelsRequest(prov, apiKey, name);
|
|
515
524
|
const discovery = resolveProviderModelDiscovery(name, prov);
|
|
516
525
|
const started = Date.now();
|
|
517
526
|
try {
|
|
518
|
-
const res =
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
527
|
+
const res = method === "POST"
|
|
528
|
+
? await providerOutboundPost(name, prov, modelsUrl, {
|
|
529
|
+
headers,
|
|
530
|
+
body: JSON.stringify({ project }),
|
|
531
|
+
signal: AbortSignal.timeout(8000),
|
|
532
|
+
})
|
|
533
|
+
: await providerOutboundGet(name, prov, modelsUrl, {
|
|
534
|
+
headers,
|
|
535
|
+
signal: AbortSignal.timeout(8000),
|
|
536
|
+
});
|
|
522
537
|
const latencyMs = Date.now() - started;
|
|
523
538
|
const redirectError = await providerRedirectError(res, modelsUrl);
|
|
524
539
|
if (redirectError) {
|
|
@@ -534,7 +549,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
534
549
|
} catch {
|
|
535
550
|
// Best-effort release for non-conforming response streams.
|
|
536
551
|
}
|
|
537
|
-
return jsonResponse({ ok: false, latencyMs, error: `upstream
|
|
552
|
+
return jsonResponse({ ok: false, latencyMs, error: `upstream model discovery returned ${res.status}` });
|
|
538
553
|
}
|
|
539
554
|
const bounded = await readBoundedDiscoveryJson(res, discovery.maxResponseBytes);
|
|
540
555
|
if (!bounded.ok) {
|
|
@@ -542,20 +557,26 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
542
557
|
ok: false,
|
|
543
558
|
latencyMs,
|
|
544
559
|
error: bounded.reason === "response_too_large"
|
|
545
|
-
|
|
546
|
-
|
|
560
|
+
? `upstream model discovery exceeded the ${discovery.maxResponseBytes}-byte response limit`
|
|
561
|
+
: "upstream model discovery returned invalid JSON",
|
|
547
562
|
});
|
|
548
563
|
}
|
|
564
|
+
const ccaModels = antigravity ? parseAntigravityAvailableModels(bounded.value, discovery.maxModels) : undefined;
|
|
565
|
+
if (antigravity && !ccaModels) {
|
|
566
|
+
return jsonResponse({ ok: false, latencyMs, error: "upstream CCA model discovery returned an unexpected shape" });
|
|
567
|
+
}
|
|
549
568
|
// OpenAI-style lists (and Together top-level arrays) use the same validation/dedupe/filter
|
|
550
569
|
// as catalog discovery. Google's /v1beta/models uses `models[].name` and remains a
|
|
551
570
|
// connectivity-only count because it is not an authoritative catalog source.
|
|
552
571
|
const record = bounded.value !== null && typeof bounded.value === "object" && !Array.isArray(bounded.value)
|
|
553
572
|
? bounded.value as Record<string, unknown>
|
|
554
573
|
: undefined;
|
|
555
|
-
const extracted =
|
|
574
|
+
const extracted = ccaModels
|
|
575
|
+
? undefined
|
|
576
|
+
: Array.isArray(bounded.value) || Array.isArray(record?.data)
|
|
556
577
|
? extractProviderModelItems(bounded.value, discovery)
|
|
557
578
|
: extractModelEnvelopeRows(bounded.value, discovery.maxModels, ["models"]);
|
|
558
|
-
if (!extracted.ok) {
|
|
579
|
+
if (extracted && !extracted.ok) {
|
|
559
580
|
return jsonResponse({
|
|
560
581
|
ok: false,
|
|
561
582
|
latencyMs,
|
|
@@ -564,7 +585,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
564
585
|
: "upstream /models returned an unexpected shape",
|
|
565
586
|
});
|
|
566
587
|
}
|
|
567
|
-
const models = "items" in extracted ? extracted
|
|
588
|
+
const models = ccaModels?.length ?? ("items" in extracted! ? extracted!.items.length : extracted!.rows.length);
|
|
568
589
|
return jsonResponse({
|
|
569
590
|
ok: true,
|
|
570
591
|
latencyMs,
|
|
@@ -615,13 +636,20 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
615
636
|
const { saveConfigPreservingClaudeCode: save } = await import("../../config");
|
|
616
637
|
if (fallbackDefault) config.defaultProvider = fallbackDefault;
|
|
617
638
|
delete config.providers[name];
|
|
639
|
+
const { dropProviderCustomModels } = await import("../../providers/provider-id-rewrite");
|
|
640
|
+
const droppedCustomModels = dropProviderCustomModels(config, name);
|
|
618
641
|
setProviderContextCap(config, name, false);
|
|
619
642
|
save(config);
|
|
620
643
|
reconcileLiveStateStores();
|
|
621
644
|
const { clearModelCache: clearCache } = await import("../../codex/model-cache");
|
|
622
645
|
clearCache(name);
|
|
623
646
|
const catalogRefresh = await convergeCodexCatalog();
|
|
624
|
-
return jsonResponse({
|
|
647
|
+
return jsonResponse({
|
|
648
|
+
success: true,
|
|
649
|
+
...(fallbackDefault ? { defaultProvider: fallbackDefault } : {}),
|
|
650
|
+
...(droppedCustomModels > 0 ? { droppedCustomModels } : {}),
|
|
651
|
+
catalogRefresh,
|
|
652
|
+
});
|
|
625
653
|
}
|
|
626
654
|
|
|
627
655
|
if (url.pathname === "/api/provider-context-caps" && req.method === "GET") {
|
|
@@ -629,8 +657,12 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
629
657
|
}
|
|
630
658
|
|
|
631
659
|
if (url.pathname === "/api/provider-context-caps" && req.method === "PUT") {
|
|
632
|
-
let
|
|
633
|
-
try {
|
|
660
|
+
let rawBody: unknown;
|
|
661
|
+
try { rawBody = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
662
|
+
// Reject non-object payloads (e.g. `{"provider": true}` or a bare array) before any
|
|
663
|
+
// property access, with the route's consistent 400 response.
|
|
664
|
+
if (!isPlainRecord(rawBody)) return jsonResponse({ error: "provider-context-caps body must be a plain object" }, 400);
|
|
665
|
+
const body = rawBody as { provider?: unknown; enabled?: unknown; value?: unknown; setAll?: unknown };
|
|
634
666
|
const { saveConfigPreservingClaudeCode: save } = await import("../../config");
|
|
635
667
|
const { clearModelCache } = await import("../../codex/model-cache");
|
|
636
668
|
const respond = (catalogRefresh: Awaited<ReturnType<typeof convergeCodexCatalog>>) => jsonResponse({
|
|
@@ -641,21 +673,78 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
641
673
|
catalogRefresh,
|
|
642
674
|
});
|
|
643
675
|
|
|
644
|
-
//
|
|
676
|
+
// Reject malformed and mixed payloads before branch selection: `provider` and `enabled`
|
|
677
|
+
// must appear together with their expected types, and provider updates cannot be
|
|
678
|
+
// combined with `setAll` (which would otherwise be silently ignored).
|
|
679
|
+
const hasProviderFields = Object.hasOwn(body, "provider") || Object.hasOwn(body, "enabled");
|
|
680
|
+
if (hasProviderFields) {
|
|
681
|
+
if (typeof body.provider !== "string" || typeof body.enabled !== "boolean") {
|
|
682
|
+
return jsonResponse({ error: "provider and enabled are required together" }, 400);
|
|
683
|
+
}
|
|
684
|
+
if (Object.hasOwn(body, "setAll")) {
|
|
685
|
+
return jsonResponse({ error: "setAll cannot be combined with provider updates" }, 400);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Branch 1: per-provider toggle (checked first: a per-provider request may carry an
|
|
690
|
+
// explicit `value`, which must never fall through to the global-value branch). Enable
|
|
691
|
+
// writes the current global default unless an explicit per-provider value is supplied;
|
|
692
|
+
// that value is never copied to other providers.
|
|
693
|
+
if (typeof body.provider === "string" && typeof body.enabled === "boolean") {
|
|
694
|
+
const provider = body.provider.trim();
|
|
695
|
+
if (!isValidProviderName(provider)) {
|
|
696
|
+
return jsonResponse({ error: "provider name must use letters, numbers, dot, underscore, or hyphen and cannot be a reserved object key" }, 400);
|
|
697
|
+
}
|
|
698
|
+
if (!hasOwnProvider(config.providers, provider)) {
|
|
699
|
+
return jsonResponse({ error: "unknown provider" }, 404);
|
|
700
|
+
}
|
|
701
|
+
// Validate a supplied per-provider value before mutating anything: it must be a
|
|
702
|
+
// finite number, and after flooring it must still be >= 1 — a value like 0.5 would
|
|
703
|
+
// otherwise floor to 0 and silently fall back to the global default.
|
|
704
|
+
if (body.value !== undefined && (typeof body.value !== "number" || !Number.isFinite(body.value))) {
|
|
705
|
+
return jsonResponse({ error: "value must be a positive number" }, 400);
|
|
706
|
+
}
|
|
707
|
+
const perProviderValue = typeof body.value === "number" ? Math.floor(body.value) : undefined;
|
|
708
|
+
if (perProviderValue !== undefined && perProviderValue < 1) {
|
|
709
|
+
return jsonResponse({ error: "value must be a positive number" }, 400);
|
|
710
|
+
}
|
|
711
|
+
setProviderContextCap(config, provider, body.enabled, perProviderValue);
|
|
712
|
+
save(config);
|
|
713
|
+
reconcileLiveStateStores();
|
|
714
|
+
clearModelCache(provider);
|
|
715
|
+
const catalogRefresh = await convergeCodexCatalog();
|
|
716
|
+
return respond(catalogRefresh);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Branch 2: set the global cap value. When `setAll` accompanies it (dashboard "apply to
|
|
720
|
+
// every routed provider" toggle), re-point every enabled provider; otherwise keep each
|
|
721
|
+
// provider's own cap value and only change the default for future toggles.
|
|
645
722
|
if (body.value !== undefined) {
|
|
646
|
-
|
|
723
|
+
// Same normalization as the per-provider branch: floor before validating so a value
|
|
724
|
+
// that floors to zero is rejected rather than silently stored.
|
|
725
|
+
if (typeof body.value !== "number" || !Number.isFinite(body.value)) {
|
|
647
726
|
return jsonResponse({ error: "value must be a positive number" }, 400);
|
|
648
727
|
}
|
|
728
|
+
const normalizedValue = Math.floor(body.value);
|
|
729
|
+
if (normalizedValue < 1) {
|
|
730
|
+
return jsonResponse({ error: "value must be a positive number" }, 400);
|
|
731
|
+
}
|
|
732
|
+
if (body.setAll !== undefined && typeof body.setAll !== "boolean") {
|
|
733
|
+
return jsonResponse({ error: "setAll must be a boolean" }, 400);
|
|
734
|
+
}
|
|
649
735
|
const affected = Object.keys(providerContextCaps(config));
|
|
650
|
-
|
|
736
|
+
const applyToAll = body.setAll === true;
|
|
737
|
+
setGlobalContextCapValue(config, normalizedValue, applyToAll);
|
|
651
738
|
save(config);
|
|
652
739
|
reconcileLiveStateStores();
|
|
653
|
-
|
|
740
|
+
if (applyToAll) {
|
|
741
|
+
for (const provider of affected) clearModelCache(provider);
|
|
742
|
+
}
|
|
654
743
|
const catalogRefresh = await convergeCodexCatalog();
|
|
655
744
|
return respond(catalogRefresh);
|
|
656
745
|
}
|
|
657
746
|
|
|
658
|
-
// Branch
|
|
747
|
+
// Branch 3: enable/clear the cap for every provider at once.
|
|
659
748
|
if (body.setAll !== undefined) {
|
|
660
749
|
if (typeof body.setAll !== "boolean") {
|
|
661
750
|
return jsonResponse({ error: "setAll must be a boolean" }, 400);
|
|
@@ -670,23 +759,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
670
759
|
return respond(catalogRefresh);
|
|
671
760
|
}
|
|
672
761
|
|
|
673
|
-
//
|
|
674
|
-
|
|
675
|
-
return jsonResponse({ error: "provider string and enabled boolean are required" }, 400);
|
|
676
|
-
}
|
|
677
|
-
const provider = body.provider.trim();
|
|
678
|
-
if (!isValidProviderName(provider)) {
|
|
679
|
-
return jsonResponse({ error: "provider name must use letters, numbers, dot, underscore, or hyphen and cannot be a reserved object key" }, 400);
|
|
680
|
-
}
|
|
681
|
-
if (!hasOwnProvider(config.providers, provider)) {
|
|
682
|
-
return jsonResponse({ error: "unknown provider" }, 404);
|
|
683
|
-
}
|
|
684
|
-
setProviderContextCap(config, provider, body.enabled);
|
|
685
|
-
save(config);
|
|
686
|
-
reconcileLiveStateStores();
|
|
687
|
-
clearModelCache(provider);
|
|
688
|
-
const catalogRefresh = await convergeCodexCatalog();
|
|
689
|
-
return respond(catalogRefresh);
|
|
762
|
+
// Unrecognized payload: reject rather than silently succeeding.
|
|
763
|
+
return jsonResponse({ error: "provider string and enabled boolean are required" }, 400);
|
|
690
764
|
}
|
|
691
765
|
|
|
692
766
|
// Complete GUI picker presets, derived from the canonical provider registry. The GUI is a
|
|
@@ -20,10 +20,6 @@ import { candidateCapabilityEvidence } from "../../routing/capability";
|
|
|
20
20
|
import { policyCandidateHealthEvidence } from "../../routing/health";
|
|
21
21
|
import { quotaEvidenceForCandidate } from "../../routing/quota";
|
|
22
22
|
import { costEvidenceForCandidate } from "../../routing/cost";
|
|
23
|
-
import { providerCodexAccountMode } from "../../providers/registry";
|
|
24
|
-
import { getEffectiveActiveCodexAccountId } from "../../codex/routing";
|
|
25
|
-
import { getAccountSet } from "../../oauth/store";
|
|
26
|
-
import { OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
|
|
27
23
|
import { saveConfigPreservingClaudeCode } from "../../config";
|
|
28
24
|
import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
|
|
29
25
|
import { isPlainRecord } from "./shared";
|
|
@@ -115,24 +111,6 @@ function assembleCandidateEvidence(
|
|
|
115
111
|
quota: quotaEvidenceForCandidate({
|
|
116
112
|
provider: candidate.provider,
|
|
117
113
|
model: candidate.model,
|
|
118
|
-
...(candidate.provider === OPENAI_CODEX_PROVIDER_ID
|
|
119
|
-
&& providerCodexAccountMode(
|
|
120
|
-
OPENAI_CODEX_PROVIDER_ID,
|
|
121
|
-
config.providers[OPENAI_CODEX_PROVIDER_ID],
|
|
122
|
-
) === "pool"
|
|
123
|
-
? (() => {
|
|
124
|
-
const codexAccountId = getEffectiveActiveCodexAccountId(config);
|
|
125
|
-
return {
|
|
126
|
-
codexAccountId,
|
|
127
|
-
codexAccountPlan: codexAccountId
|
|
128
|
-
? config.codexAccounts?.find(account => account.id === codexAccountId)?.plan
|
|
129
|
-
: undefined,
|
|
130
|
-
};
|
|
131
|
-
})()
|
|
132
|
-
: {}),
|
|
133
|
-
accountRef: candidate.provider === "anthropic"
|
|
134
|
-
? getAccountSet("anthropic")?.activeAccountId
|
|
135
|
-
: undefined,
|
|
136
114
|
}),
|
|
137
115
|
cost: costEvidenceForCandidate({
|
|
138
116
|
provider: candidate.provider,
|
|
@@ -77,6 +77,7 @@ export type { ManagementApiDeps } from "./management/context";
|
|
|
77
77
|
import { fetchAllModels } from "./management/shared";
|
|
78
78
|
import { CatalogGatherBusyError } from "../codex/catalog/provider-fetch";
|
|
79
79
|
import type { CatalogDisposition, ConvergeCodex } from "../codex/convergence-types";
|
|
80
|
+
import { normalizeCatalogDisposition } from "../codex/catalog-refresh-status";
|
|
80
81
|
import { managementBodyTooLargeResponse } from "./management/body";
|
|
81
82
|
|
|
82
83
|
// installed npm version instead of a stale hardcode.
|
|
@@ -88,28 +89,6 @@ export const VERSION = (() => {
|
|
|
88
89
|
}
|
|
89
90
|
})();
|
|
90
91
|
|
|
91
|
-
function isCatalogDisposition(value: unknown): value is CatalogDisposition {
|
|
92
|
-
if (!value || typeof value !== "object" || !("status" in value)) return false;
|
|
93
|
-
const disposition = value as Record<string, unknown>;
|
|
94
|
-
if (disposition.status === "committed") {
|
|
95
|
-
return typeof disposition.changed === "boolean"
|
|
96
|
-
&& typeof disposition.degraded === "boolean"
|
|
97
|
-
&& Array.isArray(disposition.notices)
|
|
98
|
-
&& disposition.notices.every(notice => notice === "provider-auth" || notice === "provider-network" || notice === "fallback");
|
|
99
|
-
}
|
|
100
|
-
if (disposition.status === "skipped") {
|
|
101
|
-
return ["not-requested", "catalog-unavailable", "busy", "stale", "refused"].includes(String(disposition.reason))
|
|
102
|
-
&& typeof disposition.retryable === "boolean";
|
|
103
|
-
}
|
|
104
|
-
if (disposition.status === "failed") {
|
|
105
|
-
return ["provider-auth", "provider-network", "disk"].includes(String(disposition.reason))
|
|
106
|
-
&& (disposition.phase === "gather" || disposition.phase === "commit")
|
|
107
|
-
&& typeof disposition.retryable === "boolean"
|
|
108
|
-
&& typeof disposition.partialWrite === "boolean";
|
|
109
|
-
}
|
|
110
|
-
return false;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
92
|
const managementConvergenceBindings = new WeakMap<object, Readonly<{
|
|
114
93
|
factory: (config: Readonly<OcxConfig>) => ConvergeCodex;
|
|
115
94
|
converge: ConvergeCodex;
|
|
@@ -153,10 +132,13 @@ export async function handleManagementAPI(
|
|
|
153
132
|
const { createCatalogConvergeRequest } = await import("../codex/catalog-admission");
|
|
154
133
|
convergenceInvoked = true;
|
|
155
134
|
const outcome = await managementConvergeCodex(createCatalogConvergeRequest({ deadlineMs: 1_000 }));
|
|
156
|
-
|
|
135
|
+
const catalogRefresh = outcome?.kind === "catalog-only"
|
|
136
|
+
? normalizeCatalogDisposition(outcome.catalogRefresh)
|
|
137
|
+
: null;
|
|
138
|
+
if (!catalogRefresh) {
|
|
157
139
|
throw new TypeError("Catalog convergence returned an invalid outcome.");
|
|
158
140
|
}
|
|
159
|
-
return
|
|
141
|
+
return catalogRefresh;
|
|
160
142
|
} catch {
|
|
161
143
|
return {
|
|
162
144
|
status: "failed",
|
|
@@ -263,7 +245,7 @@ export async function handleManagementAPI(
|
|
|
263
245
|
const { ConfigMutationLockError } = await import("../config");
|
|
264
246
|
const { CodexCredentialRefreshLockTimeoutError } = await import("../codex/account-store");
|
|
265
247
|
try {
|
|
266
|
-
return await handleCodexAuthAPI(req, url, config);
|
|
248
|
+
return await handleCodexAuthAPI(req, url, config, convergeCodexCatalog);
|
|
267
249
|
} catch (error) {
|
|
268
250
|
// Credential writers remap ConfigMutationLockError to CodexCredentialRefreshLockTimeoutError;
|
|
269
251
|
// treat both as the same retryable busy response.
|
|
@@ -40,7 +40,6 @@ import type { AdapterEvent, OcxConfig, OcxParsedRequest, OcxProviderConfig, OcxP
|
|
|
40
40
|
import {
|
|
41
41
|
forceRefreshOAuthAccessSnapshot,
|
|
42
42
|
getOAuthCredentialApiBaseUrl,
|
|
43
|
-
getOAuthCredentialProjectId,
|
|
44
43
|
getValidAccessTokenForAccount,
|
|
45
44
|
getValidAccessTokenSnapshot,
|
|
46
45
|
type OAuthAccessSnapshot,
|
|
@@ -1677,9 +1676,10 @@ async function handleResponsesInner(
|
|
|
1677
1676
|
parsed._kiroAuthContext = { ...(resolved.kiro ?? {}) };
|
|
1678
1677
|
}
|
|
1679
1678
|
// Antigravity (cloud-code-assist) needs the discovered Cloud Code Assist project id in the
|
|
1680
|
-
// CCA envelope
|
|
1679
|
+
// CCA envelope. Keep it paired with the token snapshot so an account rotation cannot mix
|
|
1680
|
+
// a fresh token with project metadata re-read from a different credential generation.
|
|
1681
1681
|
if (route.provider.googleMode === "cloud-code-assist" && !route.provider.project) {
|
|
1682
|
-
const projectId =
|
|
1682
|
+
const projectId = resolved.projectId;
|
|
1683
1683
|
if (projectId) route.provider = { ...route.provider, project: projectId };
|
|
1684
1684
|
}
|
|
1685
1685
|
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { comboFailureDecision } from "../../combos/failover";
|
|
2
|
+
import { readBoundedResponseBody } from "../../lib/bounded-body";
|
|
3
|
+
import { readJsonRequestBody } from "../request-decompress";
|
|
4
|
+
import { finishRequestAttempt, type RequestLogContext } from "../request-log";
|
|
5
|
+
import type { OcxConfig } from "../../types";
|
|
6
|
+
import type { RouteCandidateTrace, RouteDecisionTraceV1 } from "../../routing/trace";
|
|
7
|
+
import { handleResponses as handleResponsesCore } from "./core";
|
|
8
|
+
|
|
9
|
+
type CoreHandler = typeof handleResponsesCore;
|
|
10
|
+
type CoreOptions = Parameters<CoreHandler>[3];
|
|
11
|
+
|
|
12
|
+
export interface PolicyFallbackDeps {
|
|
13
|
+
runCore?: CoreHandler;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function candidateKey(candidate: Pick<RouteCandidateTrace, "provider" | "model">): string {
|
|
17
|
+
return `${candidate.provider}\u0000${candidate.model}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Rank the remaining candidates from the ORIGINAL policy trace. The initial
|
|
22
|
+
* decision stays immutable; fallback execution belongs in attempts[], not in a
|
|
23
|
+
* rewritten decision trace.
|
|
24
|
+
*/
|
|
25
|
+
export function rankPolicyFallbackCandidates(
|
|
26
|
+
trace: RouteDecisionTraceV1,
|
|
27
|
+
tried: ReadonlySet<string>,
|
|
28
|
+
): RouteCandidateTrace[] {
|
|
29
|
+
return trace.candidates
|
|
30
|
+
.map((candidate, index) => ({ candidate, index }))
|
|
31
|
+
.filter(({ candidate }) =>
|
|
32
|
+
candidate.eligible
|
|
33
|
+
&& candidate.exclusions.length === 0
|
|
34
|
+
&& !tried.has(candidateKey(candidate)))
|
|
35
|
+
.sort((left, right) => {
|
|
36
|
+
const scoreDelta = (right.candidate.score?.total ?? Number.NEGATIVE_INFINITY)
|
|
37
|
+
- (left.candidate.score?.total ?? Number.NEGATIVE_INFINITY);
|
|
38
|
+
return scoreDelta || left.index - right.index;
|
|
39
|
+
})
|
|
40
|
+
.map(({ candidate }) => candidate);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function requestWithCandidate(
|
|
44
|
+
req: Request,
|
|
45
|
+
rawBody: Record<string, unknown>,
|
|
46
|
+
candidate: Pick<RouteCandidateTrace, "provider" | "model">,
|
|
47
|
+
): Request {
|
|
48
|
+
const headers = new Headers(req.headers);
|
|
49
|
+
headers.delete("content-encoding");
|
|
50
|
+
headers.delete("content-length");
|
|
51
|
+
headers.set("content-type", "application/json");
|
|
52
|
+
return new Request(req.url, {
|
|
53
|
+
method: req.method,
|
|
54
|
+
headers,
|
|
55
|
+
body: JSON.stringify({ ...rawBody, model: `${candidate.provider}/${candidate.model}` }),
|
|
56
|
+
signal: req.signal,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function errorCodeFromText(text: string): string | undefined {
|
|
61
|
+
if (!text) return undefined;
|
|
62
|
+
try {
|
|
63
|
+
const payload = JSON.parse(text) as { error?: { code?: unknown; type?: unknown }; code?: unknown };
|
|
64
|
+
const candidate = payload.error?.code ?? payload.error?.type ?? payload.code;
|
|
65
|
+
return typeof candidate === "string" ? candidate : undefined;
|
|
66
|
+
} catch {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function shouldHopPolicyCandidate(response: Response, signal?: AbortSignal): Promise<boolean> {
|
|
72
|
+
if (response.status < 400 || signal?.aborted) return false;
|
|
73
|
+
try {
|
|
74
|
+
const inspected = await readBoundedResponseBody(response.clone(), { signal });
|
|
75
|
+
const text = inspected.displaySafe ? inspected.text : "";
|
|
76
|
+
return comboFailureDecision(response.status, text, { code: errorCodeFromText(text) }) === "hop";
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isPolicyDecision(trace: RouteDecisionTraceV1 | undefined): trace is RouteDecisionTraceV1 {
|
|
83
|
+
return trace?.routeKind === "policy" && !!trace.profile;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Finalize the failed physical attempt so the retry receives a fresh attempt row. */
|
|
87
|
+
function finishFailedPolicyAttempt(logCtx: RequestLogContext, status: number): void {
|
|
88
|
+
const attempt = logCtx.activeAttempt;
|
|
89
|
+
if (attempt) {
|
|
90
|
+
const startedAt = logCtx.activeAttemptStartedAt ?? Date.now();
|
|
91
|
+
finishRequestAttempt(attempt, status, Math.max(0, Date.now() - startedAt), attempt.usage ?? logCtx.usage);
|
|
92
|
+
}
|
|
93
|
+
delete logCtx.activeAttempt;
|
|
94
|
+
delete logCtx.activeAttemptStartedAt;
|
|
95
|
+
delete logCtx.usage;
|
|
96
|
+
delete logCtx.usageFromBridge;
|
|
97
|
+
delete logCtx.upstreamError;
|
|
98
|
+
delete logCtx.terminalHttpStatus;
|
|
99
|
+
delete logCtx.terminalIncompleteReason;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Run a Responses request and, only for an explicitly selected policy profile,
|
|
104
|
+
* hop to the next eligible policy candidate after a retryable pre-success
|
|
105
|
+
* failure. The initial policy trace remains the canonical selection evidence;
|
|
106
|
+
* physical retries continue to accumulate in the existing request attempts.
|
|
107
|
+
*/
|
|
108
|
+
export async function handleResponsesWithPolicyFallback(
|
|
109
|
+
req: Request,
|
|
110
|
+
config: OcxConfig,
|
|
111
|
+
logCtx: RequestLogContext,
|
|
112
|
+
options: CoreOptions = {},
|
|
113
|
+
deps: PolicyFallbackDeps = {},
|
|
114
|
+
): Promise<Response> {
|
|
115
|
+
const runCore = deps.runCore ?? handleResponsesCore;
|
|
116
|
+
let rawBody: Record<string, unknown> | null = null;
|
|
117
|
+
try {
|
|
118
|
+
const parsed = await readJsonRequestBody(req.clone());
|
|
119
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) rawBody = parsed as Record<string, unknown>;
|
|
120
|
+
} catch {
|
|
121
|
+
// Core owns the client-facing parse/decompression error.
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let response = await runCore(req, config, logCtx, options);
|
|
125
|
+
const initialTrace = logCtx.routeDecision;
|
|
126
|
+
const initialRequestedModel = logCtx.requestedModel;
|
|
127
|
+
if (!rawBody || !isPolicyDecision(initialTrace)) return response;
|
|
128
|
+
|
|
129
|
+
const tried = new Set<string>([
|
|
130
|
+
candidateKey({ provider: initialTrace.selected.provider, model: initialTrace.selected.model }),
|
|
131
|
+
]);
|
|
132
|
+
|
|
133
|
+
while (await shouldHopPolicyCandidate(response, req.signal)) {
|
|
134
|
+
if (req.signal.aborted) return response;
|
|
135
|
+
const next = rankPolicyFallbackCandidates(initialTrace, tried)[0];
|
|
136
|
+
if (!next) return response;
|
|
137
|
+
tried.add(candidateKey(next));
|
|
138
|
+
|
|
139
|
+
finishFailedPolicyAttempt(logCtx, response.status);
|
|
140
|
+
const retryRequest = requestWithCandidate(req, rawBody, next);
|
|
141
|
+
try {
|
|
142
|
+
response = await runCore(retryRequest, config, logCtx, options);
|
|
143
|
+
} finally {
|
|
144
|
+
logCtx.requestedModel = initialRequestedModel;
|
|
145
|
+
logCtx.routeDecision = initialTrace;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return response;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export const handleResponses = handleResponsesWithPolicyFallback;
|
package/src/server/responses.ts
CHANGED
|
@@ -5,5 +5,6 @@ export type { MultiAgentGuidanceOptions, MultiAgentGuidanceDeps } from "./respon
|
|
|
5
5
|
export { hasUnreadableEncryptedAgentTask, sanitizeEncryptedContentInPlace } from "./responses/encrypted-payload";
|
|
6
6
|
export { COMPACT_RESPONSE_MAX_BYTES, bufferCompactResponse, handleResponsesCompact } from "./responses/compact";
|
|
7
7
|
export { disableResponsesRequestTimeout, safeHostLabel, fetchWithHeaderTimeout } from "./responses/fetch-helpers";
|
|
8
|
-
export { sidecarOutcomeRecorder, isShadowSourceModel, codexLogAccountId, usesCodexForwardPoolAuth, codexForwardTerminalOutcomeRecorder, decodeRequestErrorResponse, buildComboChildHeaders,
|
|
9
|
-
export {
|
|
8
|
+
export { sidecarOutcomeRecorder, isShadowSourceModel, codexLogAccountId, usesCodexForwardPoolAuth, codexForwardTerminalOutcomeRecorder, decodeRequestErrorResponse, buildComboChildHeaders, linkAbortSignal } from "./responses/core";
|
|
9
|
+
export { handleResponses, handleResponsesWithPolicyFallback, rankPolicyFallbackCandidates } from "./responses/policy-fallback";
|
|
10
|
+
export { adapterNeedsForcedContinuation } from "./responses/core";
|
package/src/types.ts
CHANGED
|
@@ -583,8 +583,8 @@ export interface OcxConfig {
|
|
|
583
583
|
defaultProvider: string;
|
|
584
584
|
/** OpenAI provider-contract migration marker (v2 = single `openai` provider with account mode). */
|
|
585
585
|
openaiProviderTierVersion?: 1 | 2;
|
|
586
|
-
/** One-time migration marker for Antigravity's static
|
|
587
|
-
googleAntigravityStaticCatalogVersion?: 1;
|
|
586
|
+
/** One-time migration marker for Antigravity's static-catalog defaults. */
|
|
587
|
+
googleAntigravityStaticCatalogVersion?: 1 | 2;
|
|
588
588
|
/** Claude Code inbound + launcher settings. */
|
|
589
589
|
claudeCode?: OcxClaudeCodeConfig;
|
|
590
590
|
/**
|
|
@@ -700,6 +700,12 @@ export interface OcxConfig {
|
|
|
700
700
|
disabledModels?: string[];
|
|
701
701
|
/** 사용자가 대시보드에서 직접 추가한 커스텀 모델 목록. */
|
|
702
702
|
customModels?: OcxCustomModel[];
|
|
703
|
+
/**
|
|
704
|
+
* Internal, versioned evidence for reconciling custom-model deletions with
|
|
705
|
+
* pre-marker Codex catalog rows. Consumers must parse this defensively so a
|
|
706
|
+
* future state written by a newer binary survives older whole-config saves.
|
|
707
|
+
*/
|
|
708
|
+
customModelCatalogMigration?: unknown;
|
|
703
709
|
/**
|
|
704
710
|
* Shadow call intercept: redirect Codex's hard-coded helper calls (title generation,
|
|
705
711
|
* commit messages, skill orchestration) to a user-chosen model. Default intercepted
|
|
@@ -899,6 +905,13 @@ export interface OcxComboConfig {
|
|
|
899
905
|
* mandated model id; exact-match requests route here before any provider resolution.
|
|
900
906
|
*/
|
|
901
907
|
alias?: string;
|
|
908
|
+
/**
|
|
909
|
+
* Explicitly allow a bare OpenAI-native alias (for example `gpt-5.6-sol`) to
|
|
910
|
+
* be represented by this routed combo. Never inferred from `alias`.
|
|
911
|
+
*/
|
|
912
|
+
nativeAlias?: boolean;
|
|
913
|
+
/** Display-only label for the public catalog row. Required for native aliases. */
|
|
914
|
+
displayName?: string;
|
|
902
915
|
}
|
|
903
916
|
|
|
904
917
|
export type OcxRoutingUnknownEvidenceMode = "allow" | "penalize" | "exclude";
|
package/src/usage/cost.ts
CHANGED
|
Binary file
|
package/src/vision/describe.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OcxProviderConfig } from "../types";
|
|
2
|
+
import type { VisionReasoningEffort } from "../reasoning-effort";
|
|
2
3
|
import { FORWARD_HEADERS } from "../adapters/openai-responses";
|
|
3
4
|
import { signalWithTimeout, cancelBodyOnAbort } from "../lib/abort";
|
|
4
5
|
import { redactSecretString } from "../lib/redact";
|
|
@@ -9,6 +10,7 @@ import type { SidecarOutcomeRecorder } from "../web-search/executor";
|
|
|
9
10
|
|
|
10
11
|
export interface VisionSettings {
|
|
11
12
|
model: string;
|
|
13
|
+
reasoning: VisionReasoningEffort;
|
|
12
14
|
timeoutMs: number;
|
|
13
15
|
}
|
|
14
16
|
|
|
@@ -77,7 +79,7 @@ export async function describeImage(
|
|
|
77
79
|
"verbatim, and note UI/layout, colors, branding/logos, charts, and notable details. Focus on " +
|
|
78
80
|
"what's relevant to the user's request. Output only the description.",
|
|
79
81
|
input: [{ type: "message", role: "user", content }],
|
|
80
|
-
reasoning: { effort:
|
|
82
|
+
reasoning: { effort: settings.reasoning },
|
|
81
83
|
// The ChatGPT (codex) backend rejects `max_output_tokens` ("Unsupported parameter"); the
|
|
82
84
|
// description is clamped downstream (DESC_MAX_CHARS) instead.
|
|
83
85
|
store: false,
|