@bitkyc08/opencodex 2.40.0 → 2.42.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/README.md +4 -0
- package/gui/dist/assets/index-BU1tE0sr.js +112 -0
- package/gui/dist/assets/index-DL9-iS6J.css +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/meta.svg +1 -0
- package/package.json +4 -3
- package/src/adapters/cursor/catalog.ts +71 -29
- package/src/adapters/cursor/claude-id.ts +76 -0
- package/src/adapters/cursor/discovery.ts +16 -3
- package/src/adapters/cursor/effort-map.ts +27 -12
- package/src/adapters/cursor/protobuf-request.ts +41 -21
- package/src/adapters/google.ts +39 -2
- package/src/adapters/identity.ts +8 -2
- package/src/adapters/openai-responses.ts +57 -4
- package/src/bridge.ts +25 -3
- package/src/cli/account-auth.ts +28 -3
- package/src/cli/account-extended.ts +7 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/claude.ts +11 -2
- package/src/cli/connect.ts +7 -1
- package/src/cli/observe.ts +3 -1
- package/src/cli/registry.ts +1 -1
- package/src/cli/status.ts +19 -4
- package/src/client/connect.ts +5 -1
- package/src/client/hub-client.ts +29 -5
- package/src/clients/config-export.ts +12 -2
- package/src/codex/auth-api.ts +102 -9
- package/src/codex/catalog/aggregation.ts +8 -0
- package/src/codex/catalog/effort.ts +15 -2
- package/src/codex/catalog/metadata.ts +119 -9
- package/src/codex/catalog/native-models.ts +71 -0
- package/src/codex/catalog/parsing.ts +5 -3
- package/src/codex/catalog/provider-fetch.ts +166 -28
- package/src/codex/catalog.ts +1 -1
- package/src/codex/convergence-types.ts +1 -0
- package/src/codex/data/upstream-models.json +169 -0
- package/src/codex/desired-state.ts +18 -11
- package/src/codex/inject.ts +96 -6
- package/src/codex/injected-marker.ts +30 -4
- package/src/codex/journal.ts +14 -0
- package/src/combos/failover.ts +185 -6
- package/src/combos/index.ts +6 -0
- package/src/combos/resolve.ts +43 -6
- package/src/config.ts +5 -1
- package/src/generated/compatibility-version.json +115 -83
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/sync.ts +10 -2
- package/src/integrations/cursor-effort-table.ts +143 -0
- package/src/integrations/state.ts +1 -1
- package/src/integrations/writer.ts +2 -2
- package/src/lib/app-owned-memory-stores.ts +27 -8
- package/src/lib/bounded-body.ts +16 -1
- package/src/oauth/account-quota-rank.ts +40 -1
- package/src/oauth/chatgpt-device.ts +187 -0
- package/src/oauth/chatgpt.ts +31 -4
- package/src/oauth/generic-account-failover.ts +2 -2
- package/src/oauth/index.ts +24 -3
- package/src/oauth/log.ts +3 -0
- package/src/oauth/meta-muse.ts +235 -0
- package/src/providers/antigravity-models.ts +71 -13
- package/src/providers/command-code-efforts.ts +15 -0
- package/src/providers/free-directory.ts +4 -1
- package/src/providers/muse-subscription-usage.ts +95 -0
- package/src/providers/quota.ts +96 -0
- package/src/providers/registry.ts +116 -8
- package/src/responses/code-mode-helper-compat.ts +4 -1
- package/src/responses/state.ts +5 -4
- package/src/server/auth-cors.ts +241 -56
- package/src/server/chat-completions.ts +11 -2
- package/src/server/chat-native.ts +30 -4
- package/src/server/claude-messages.ts +17 -3
- package/src/server/effort-row.ts +131 -0
- package/src/server/index.ts +82 -45
- package/src/server/live.ts +18 -4
- package/src/server/management/api-key-rotation.ts +2 -1
- package/src/server/management/api-key-usage.ts +97 -43
- package/src/server/management/context.ts +3 -0
- package/src/server/management/cursor-integration-routes.ts +36 -7
- package/src/server/management/logs-usage-routes.ts +64 -87
- package/src/server/management/oauth-account-routes.ts +10 -3
- package/src/server/management/provider-routes.ts +218 -1
- package/src/server/management/route-registry.ts +1 -0
- package/src/server/management/usage-aggregate-cache.ts +464 -0
- package/src/server/management/usage-summary-cache.ts +4 -0
- package/src/server/models-capabilities.ts +60 -5
- package/src/server/responses/core.ts +95 -7
- package/src/server/responses/empty-completion-guard.ts +4 -0
- package/src/types/config.ts +10 -1
- package/src/types/request.ts +8 -0
- package/src/types/tools.ts +12 -9
- package/src/usage/expected-prices.ts +43 -7
- package/src/usage/ledger-scanner.ts +448 -0
- package/src/usage/log.ts +1 -1
- package/src/usage/summary.ts +915 -655
- package/src/web-search/index.ts +1 -1
- package/gui/dist/assets/index-BHe2rl_C.js +0 -112
- package/gui/dist/assets/index-CJSb3HPe.css +0 -1
package/src/server/auth-cors.ts
CHANGED
|
@@ -749,73 +749,258 @@ export function copyIfDefined<K extends keyof OcxProviderConfig>(
|
|
|
749
749
|
if (value !== undefined) out[key as string] = value as unknown;
|
|
750
750
|
}
|
|
751
751
|
|
|
752
|
+
/**
|
|
753
|
+
* Exhaustive provider-field policy shared by dashboard redaction and editor
|
|
754
|
+
* admission. `satisfies Record<keyof OcxProviderConfig, ...>` makes a newly added
|
|
755
|
+
* provider field fail typecheck until it is deliberately classified.
|
|
756
|
+
*
|
|
757
|
+
* `editor` fields are user-authored, `redacted` fields may contain credentials,
|
|
758
|
+
* and `runtime` fields are observations/limits that must never become editor write
|
|
759
|
+
* authority. MCP and desktop executor blocks are redacted as a whole because both
|
|
760
|
+
* contain arbitrary environment variables and/or headers.
|
|
761
|
+
*/
|
|
762
|
+
type ProviderConfigFieldPolicy = "editor" | "redacted" | "runtime";
|
|
763
|
+
|
|
764
|
+
const PROVIDER_CONFIG_FIELD_POLICY = {
|
|
765
|
+
alias: "editor",
|
|
766
|
+
modelAliases: "editor",
|
|
767
|
+
modelDisplayNames: "editor",
|
|
768
|
+
defaultAliases: "editor",
|
|
769
|
+
adapter: "editor",
|
|
770
|
+
codexToolMode: "editor",
|
|
771
|
+
requestPacing: "editor",
|
|
772
|
+
mcpMaxTools: "editor",
|
|
773
|
+
mcpMaxSchemaBytes: "editor",
|
|
774
|
+
mcpMaxResultBytes: "editor",
|
|
775
|
+
modelAdapters: "editor",
|
|
776
|
+
fastWire: "editor",
|
|
777
|
+
baseUrl: "editor",
|
|
778
|
+
responsesPath: "editor",
|
|
779
|
+
commandCodeVersion: "editor",
|
|
780
|
+
statelessResponses: "editor",
|
|
781
|
+
requiresAdjacentResponsesToolResults: "editor",
|
|
782
|
+
annotateEmptyToolOutputs: "editor",
|
|
783
|
+
supportsServiceTier: "editor",
|
|
784
|
+
modelSupportsServiceTier: "editor",
|
|
785
|
+
preserveResponsesReasoningContent: "editor",
|
|
786
|
+
decodesNativeCompactionBlobs: "editor",
|
|
787
|
+
allowPrivateNetwork: "editor",
|
|
788
|
+
upstreamHttpVersion: "editor",
|
|
789
|
+
upstreamWebsocket: "editor",
|
|
790
|
+
directGeminiWireRenames: "editor",
|
|
791
|
+
disabled: "editor",
|
|
792
|
+
codexAccountMode: "editor",
|
|
793
|
+
apiKey: "redacted",
|
|
794
|
+
apiKeyTransport: "editor",
|
|
795
|
+
apiKeyPool: "redacted",
|
|
796
|
+
defaultModel: "editor",
|
|
797
|
+
models: "editor",
|
|
798
|
+
liveModels: "editor",
|
|
799
|
+
selectedModels: "editor",
|
|
800
|
+
retainModels: "editor",
|
|
801
|
+
newModelPolicy: "editor",
|
|
802
|
+
modelPreset: "editor",
|
|
803
|
+
contextWindow: "editor",
|
|
804
|
+
modelContextWindows: "editor",
|
|
805
|
+
modelInputModalities: "editor",
|
|
806
|
+
modelMaxInputTokens: "runtime",
|
|
807
|
+
modelAutoCompactTokenLimits: "editor",
|
|
808
|
+
defaultMaxOutputTokens: "editor",
|
|
809
|
+
modelMaxOutputTokens: "editor",
|
|
810
|
+
modelCosts: "editor",
|
|
811
|
+
headers: "redacted",
|
|
812
|
+
openRouterRouting: "editor",
|
|
813
|
+
modelOpenRouterRouting: "editor",
|
|
814
|
+
vercelGatewayRouting: "editor",
|
|
815
|
+
modelVercelGatewayRouting: "editor",
|
|
816
|
+
authMode: "editor",
|
|
817
|
+
oauthAccountFailover: "editor",
|
|
818
|
+
keyOptional: "editor",
|
|
819
|
+
freeTier: "editor",
|
|
820
|
+
note: "editor",
|
|
821
|
+
modelSuffixBracketStrip: "editor",
|
|
822
|
+
refreshPolicy: "editor",
|
|
823
|
+
reasoningEfforts: "editor",
|
|
824
|
+
modelReasoningEfforts: "editor",
|
|
825
|
+
modelDefaultReasoningEfforts: "editor",
|
|
826
|
+
modelSupportsReasoningSummaries: "editor",
|
|
827
|
+
modelSupportsVerbosity: "editor",
|
|
828
|
+
supportsVerbosity: "editor",
|
|
829
|
+
modelReasoningSummaryDelivery: "editor",
|
|
830
|
+
modelPreferHostedTools: "editor",
|
|
831
|
+
supportsOpenAiWebSearchToolFields: "editor",
|
|
832
|
+
xaiResponsesXSearch: "editor",
|
|
833
|
+
supportsResponsesCustomTools: "editor",
|
|
834
|
+
responsesSnapshotRepair: "editor",
|
|
835
|
+
reasoningEffortMap: "editor",
|
|
836
|
+
modelReasoningEffortMap: "editor",
|
|
837
|
+
reasoningWireFormat: "editor",
|
|
838
|
+
noReasoningModels: "editor",
|
|
839
|
+
noTemperatureModels: "editor",
|
|
840
|
+
noTopPModels: "editor",
|
|
841
|
+
noPenaltyModels: "editor",
|
|
842
|
+
noStructuredOutputModels: "editor",
|
|
843
|
+
omitReasoningEffortWithToolsModels: "editor",
|
|
844
|
+
parallelToolCalls: "editor",
|
|
845
|
+
pinParallelToolCallsFalse: "editor",
|
|
846
|
+
terminalContinuationGuard: "editor",
|
|
847
|
+
openaiChatEofTolerance: "editor",
|
|
848
|
+
promptCacheKey: "editor",
|
|
849
|
+
chatServiceTier: "editor",
|
|
850
|
+
responsesItemIdRepair: "editor",
|
|
851
|
+
autoToolChoiceOnlyModels: "editor",
|
|
852
|
+
preserveReasoningContentModels: "editor",
|
|
853
|
+
requiresReasoningPlaceholderModels: "editor",
|
|
854
|
+
retryOn429: "editor",
|
|
855
|
+
transientRetryOn5xx: "editor",
|
|
856
|
+
reasoningSplitModels: "editor",
|
|
857
|
+
reasoningDetailsModels: "editor",
|
|
858
|
+
thinkingToggleModels: "editor",
|
|
859
|
+
thinkingBudgetModels: "editor",
|
|
860
|
+
escapeBuiltinToolNames: "editor",
|
|
861
|
+
anthropicEofTolerance: "editor",
|
|
862
|
+
noVisionModels: "editor",
|
|
863
|
+
googleMode: "editor",
|
|
864
|
+
project: "editor",
|
|
865
|
+
location: "editor",
|
|
866
|
+
mcpServers: "redacted",
|
|
867
|
+
desktopExecutor: "redacted",
|
|
868
|
+
unsafeAllowNativeLocalExec: "editor",
|
|
869
|
+
nativeLocalExec: "editor",
|
|
870
|
+
} as const satisfies Record<keyof OcxProviderConfig, ProviderConfigFieldPolicy>;
|
|
871
|
+
|
|
872
|
+
type ProviderFieldWithPolicy<Policy extends ProviderConfigFieldPolicy> = {
|
|
873
|
+
[Field in keyof typeof PROVIDER_CONFIG_FIELD_POLICY]:
|
|
874
|
+
typeof PROVIDER_CONFIG_FIELD_POLICY[Field] extends Policy ? Field : never;
|
|
875
|
+
}[keyof typeof PROVIDER_CONFIG_FIELD_POLICY];
|
|
876
|
+
|
|
877
|
+
type RedactedProviderField = ProviderFieldWithPolicy<"redacted">;
|
|
878
|
+
type RuntimeProviderField = ProviderFieldWithPolicy<"runtime">;
|
|
879
|
+
export const REDACTED_PROVIDER_FIELDS = Object.freeze(Object.entries(PROVIDER_CONFIG_FIELD_POLICY)
|
|
880
|
+
.filter(([, policy]) => policy === "redacted")
|
|
881
|
+
.map(([field]) => field as RedactedProviderField));
|
|
882
|
+
const RUNTIME_PROVIDER_FIELDS = Object.freeze(Object.entries(PROVIDER_CONFIG_FIELD_POLICY)
|
|
883
|
+
.filter(([, policy]) => policy === "runtime")
|
|
884
|
+
.map(([field]) => field as RuntimeProviderField));
|
|
885
|
+
|
|
886
|
+
const PROVIDER_EDITOR_DERIVED_FIELDS = [
|
|
887
|
+
...RUNTIME_PROVIDER_FIELDS,
|
|
888
|
+
...FORBIDDEN_PROVIDER_RUNTIME_FIELDS,
|
|
889
|
+
"fetch",
|
|
890
|
+
"hasApiKey",
|
|
891
|
+
"hasHeaders",
|
|
892
|
+
"xaiResponsesOptInState",
|
|
893
|
+
] as const;
|
|
894
|
+
|
|
895
|
+
export const PROVIDER_EDITOR_DENIED_FIELDS = [
|
|
896
|
+
...REDACTED_PROVIDER_FIELDS,
|
|
897
|
+
...PROVIDER_EDITOR_DERIVED_FIELDS,
|
|
898
|
+
] as const;
|
|
899
|
+
|
|
900
|
+
export type ProviderEditorProviderDTO = Omit<OcxProviderConfig, RedactedProviderField | RuntimeProviderField>
|
|
901
|
+
& Record<string, unknown>;
|
|
902
|
+
|
|
903
|
+
export interface ProviderEditorConfigDTO {
|
|
904
|
+
defaultProvider: string;
|
|
905
|
+
providers: Record<string, ProviderEditorProviderDTO>;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
export type ProviderEditorConfigParseResult =
|
|
909
|
+
| { ok: true; value: ProviderEditorConfigDTO }
|
|
910
|
+
| { ok: false; error: string; code: "invalid_provider_editor_body" | "invalid_provider_editor_field" };
|
|
911
|
+
|
|
912
|
+
const PROVIDER_EDITOR_DENIED_FIELD_SET = new Set<string>(PROVIDER_EDITOR_DENIED_FIELDS);
|
|
913
|
+
const PROVIDER_CONFIG_FIELD_SET = new Set<string>(Object.keys(PROVIDER_CONFIG_FIELD_POLICY));
|
|
914
|
+
|
|
915
|
+
function isPlainDataRecord(value: unknown): value is Record<string, unknown> {
|
|
916
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
917
|
+
const prototype = Object.getPrototypeOf(value);
|
|
918
|
+
return prototype === Object.prototype || prototype === null;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Project one provider through the same redaction path used by both the public
|
|
923
|
+
* config DTO and the raw editor. Persisted unknown fields remain on disk but are
|
|
924
|
+
* not exposed until OcxProviderConfig classifies them as editor-safe.
|
|
925
|
+
*/
|
|
926
|
+
function providerEditorProviderDTO(name: string, provider: OcxProviderConfig): ProviderEditorProviderDTO {
|
|
927
|
+
const dto = Object.fromEntries(Object.entries(provider)
|
|
928
|
+
.filter(([field]) => PROVIDER_CONFIG_FIELD_SET.has(field) && !PROVIDER_EDITOR_DENIED_FIELD_SET.has(field))
|
|
929
|
+
.map(([field, value]) => [field, structuredClone(value)])) as Record<string, unknown>;
|
|
930
|
+
dto.baseUrl = publicProviderBaseUrl(provider.baseUrl);
|
|
931
|
+
const modelCosts = sanitizeModelCostsForDisplay(provider.modelCosts);
|
|
932
|
+
if (modelCosts) dto.modelCosts = modelCosts;
|
|
933
|
+
else delete dto.modelCosts;
|
|
934
|
+
|
|
935
|
+
const registryNote = (providerMatchesRegistryTransport(name, provider)
|
|
936
|
+
? getProviderRegistryEntry(name)
|
|
937
|
+
: registryEntryForProviderDestination(provider))?.note;
|
|
938
|
+
if (typeof registryNote === "string" && registryNote.trim()) dto.note = registryNote;
|
|
939
|
+
const codexAccountMode = providerCodexAccountMode(name, provider);
|
|
940
|
+
if (codexAccountMode) dto.codexAccountMode = codexAccountMode;
|
|
941
|
+
return dto as ProviderEditorProviderDTO;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/** The complete non-secret provider shape the raw GUI editor may round-trip. */
|
|
945
|
+
export function providerEditorConfigDTO(config: OcxConfig): ProviderEditorConfigDTO {
|
|
946
|
+
const providers: Record<string, ProviderEditorProviderDTO> = Object.create(null);
|
|
947
|
+
for (const [name, provider] of Object.entries(config.providers)) {
|
|
948
|
+
providers[name] = providerEditorProviderDTO(name, provider);
|
|
949
|
+
}
|
|
950
|
+
return { defaultProvider: config.defaultProvider, providers };
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/** Parse an editor snapshot; unknown, redacted, and derived fields fail closed. */
|
|
954
|
+
export function parseProviderEditorConfigDTO(value: unknown): ProviderEditorConfigParseResult {
|
|
955
|
+
if (!isPlainDataRecord(value)) {
|
|
956
|
+
return { ok: false, error: "provider editor config must be a plain object", code: "invalid_provider_editor_body" };
|
|
957
|
+
}
|
|
958
|
+
const rootKeys = Object.keys(value);
|
|
959
|
+
if (rootKeys.length !== 2 || !Object.hasOwn(value, "defaultProvider") || !Object.hasOwn(value, "providers")) {
|
|
960
|
+
return { ok: false, error: "provider editor config must contain only defaultProvider and providers", code: "invalid_provider_editor_body" };
|
|
961
|
+
}
|
|
962
|
+
if (typeof value.defaultProvider !== "string" || value.defaultProvider.trim() === "") {
|
|
963
|
+
return { ok: false, error: "defaultProvider must be a non-empty string", code: "invalid_provider_editor_body" };
|
|
964
|
+
}
|
|
965
|
+
if (!isPlainDataRecord(value.providers)) {
|
|
966
|
+
return { ok: false, error: "providers must be a plain object", code: "invalid_provider_editor_body" };
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const providers: Record<string, ProviderEditorProviderDTO> = Object.create(null);
|
|
970
|
+
for (const [name, provider] of Object.entries(value.providers)) {
|
|
971
|
+
if (!isPlainDataRecord(provider)) {
|
|
972
|
+
return { ok: false, error: `provider ${JSON.stringify(redactSecretString(name))} must be a plain object`, code: "invalid_provider_editor_body" };
|
|
973
|
+
}
|
|
974
|
+
const deniedField = Object.keys(provider).find(field =>
|
|
975
|
+
!PROVIDER_CONFIG_FIELD_SET.has(field) || PROVIDER_EDITOR_DENIED_FIELD_SET.has(field));
|
|
976
|
+
if (deniedField) {
|
|
977
|
+
return {
|
|
978
|
+
ok: false,
|
|
979
|
+
error: `provider ${JSON.stringify(redactSecretString(name))} contains non-editable field ${JSON.stringify(redactSecretString(deniedField))}`,
|
|
980
|
+
code: "invalid_provider_editor_field",
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
providers[name] = structuredClone(provider) as ProviderEditorProviderDTO;
|
|
984
|
+
}
|
|
985
|
+
return {
|
|
986
|
+
ok: true,
|
|
987
|
+
value: { defaultProvider: value.defaultProvider, providers },
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
|
|
752
991
|
/** Public dashboard DTO for config.json: provider entries with secrets stripped and documented fields exposed (including `modelCosts`). */
|
|
753
992
|
export function safeConfigDTO(config: OcxConfig): unknown {
|
|
993
|
+
const editor = providerEditorConfigDTO(config);
|
|
754
994
|
const providers: Record<string, Record<string, unknown>> = {};
|
|
755
995
|
for (const [name, provider] of Object.entries(config.providers)) {
|
|
756
996
|
const dto: Record<string, unknown> = {
|
|
757
|
-
|
|
758
|
-
baseUrl: publicProviderBaseUrl(provider.baseUrl),
|
|
997
|
+
...editor.providers[name],
|
|
759
998
|
hasApiKey: !!provider.apiKey,
|
|
760
999
|
hasHeaders: !!provider.headers && Object.keys(provider.headers).length > 0,
|
|
761
1000
|
};
|
|
762
1001
|
if (name === "xai") {
|
|
763
1002
|
dto.xaiResponsesOptInState = xaiResponsesOptInState(provider);
|
|
764
1003
|
}
|
|
765
|
-
for (const key of [
|
|
766
|
-
"defaultModel",
|
|
767
|
-
"alias",
|
|
768
|
-
"modelAliases",
|
|
769
|
-
"defaultAliases",
|
|
770
|
-
"disabled",
|
|
771
|
-
"allowPrivateNetwork",
|
|
772
|
-
"authMode",
|
|
773
|
-
"apiKeyTransport",
|
|
774
|
-
"keyOptional",
|
|
775
|
-
"freeTier",
|
|
776
|
-
"liveModels",
|
|
777
|
-
"requestPacing",
|
|
778
|
-
"models",
|
|
779
|
-
"contextWindow",
|
|
780
|
-
"modelContextWindows",
|
|
781
|
-
"modelAutoCompactTokenLimits",
|
|
782
|
-
"defaultMaxOutputTokens",
|
|
783
|
-
"modelMaxOutputTokens",
|
|
784
|
-
"openRouterRouting",
|
|
785
|
-
"modelOpenRouterRouting",
|
|
786
|
-
"vercelGatewayRouting",
|
|
787
|
-
"modelVercelGatewayRouting",
|
|
788
|
-
"reasoningEfforts",
|
|
789
|
-
"modelReasoningEfforts",
|
|
790
|
-
"reasoningWireFormat",
|
|
791
|
-
"noVisionModels",
|
|
792
|
-
"noReasoningModels",
|
|
793
|
-
"noTemperatureModels",
|
|
794
|
-
"noTopPModels",
|
|
795
|
-
"noPenaltyModels",
|
|
796
|
-
"noStructuredOutputModels",
|
|
797
|
-
"retainModels",
|
|
798
|
-
"omitReasoningEffortWithToolsModels",
|
|
799
|
-
"upstreamHttpVersion",
|
|
800
|
-
"autoToolChoiceOnlyModels",
|
|
801
|
-
"preserveReasoningContentModels",
|
|
802
|
-
"requiresReasoningPlaceholderModels",
|
|
803
|
-
"escapeBuiltinToolNames",
|
|
804
|
-
] as const) {
|
|
805
|
-
copyIfDefined(dto, provider, key);
|
|
806
|
-
}
|
|
807
|
-
const modelCosts = sanitizeModelCostsForDisplay(provider.modelCosts);
|
|
808
|
-
if (modelCosts) dto.modelCosts = modelCosts;
|
|
809
|
-
// Resolve the note by DESTINATION, not by name. A preset saved under a custom name is
|
|
810
|
-
// still pointed at the same vendor route, and a usage restriction the user needs to see
|
|
811
|
-
// must not disappear because the row was renamed. Prefer the same-name entry so an
|
|
812
|
-
// unrenamed provider keeps its exact registry note.
|
|
813
|
-
const registryNote = (providerMatchesRegistryTransport(name, provider)
|
|
814
|
-
? getProviderRegistryEntry(name)
|
|
815
|
-
: registryEntryForProviderDestination(provider))?.note;
|
|
816
|
-
if (typeof registryNote === "string" && registryNote.trim()) dto.note = registryNote;
|
|
817
|
-
const codexAccountMode = providerCodexAccountMode(name, provider);
|
|
818
|
-
if (codexAccountMode) dto.codexAccountMode = codexAccountMode;
|
|
819
1004
|
providers[name] = dto;
|
|
820
1005
|
}
|
|
821
1006
|
return {
|
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
type TranslatorBudget,
|
|
47
47
|
} from "../lib/translator-budget";
|
|
48
48
|
import { handleNativeChatCompletions, isNativeChatRouteEligible } from "./chat-native";
|
|
49
|
+
import { parseRequestEffortRowId } from "./effort-row";
|
|
49
50
|
|
|
50
51
|
type Rec = Record<string, unknown>;
|
|
51
52
|
|
|
@@ -104,6 +105,8 @@ async function handleChatCompletionsWithBudget(
|
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
const requestedModel = chatBody.model as string;
|
|
108
|
+
const effortRow = parseRequestEffortRowId(requestedModel, config);
|
|
109
|
+
if (effortRow) chatBody.model = effortRow.baseId;
|
|
107
110
|
const stream = chatBody.stream === true;
|
|
108
111
|
// Best-effort Grok attribution: the managed fence stamps this header on every model
|
|
109
112
|
// it registers (extra_headers, sent verbatim by upstream Grok). Dashboard usage
|
|
@@ -113,7 +116,7 @@ async function handleChatCompletionsWithBudget(
|
|
|
113
116
|
let settledRoute: ReturnType<typeof routeModel> | null = null;
|
|
114
117
|
let chatNativeRoute: ReturnType<typeof routeModel> | null = null;
|
|
115
118
|
try {
|
|
116
|
-
const route = routeModel(config,
|
|
119
|
+
const route = routeModel(config, chatBody.model as string, evidenceFromBody(chatBody));
|
|
117
120
|
// Settle the wire once so every branch below reads the adapter this model will
|
|
118
121
|
// actually use, not the provider-wide default (#404).
|
|
119
122
|
route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, "chat");
|
|
@@ -133,7 +136,7 @@ async function handleChatCompletionsWithBudget(
|
|
|
133
136
|
if (chatBody.tools !== undefined) parts.push(JSON.stringify(chatBody.tools));
|
|
134
137
|
logCtx.usageLogInputTokens = Math.max(1, estimateTokens(parts.join("\n"), requestedModel));
|
|
135
138
|
}
|
|
136
|
-
if (isNativeChatRouteEligible(route, chatBody)) chatNativeRoute = route;
|
|
139
|
+
if (!effortRow && isNativeChatRouteEligible(route, chatBody)) chatNativeRoute = route;
|
|
137
140
|
} catch (err) {
|
|
138
141
|
if (err instanceof NoEligiblePolicyCandidateError) {
|
|
139
142
|
logCtx.routeDecision = err.trace;
|
|
@@ -162,6 +165,12 @@ async function handleChatCompletionsWithBudget(
|
|
|
162
165
|
// Validate the full Chat boundary after routing. Native Chat keeps `chatBody` as
|
|
163
166
|
// its wire source; this Responses projection is used only by the fallback path.
|
|
164
167
|
internalBody = chatCompletionsToResponsesBody(chatBody);
|
|
168
|
+
if (effortRow) {
|
|
169
|
+
internalBody.reasoning = {
|
|
170
|
+
...(isRec(internalBody.reasoning) ? internalBody.reasoning : {}),
|
|
171
|
+
effort: effortRow.effort,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
165
174
|
} catch (err) {
|
|
166
175
|
const overflow = isTranslatorBudgetExceededError(err);
|
|
167
176
|
const status = overflow ? 413 : err instanceof ChatCompletionsRequestError ? 400 : 500;
|
|
@@ -204,12 +204,24 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
|
|
|
204
204
|
return fail(400, error instanceof Error ? error.message : String(error), "invalid_request_error");
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
// One inbound request owns one transient send allowance. Capture the policy before any
|
|
208
|
+
// key rotation so recovery cannot replace the ceiling along with the active credential.
|
|
209
|
+
const requestTransientPolicy = transientRetryPolicyFor(activeProvider);
|
|
210
|
+
let transientSendsUsed = 0;
|
|
211
|
+
const remainingTransientSends = (): number => requestTransientPolicy
|
|
212
|
+
? Math.max(0, requestTransientPolicy.attempts - transientSendsUsed)
|
|
213
|
+
: Number.POSITIVE_INFINITY;
|
|
214
|
+
const transientSendAvailable = (): boolean => remainingTransientSends() > 0;
|
|
215
|
+
|
|
207
216
|
const send = async (request: AdapterRequest, recovery?: "rate-limit-429" | "key-429"): Promise<Response> => {
|
|
208
217
|
try {
|
|
209
218
|
// #2643: opted-in key-auth openai-chat providers retry pre-stream transient statuses on
|
|
210
219
|
// the native chat lane too; everyone else keeps reset-only semantics.
|
|
211
|
-
const
|
|
212
|
-
|
|
220
|
+
const remaining = remainingTransientSends();
|
|
221
|
+
if (requestTransientPolicy && remaining <= 0) {
|
|
222
|
+
throw new Error("native Chat transient send budget exhausted before recovery dispatch");
|
|
223
|
+
}
|
|
224
|
+
const fetchWithPolicy = requestTransientPolicy ? fetchWithTransientRetry : fetchWithResetRetry;
|
|
213
225
|
return await fetchWithPolicy(
|
|
214
226
|
(transportRecovery?: UpstreamSendRecovery) => {
|
|
215
227
|
noteAttemptSend(attempt, logCtx.usageLogInputTokens, transportRecovery ?? recovery);
|
|
@@ -232,7 +244,12 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
|
|
|
232
244
|
{
|
|
233
245
|
abortSignal: upstream.signal,
|
|
234
246
|
label: safeHostLabel(request.url),
|
|
235
|
-
...(
|
|
247
|
+
...(requestTransientPolicy
|
|
248
|
+
? {
|
|
249
|
+
attempts: remaining,
|
|
250
|
+
onSendsConsumed: (sends: number) => { transientSendsUsed += Math.max(0, sends); },
|
|
251
|
+
}
|
|
252
|
+
: {}),
|
|
236
253
|
},
|
|
237
254
|
);
|
|
238
255
|
} finally {
|
|
@@ -245,7 +262,12 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
|
|
|
245
262
|
response = await send(activeRequest);
|
|
246
263
|
const retryPolicy = rateLimitRetryPolicyFor(activeProvider);
|
|
247
264
|
let retries = 0;
|
|
248
|
-
while (
|
|
265
|
+
while (
|
|
266
|
+
response.status === 429
|
|
267
|
+
&& retryPolicy
|
|
268
|
+
&& retries < retryPolicy.attempts
|
|
269
|
+
&& transientSendAvailable()
|
|
270
|
+
) {
|
|
249
271
|
retries += 1;
|
|
250
272
|
for await (const _ of prepareSameTarget429Wait({
|
|
251
273
|
body: response.body,
|
|
@@ -263,6 +285,10 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
|
|
|
263
285
|
promptCacheKey: typeof options.chatBody.prompt_cache_key === "string" ? options.chatBody.prompt_cache_key : undefined,
|
|
264
286
|
});
|
|
265
287
|
if (!rotated) break;
|
|
288
|
+
// Rotation also records the failed key's cooldown and persists the next healthy key.
|
|
289
|
+
// Keep that bookkeeping when this request has spent its final send, but preserve the
|
|
290
|
+
// terminal 429 body and do not dispatch with the replacement credential.
|
|
291
|
+
if (!transientSendAvailable()) break;
|
|
266
292
|
try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
|
|
267
293
|
activeProvider = rotated;
|
|
268
294
|
activeAdapter = createOpenAIChatAdapter(activeProvider);
|
|
@@ -50,6 +50,10 @@ import {
|
|
|
50
50
|
isTranslatorBudgetExceededError,
|
|
51
51
|
type TranslatorBudget,
|
|
52
52
|
} from "../lib/translator-budget";
|
|
53
|
+
import {
|
|
54
|
+
parseRequestEffortRowId,
|
|
55
|
+
type ParsedEffortRowId,
|
|
56
|
+
} from "./effort-row";
|
|
53
57
|
|
|
54
58
|
type Rec = Record<string, unknown>;
|
|
55
59
|
|
|
@@ -600,7 +604,9 @@ async function handleClaudeMessagesWithBudget(
|
|
|
600
604
|
let anthropicBody: unknown;
|
|
601
605
|
let internalBody: Rec;
|
|
602
606
|
let cacheKeySource: ClaudeCacheKeySource = null;
|
|
603
|
-
let effortOverride:
|
|
607
|
+
let effortOverride: string | null = null;
|
|
608
|
+
let effortRow: ParsedEffortRowId | null = null;
|
|
609
|
+
let requestedModel = "";
|
|
604
610
|
try {
|
|
605
611
|
anthropicBody = await readAnthropicBody(req, translatorBudget);
|
|
606
612
|
// Defensive [1m] strip (devlog 138): clients normally remove the context-variant
|
|
@@ -620,6 +626,14 @@ async function handleClaudeMessagesWithBudget(
|
|
|
620
626
|
effortOverride = extractOcxEffortDirective(anthropicBody);
|
|
621
627
|
}
|
|
622
628
|
}
|
|
629
|
+
if (isRec(anthropicBody) && typeof anthropicBody.model === "string") {
|
|
630
|
+
requestedModel = anthropicBody.model;
|
|
631
|
+
effortRow = parseRequestEffortRowId(requestedModel, config);
|
|
632
|
+
if (effortRow) {
|
|
633
|
+
anthropicBody.model = effortRow.baseId;
|
|
634
|
+
effortOverride = effortRow.effort;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
623
637
|
// Debug capture (opt-in allowlist scalars) BEFORE the passthrough branch so
|
|
624
638
|
// native, routed, and disabled-alias paths are all observable (devlog 130 B1).
|
|
625
639
|
captureClaudeInbound(
|
|
@@ -643,7 +657,7 @@ async function handleClaudeMessagesWithBudget(
|
|
|
643
657
|
);
|
|
644
658
|
if (claudeConversationId) logCtx.conversationId = claudeConversationId;
|
|
645
659
|
}
|
|
646
|
-
if (isRec(anthropicBody) && wantsNativePassthrough(req, config, requestPolicy, anthropicBody.model)) {
|
|
660
|
+
if (!effortRow && isRec(anthropicBody) && wantsNativePassthrough(req, config, requestPolicy, anthropicBody.model)) {
|
|
647
661
|
return await anthropicNativePassthrough(req, config, logCtx, logIds, anthropicBody, "/v1/messages");
|
|
648
662
|
}
|
|
649
663
|
if (isRec(anthropicBody) && effortOverride) {
|
|
@@ -669,7 +683,7 @@ async function handleClaudeMessagesWithBudget(
|
|
|
669
683
|
);
|
|
670
684
|
}
|
|
671
685
|
|
|
672
|
-
|
|
686
|
+
if (!requestedModel) requestedModel = (anthropicBody as Rec).model as string;
|
|
673
687
|
const stream = internalBody.stream === true;
|
|
674
688
|
// Routed adapters only support streamed turns; always stream internally and fold
|
|
675
689
|
// the translated Anthropic SSE into a message JSON for non-streaming clients.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { comboModelId, comboPublicModelId } from "../combos/types";
|
|
2
|
+
import { detectCursorInstalls } from "../integrations/cursor-detect";
|
|
3
|
+
import {
|
|
4
|
+
loadCursorEffortTable,
|
|
5
|
+
type CursorEffortTable,
|
|
6
|
+
} from "../integrations/cursor-effort-table";
|
|
7
|
+
import {
|
|
8
|
+
canonicalizeReasoningEfforts,
|
|
9
|
+
isDeclaredReasoningEffort,
|
|
10
|
+
} from "../reasoning-effort";
|
|
11
|
+
import { knownModelIdsForProvider } from "../router";
|
|
12
|
+
import { policyModelId, policyPublicModelId } from "../routing/profile";
|
|
13
|
+
import type { OcxConfig } from "../types";
|
|
14
|
+
import { routedSlug } from "../providers/slug-codec";
|
|
15
|
+
import { predictCursorEffort } from "./models-capabilities";
|
|
16
|
+
|
|
17
|
+
const EFFORT_ROW_SEPARATOR = "--";
|
|
18
|
+
|
|
19
|
+
export interface ParsedEffortRowId {
|
|
20
|
+
baseId: string;
|
|
21
|
+
effort: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type EffortRowKnownIds = ReadonlySet<string> | ((id: string) => boolean);
|
|
25
|
+
|
|
26
|
+
export interface EffortRowOptions {
|
|
27
|
+
knownIds?: EffortRowKnownIds;
|
|
28
|
+
table?: CursorEffortTable | null;
|
|
29
|
+
supportsReasoning?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isKnownId(knownIds: EffortRowKnownIds | undefined, id: string): boolean {
|
|
33
|
+
return typeof knownIds === "function" ? knownIds(id) : knownIds?.has(id) === true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function effortRowId(baseId: string, effort: string): string {
|
|
37
|
+
return `${baseId}${EFFORT_ROW_SEPARATOR}${effort}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Exact configured/public ids that must beat the synthetic terminal-suffix grammar.
|
|
42
|
+
* This is request-local because live-model cache contents can change while the server runs.
|
|
43
|
+
*/
|
|
44
|
+
export function knownEffortRowIds(config: OcxConfig): Set<string> {
|
|
45
|
+
const ids = new Set<string>();
|
|
46
|
+
for (const [providerName, provider] of Object.entries(config.providers)) {
|
|
47
|
+
const known = knownModelIdsForProvider(providerName, provider, config);
|
|
48
|
+
const namespaces = [providerName, provider.alias].filter((value): value is string => (
|
|
49
|
+
typeof value === "string" && value.length > 0
|
|
50
|
+
));
|
|
51
|
+
for (const id of known) {
|
|
52
|
+
ids.add(id);
|
|
53
|
+
ids.add(routedSlug(providerName, id));
|
|
54
|
+
for (const namespace of namespaces) ids.add(`${namespace}/${id}`);
|
|
55
|
+
}
|
|
56
|
+
for (const alias of Object.values(provider.modelAliases ?? {})) {
|
|
57
|
+
ids.add(alias);
|
|
58
|
+
for (const namespace of namespaces) ids.add(`${namespace}/${alias}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for (const [id, combo] of Object.entries(config.combos ?? {})) {
|
|
62
|
+
ids.add(comboModelId(id));
|
|
63
|
+
ids.add(comboPublicModelId(id, combo));
|
|
64
|
+
}
|
|
65
|
+
for (const [id, profile] of Object.entries(config.routingProfiles ?? {})) {
|
|
66
|
+
ids.add(policyModelId(id));
|
|
67
|
+
ids.add(policyPublicModelId(id, profile));
|
|
68
|
+
}
|
|
69
|
+
return ids;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Resolve the installed Private Inference effort table once for the current request. */
|
|
73
|
+
export function loadDetectedCursorEffortTable(): CursorEffortTable | null {
|
|
74
|
+
const privateInference = detectCursorInstalls().find(install => install.build === "private-inference");
|
|
75
|
+
return loadCursorEffortTable(privateInference);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function parseEffortRowId(
|
|
79
|
+
id: string,
|
|
80
|
+
config: Pick<OcxConfig, "cursorEffortRows">,
|
|
81
|
+
options: EffortRowOptions = {},
|
|
82
|
+
): ParsedEffortRowId | null {
|
|
83
|
+
if (config.cursorEffortRows !== true || isKnownId(options.knownIds, id)) return null;
|
|
84
|
+
|
|
85
|
+
const separator = id.lastIndexOf(EFFORT_ROW_SEPARATOR);
|
|
86
|
+
if (separator <= 0) return null;
|
|
87
|
+
const baseId = id.slice(0, separator);
|
|
88
|
+
const effort = id.slice(separator + EFFORT_ROW_SEPARATOR.length);
|
|
89
|
+
// "none" is never published as a row (discovery filters it), so it is never accepted either.
|
|
90
|
+
if (effort === "none" || !isDeclaredReasoningEffort(effort)) return null;
|
|
91
|
+
if (predictCursorEffort(baseId, options.table ?? null, options.supportsReasoning).ladder !== null) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return { baseId, effort };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Parse one ingress selector against the current config and installed Cursor table. */
|
|
98
|
+
export function parseRequestEffortRowId(id: string, config: OcxConfig): ParsedEffortRowId | null {
|
|
99
|
+
if (config.cursorEffortRows !== true) return null;
|
|
100
|
+
// Ordinary ids carry no separator; bail before the known-id scan and install detection so
|
|
101
|
+
// the flag costs nothing on the request path for models that are not effort rows.
|
|
102
|
+
if (id.lastIndexOf(EFFORT_ROW_SEPARATOR) <= 0) return null;
|
|
103
|
+
return parseEffortRowId(id, config, {
|
|
104
|
+
knownIds: knownEffortRowIds(config),
|
|
105
|
+
table: loadDetectedCursorEffortTable(),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function expandCursorEffortRow<T extends { id: string }>(
|
|
110
|
+
row: T,
|
|
111
|
+
efforts: readonly string[] | undefined,
|
|
112
|
+
config: Pick<OcxConfig, "cursorEffortRows">,
|
|
113
|
+
options: EffortRowOptions = {},
|
|
114
|
+
): T[] {
|
|
115
|
+
if (config.cursorEffortRows !== true) return [row];
|
|
116
|
+
|
|
117
|
+
const supported = canonicalizeReasoningEfforts(
|
|
118
|
+
(efforts ?? []).filter(effort => effort !== "none" && isDeclaredReasoningEffort(effort)),
|
|
119
|
+
);
|
|
120
|
+
const supportsReasoning = options.supportsReasoning ?? supported.length > 0;
|
|
121
|
+
if (predictCursorEffort(row.id, options.table ?? null, supportsReasoning).ladder !== null) {
|
|
122
|
+
return [row];
|
|
123
|
+
}
|
|
124
|
+
return [
|
|
125
|
+
row,
|
|
126
|
+
...supported
|
|
127
|
+
.map(effort => effortRowId(row.id, effort))
|
|
128
|
+
.filter(id => !isKnownId(options.knownIds, id))
|
|
129
|
+
.map(id => ({ ...row, id })),
|
|
130
|
+
];
|
|
131
|
+
}
|