@bitkyc08/opencodex 2.7.31 → 2.7.33
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.ja.md +438 -0
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/README.ru.md +2 -1
- package/README.zh-CN.md +1 -1
- package/bin/ocx.mjs +18 -1
- package/gui/dist/assets/index-D6Fcl4yM.css +1 -0
- package/gui/dist/assets/index-d63HMU0x.js +52 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +17 -2
- package/src/adapters/google-tool-schema.ts +4 -0
- package/src/adapters/google.ts +10 -2
- package/src/adapters/openai-chat.ts +36 -1
- package/src/adapters/openai-responses.ts +2 -1
- package/src/bridge.ts +12 -4
- package/src/cli/account-api.ts +4 -2
- package/src/cli/account-extended.ts +34 -0
- package/src/cli/account.ts +3 -1
- package/src/cli/claude.ts +6 -1
- package/src/cli/help.ts +25 -4
- package/src/cli/init.ts +38 -3
- package/src/cli/models.ts +206 -7
- package/src/codex/auth-api.ts +27 -3
- package/src/codex/catalog.ts +72 -2
- package/src/config.ts +60 -3
- package/src/oauth/github-copilot.ts +1 -0
- package/src/oauth/index.ts +5 -4
- package/src/oauth/kiro.ts +12 -1
- package/src/oauth/store.ts +11 -0
- package/src/oauth/types.ts +3 -1
- package/src/providers/antigravity-models.ts +103 -5
- package/src/providers/api-keys.ts +12 -0
- package/src/providers/openrouter-routing.ts +102 -0
- package/src/providers/registry.ts +7 -5
- package/src/router.ts +2 -1
- package/src/server/auth-cors.ts +5 -0
- package/src/server/management-api.ts +143 -6
- package/src/server/responses.ts +16 -4
- package/src/types.ts +42 -1
- package/src/update/index.ts +19 -2
- package/src/update/job.ts +13 -3
- package/src/usage/expected-prices.ts +10 -0
- package/src/usage/summary.ts +43 -0
- package/gui/dist/assets/index-BPa0R6EN.js +0 -46
- package/gui/dist/assets/index-BY7KvJRB.css +0 -1
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
// Google Antigravity (Cloud Code Assist) bundled model list.
|
|
2
2
|
//
|
|
3
3
|
// Single source of truth: the Antigravity `:fetchAvailableModels` backend, the same one the `agy`
|
|
4
|
-
// CLI resolves labels against. The ids below separate CCA wire ids,
|
|
5
|
-
// hidden compatibility aliases for saved selections. The CCA envelope's `model` field must
|
|
6
|
-
// "Gemini 3.1 Pro (High)" => gemini-pro-agent), while the
|
|
4
|
+
// CLI resolves labels against. The ids below separate CCA wire ids, collapsed picker entries,
|
|
5
|
+
// and hidden compatibility aliases for saved selections. The CCA envelope's `model` field must
|
|
6
|
+
// receive the wire id (for example "Gemini 3.1 Pro (High)" => gemini-pro-agent), while the
|
|
7
|
+
// picker exposes collapsed base models with reasoning-effort routing.
|
|
8
|
+
|
|
9
|
+
// ── Wire IDs (what CCA :fetchAvailableModels returns) ──
|
|
7
10
|
const ANTIGRAVITY_WIRE_MODELS = [
|
|
8
11
|
"gemini-3.6-flash-low",
|
|
9
12
|
"gemini-3.6-flash-medium",
|
|
@@ -15,12 +18,50 @@ const ANTIGRAVITY_WIRE_MODELS = [
|
|
|
15
18
|
"gpt-oss-120b-medium",
|
|
16
19
|
];
|
|
17
20
|
|
|
21
|
+
// ── Effort ladders per collapsed base model ──
|
|
22
|
+
// Gemini models: effort → wire model suffix (official agy UI pattern).
|
|
23
|
+
// Claude Opus: effort → thinkingConfig.thinkingLevel (CLIProxyAPI proven pattern).
|
|
24
|
+
export const ANTIGRAVITY_MODEL_EFFORTS: Record<string, string[]> = {
|
|
25
|
+
"gemini-3.6-flash": ["low", "medium", "high"],
|
|
26
|
+
"gemini-3.1-pro": ["low", "high"],
|
|
27
|
+
"claude-sonnet-4-6": ["low", "medium", "high", "max"],
|
|
28
|
+
"claude-opus-4-6-thinking": ["low", "medium", "high", "max"],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// ── Effort → wire model map for Gemini base models ──
|
|
32
|
+
const ANTIGRAVITY_EFFORT_WIRE_MAP: Record<string, Record<string, string>> = {
|
|
33
|
+
"gemini-3.6-flash": {
|
|
34
|
+
low: "gemini-3.6-flash-low",
|
|
35
|
+
medium: "gemini-3.6-flash-medium",
|
|
36
|
+
high: "gemini-3.6-flash-high",
|
|
37
|
+
},
|
|
38
|
+
"gemini-3.1-pro": {
|
|
39
|
+
low: "gemini-3.1-pro-low",
|
|
40
|
+
high: "gemini-pro-agent",
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// ── Default effort per Gemini base model ──
|
|
45
|
+
const ANTIGRAVITY_DEFAULT_EFFORT: Record<string, string> = {
|
|
46
|
+
"gemini-3.6-flash": "medium",
|
|
47
|
+
"gemini-3.1-pro": "high",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// ── Visible client aliases (kept for saved-config compat, not picker-visible) ──
|
|
18
51
|
const ANTIGRAVITY_VISIBLE_MODEL_ALIASES: Record<string, string> = {
|
|
19
52
|
"gemini-3.1-pro-high": "gemini-pro-agent",
|
|
20
53
|
"gemini-3.1-pro-preview": "gemini-pro-agent",
|
|
21
54
|
};
|
|
22
55
|
|
|
56
|
+
// ── Hidden compatibility aliases for saved selections ──
|
|
57
|
+
// Wire suffix IDs are identity aliases — they resolve to themselves so saved configs
|
|
58
|
+
// with explicit suffixes (e.g. gemini-3.6-flash-low) continue to work.
|
|
23
59
|
const ANTIGRAVITY_COMPATIBILITY_MODEL_ALIASES: Record<string, string> = {
|
|
60
|
+
"gemini-3.6-flash-low": "gemini-3.6-flash-low",
|
|
61
|
+
"gemini-3.6-flash-medium": "gemini-3.6-flash-medium",
|
|
62
|
+
"gemini-3.6-flash-high": "gemini-3.6-flash-high",
|
|
63
|
+
"gemini-3.1-pro-low": "gemini-3.1-pro-low",
|
|
64
|
+
"gemini-pro-agent": "gemini-pro-agent",
|
|
24
65
|
"gemini-3.5-flash-extra-low": "gemini-3.6-flash-low",
|
|
25
66
|
"gemini-3.5-flash-low": "gemini-3.6-flash-medium",
|
|
26
67
|
"gemini-3.5-flash-mid": "gemini-3.6-flash-medium",
|
|
@@ -33,9 +74,13 @@ export const ANTIGRAVITY_MODEL_ALIASES: Record<string, string> = {
|
|
|
33
74
|
...ANTIGRAVITY_COMPATIBILITY_MODEL_ALIASES,
|
|
34
75
|
};
|
|
35
76
|
|
|
77
|
+
// Picker-visible: collapsed base models only.
|
|
36
78
|
export const ANTIGRAVITY_MODELS = [
|
|
37
|
-
|
|
38
|
-
|
|
79
|
+
"gemini-3.6-flash",
|
|
80
|
+
"gemini-3.1-pro",
|
|
81
|
+
"claude-sonnet-4-6",
|
|
82
|
+
"claude-opus-4-6-thinking",
|
|
83
|
+
"gpt-oss-120b-medium",
|
|
39
84
|
];
|
|
40
85
|
|
|
41
86
|
// Context windows from the upstream `:fetchAvailableModels` maxTokens per model.
|
|
@@ -51,6 +96,10 @@ const ANTIGRAVITY_WIRE_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
|
51
96
|
};
|
|
52
97
|
|
|
53
98
|
export const ANTIGRAVITY_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
99
|
+
// Collapsed base IDs — explicit entries for the picker.
|
|
100
|
+
"gemini-3.6-flash": 1_048_576,
|
|
101
|
+
"gemini-3.1-pro": 1_048_576,
|
|
102
|
+
// Wire IDs and aliases via derivation.
|
|
54
103
|
...ANTIGRAVITY_WIRE_MODEL_CONTEXT_WINDOWS,
|
|
55
104
|
...Object.fromEntries(
|
|
56
105
|
Object.entries(ANTIGRAVITY_MODEL_ALIASES).map(([alias, wire]) => [
|
|
@@ -63,3 +112,52 @@ export const ANTIGRAVITY_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
|
63
112
|
export function resolveAntigravityWireModelId(modelId: string): string {
|
|
64
113
|
return ANTIGRAVITY_MODEL_ALIASES[modelId] ?? modelId;
|
|
65
114
|
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Whether the given model ID is a suffix wire ID or compat alias that already encodes
|
|
118
|
+
* an effort level. For these IDs, the caller must NOT send thinkingConfig — the suffix
|
|
119
|
+
* IS the effort, and sending both creates a contradictory request.
|
|
120
|
+
*/
|
|
121
|
+
export function isAntigravitySuffixModelId(modelId: string): boolean {
|
|
122
|
+
return !(ANTIGRAVITY_MODELS as string[]).includes(modelId);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Resolve a picker-visible base model + optional reasoning effort to the CCA wire model ID.
|
|
127
|
+
*
|
|
128
|
+
* Precedence (evaluated in order):
|
|
129
|
+
* 1. Suffix wire ID or compat alias → resolve via `resolveAntigravityWireModelId`, no thinkingConfig.
|
|
130
|
+
* 2. Mapped Gemini base with effort → return mapped wire ID + thinkingLevel.
|
|
131
|
+
* 3. Mapped Gemini base without effort → return default-effort wire ID, no thinkingConfig.
|
|
132
|
+
* 4. Claude Opus with effort → return identity + thinkingLevel (no suffix variants exist).
|
|
133
|
+
* 5. All other IDs → return `resolveAntigravityWireModelId(modelId)`, no thinkingConfig.
|
|
134
|
+
*/
|
|
135
|
+
export function resolveAntigravityEffortWireModel(
|
|
136
|
+
modelId: string,
|
|
137
|
+
effort?: string,
|
|
138
|
+
): { wireModelId: string; thinkingLevel?: string } {
|
|
139
|
+
// Rule 1: suffix/compat alias — suffix IS the effort.
|
|
140
|
+
if (isAntigravitySuffixModelId(modelId)) {
|
|
141
|
+
return { wireModelId: resolveAntigravityWireModelId(modelId) };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Rule 2/3: mapped Gemini base model.
|
|
145
|
+
const effortMap = ANTIGRAVITY_EFFORT_WIRE_MAP[modelId];
|
|
146
|
+
if (effortMap) {
|
|
147
|
+
if (effort && effort in effortMap) {
|
|
148
|
+
return { wireModelId: effortMap[effort]!, thinkingLevel: effort };
|
|
149
|
+
}
|
|
150
|
+
const defaultEffort = ANTIGRAVITY_DEFAULT_EFFORT[modelId]!;
|
|
151
|
+
return { wireModelId: effortMap[defaultEffort]! };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Rule 4: Claude models — effort via thinkingConfig only (no suffix variants).
|
|
155
|
+
// Anthropic adaptive thinking supports low/medium/high/max for both Sonnet 4.6 and Opus 4.6.
|
|
156
|
+
// CLIProxyAPI proves CCA accepts thinkingConfig on base IDs (validation confirmed).
|
|
157
|
+
if (/^claude-/.test(modelId) && effort) {
|
|
158
|
+
return { wireModelId: modelId, thinkingLevel: effort };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Rule 5: everything else.
|
|
162
|
+
return { wireModelId: resolveAntigravityWireModelId(modelId) };
|
|
163
|
+
}
|
|
@@ -102,6 +102,18 @@ export function setActiveProviderApiKey(config: OcxConfig, name: string, id: str
|
|
|
102
102
|
return true;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
/** Rename a key slot without changing its id, secret, or active routing state. */
|
|
106
|
+
export function setProviderApiKeyLabel(config: OcxConfig, name: string, id: string, label: string | undefined): boolean {
|
|
107
|
+
const provider = config.providers[name];
|
|
108
|
+
if (!provider || !isKeyAuthProvider(provider)) return false;
|
|
109
|
+
const entry = ensurePool(provider).find(e => e.id === id);
|
|
110
|
+
if (!entry) return false;
|
|
111
|
+
if (label) entry.label = label;
|
|
112
|
+
else delete entry.label;
|
|
113
|
+
saveConfig(config);
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
105
117
|
/** Remove one key; removing the active one promotes the first remaining. Persists config. */
|
|
106
118
|
export function removeProviderApiKey(config: OcxConfig, name: string, id: string): boolean {
|
|
107
119
|
const provider = config.providers[name];
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { OcxProviderConfig, OpenRouterProviderRouting } from "../types";
|
|
2
|
+
|
|
3
|
+
const ROUTING_KEYS = new Set(["order", "only", "allowFallbacks"]);
|
|
4
|
+
const MAX_PROVIDER_SLUGS = 64;
|
|
5
|
+
|
|
6
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
7
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
8
|
+
const prototype = Object.getPrototypeOf(value);
|
|
9
|
+
return prototype === Object.prototype || prototype === null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function isCanonicalOpenRouterTarget(baseUrl: string): boolean {
|
|
13
|
+
try {
|
|
14
|
+
const url = new URL(baseUrl);
|
|
15
|
+
return url.origin === "https://openrouter.ai"
|
|
16
|
+
&& !url.username
|
|
17
|
+
&& !url.password
|
|
18
|
+
&& !url.search
|
|
19
|
+
&& !url.hash
|
|
20
|
+
&& url.pathname.replace(/\/+$/, "") === "/api/v1";
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function routingPreferenceError(value: unknown, field: string): string | null {
|
|
27
|
+
if (!isPlainRecord(value)) return `${field} must be a plain object`;
|
|
28
|
+
const unknown = Object.keys(value).find(key => !ROUTING_KEYS.has(key));
|
|
29
|
+
if (unknown) return `${field} contains unknown field "${unknown}"`;
|
|
30
|
+
|
|
31
|
+
for (const listField of ["order", "only"] as const) {
|
|
32
|
+
const list = value[listField];
|
|
33
|
+
if (list === undefined) continue;
|
|
34
|
+
if (!Array.isArray(list) || list.length === 0 || list.length > MAX_PROVIDER_SLUGS) {
|
|
35
|
+
return `${field}.${listField} must contain 1-${MAX_PROVIDER_SLUGS} provider slugs`;
|
|
36
|
+
}
|
|
37
|
+
const seen = new Set<string>();
|
|
38
|
+
for (const slug of list) {
|
|
39
|
+
if (typeof slug !== "string" || !slug.trim() || slug !== slug.trim() || slug.length > 128) {
|
|
40
|
+
return `${field}.${listField} must contain nonblank trimmed provider slugs up to 128 characters`;
|
|
41
|
+
}
|
|
42
|
+
if (seen.has(slug)) return `${field}.${listField} must not contain duplicate provider slugs`;
|
|
43
|
+
seen.add(slug);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (value.allowFallbacks !== undefined && typeof value.allowFallbacks !== "boolean") {
|
|
47
|
+
return `${field}.allowFallbacks must be a boolean`;
|
|
48
|
+
}
|
|
49
|
+
if (value.order === undefined && value.only === undefined && value.allowFallbacks === undefined) {
|
|
50
|
+
return `${field} must define order, only, or allowFallbacks`;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function openRouterRoutingConfigError(provider: OcxProviderConfig): string | null {
|
|
56
|
+
const hasDefault = provider.openRouterRouting !== undefined;
|
|
57
|
+
const hasModels = provider.modelOpenRouterRouting !== undefined;
|
|
58
|
+
if (!hasDefault && !hasModels) return null;
|
|
59
|
+
if (provider.adapter !== "openai-chat") {
|
|
60
|
+
return "OpenRouter routing preferences require the openai-chat adapter";
|
|
61
|
+
}
|
|
62
|
+
if (!isCanonicalOpenRouterTarget(provider.baseUrl)) {
|
|
63
|
+
return "OpenRouter routing preferences require the canonical https://openrouter.ai/api/v1 baseUrl";
|
|
64
|
+
}
|
|
65
|
+
if (hasDefault) {
|
|
66
|
+
const error = routingPreferenceError(provider.openRouterRouting, "openRouterRouting");
|
|
67
|
+
if (error) return error;
|
|
68
|
+
}
|
|
69
|
+
if (hasModels) {
|
|
70
|
+
const routes = provider.modelOpenRouterRouting;
|
|
71
|
+
if (!isPlainRecord(routes)) return "modelOpenRouterRouting must be a plain object";
|
|
72
|
+
for (const [modelId, preference] of Object.entries(routes)) {
|
|
73
|
+
if (!modelId.trim() || modelId !== modelId.trim()) {
|
|
74
|
+
return "modelOpenRouterRouting keys must be nonblank trimmed model ids";
|
|
75
|
+
}
|
|
76
|
+
const error = routingPreferenceError(preference, `modelOpenRouterRouting.${modelId}`);
|
|
77
|
+
if (error) return error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function resolveOpenRouterRouting(
|
|
84
|
+
provider: OcxProviderConfig,
|
|
85
|
+
modelId: string,
|
|
86
|
+
): OpenRouterProviderRouting | undefined {
|
|
87
|
+
if (!isCanonicalOpenRouterTarget(provider.baseUrl)) return undefined;
|
|
88
|
+
const modelRoutes = provider.modelOpenRouterRouting;
|
|
89
|
+
return modelRoutes && Object.hasOwn(modelRoutes, modelId)
|
|
90
|
+
? modelRoutes[modelId]
|
|
91
|
+
: provider.openRouterRouting;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function openRouterProviderPayload(
|
|
95
|
+
preference: OpenRouterProviderRouting,
|
|
96
|
+
): Record<string, unknown> {
|
|
97
|
+
return {
|
|
98
|
+
...(preference.order ? { order: [...preference.order] } : {}),
|
|
99
|
+
...(preference.only ? { only: [...preference.only] } : {}),
|
|
100
|
+
...(preference.allowFallbacks !== undefined ? { allow_fallbacks: preference.allowFallbacks } : {}),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CodexAccountMode, OcxProviderConfig } from "../types";
|
|
2
2
|
import { KIRO_MODELS, KIRO_MODEL_CONTEXT_WINDOWS, KIRO_MODEL_REASONING_EFFORTS } from "./kiro-models";
|
|
3
|
-
import { ANTIGRAVITY_MODELS, ANTIGRAVITY_MODEL_CONTEXT_WINDOWS } from "./antigravity-models";
|
|
3
|
+
import { ANTIGRAVITY_MODELS, ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, ANTIGRAVITY_MODEL_EFFORTS } from "./antigravity-models";
|
|
4
4
|
import type { ProviderBaseUrlChoice } from "./base-url-choices";
|
|
5
5
|
import {
|
|
6
6
|
QWEN_CLOUD_BASE_URL_CHOICES, QWEN_CLOUD_TOKEN_PLAN_BASE_URL,
|
|
@@ -65,6 +65,8 @@ export interface ProviderRegistryEntry {
|
|
|
65
65
|
noPenaltyModels?: string[];
|
|
66
66
|
/** Opt this provider into parallel tool calls (see OcxProviderConfig.parallelToolCalls). */
|
|
67
67
|
parallelToolCalls?: boolean;
|
|
68
|
+
/** Opt this provider into forwarding prompt_cache_key (OpenAI-specific; strict backends reject it). */
|
|
69
|
+
promptCacheKey?: boolean;
|
|
68
70
|
autoToolChoiceOnlyModels?: string[];
|
|
69
71
|
preserveReasoningContentModels?: string[];
|
|
70
72
|
thinkingToggleModels?: string[];
|
|
@@ -638,9 +640,9 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
638
640
|
// devlog/_plan/260710_provider_hardening/001_research_frontier.md.
|
|
639
641
|
{
|
|
640
642
|
id: "google", label: "Google Gemini", adapter: "google", baseUrl: "https://generativelanguage.googleapis.com", authKind: "key", featured: true,
|
|
641
|
-
dashboardUrl: "https://aistudio.google.com/apikey", defaultModel: "gemini-3.5-flash", models: ["gemini-3.6-flash", "gemini-3.5-flash", "gemini-3.1-pro-preview"],
|
|
642
|
-
modelContextWindows: { "gemini-3.6-flash": 1_048_576, "gemini-3.5-flash": 1_000_000 },
|
|
643
|
-
modelInputModalities: { "gemini-3.6-flash": ["text", "image"] },
|
|
643
|
+
dashboardUrl: "https://aistudio.google.com/apikey", defaultModel: "gemini-3.5-flash", models: ["gemini-3.6-flash", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.1-pro-preview"],
|
|
644
|
+
modelContextWindows: { "gemini-3.6-flash": 1_048_576, "gemini-3.5-flash": 1_000_000, "gemini-3.5-flash-lite": 1_048_576 },
|
|
645
|
+
modelInputModalities: { "gemini-3.6-flash": ["text", "image"], "gemini-3.5-flash-lite": ["text", "image"] },
|
|
644
646
|
modelReasoningEfforts: {
|
|
645
647
|
"gemini-3.6-flash": ["minimal", "low", "medium", "high"],
|
|
646
648
|
"gemini-3.5-flash": ["minimal", "low", "medium", "high"],
|
|
@@ -651,7 +653,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
651
653
|
// 2026-07-10: defaultModel is frozen pending Vertex-specific Tier-2 evidence; Gemini API
|
|
652
654
|
// evidence from ai.google.dev does not establish Vertex publisher availability.
|
|
653
655
|
{ id: "google-vertex", label: "Google Vertex AI", adapter: "google", baseUrl: "https://aiplatform.googleapis.com", authKind: "key", dashboardUrl: "https://console.cloud.google.com/vertex-ai", defaultModel: "gemini-3-pro", googleMode: "vertex", jawcodeBundle: "google", extraMetadataAliases: ["gemini-vertex"] },
|
|
654
|
-
{ id: "google-antigravity", label: "Google Antigravity", adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authKind: "oauth", dashboardUrl: "https://antigravity.google", models: ANTIGRAVITY_MODELS, defaultModel: "gemini-3.6-flash
|
|
656
|
+
{ id: "google-antigravity", label: "Google Antigravity", adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authKind: "oauth", dashboardUrl: "https://antigravity.google", models: ANTIGRAVITY_MODELS, defaultModel: "gemini-3.6-flash", modelContextWindows: ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, modelReasoningEfforts: ANTIGRAVITY_MODEL_EFFORTS, googleMode: "cloud-code-assist", jawcodeBundle: "google", extraMetadataAliases: ["antigravity", "gemini-antigravity"] },
|
|
655
657
|
{ id: "azure-openai", label: "Azure OpenAI", adapter: "azure-openai", baseUrl: "https://{resource}.openai.azure.com/openai", authKind: "key", featured: true, dashboardUrl: "https://portal.azure.com" },
|
|
656
658
|
{ id: "ollama", label: "Ollama (local)", adapter: "openai-chat", baseUrl: "http://localhost:11434/v1", authKind: "local", allowPrivateNetworkByDefault: true, allowBaseUrlOverride: true, featured: true, note: "Local — key usually blank" },
|
|
657
659
|
{ id: "vllm", label: "vLLM (local)", adapter: "openai-chat", baseUrl: "http://localhost:8000/v1", authKind: "local", allowPrivateNetworkByDefault: true, allowBaseUrlOverride: true, featured: true, note: "Local — key usually blank" },
|
package/src/router.ts
CHANGED
|
@@ -182,6 +182,7 @@ function routedProviderConfig(providerName: string, provider: OcxProviderConfig)
|
|
|
182
182
|
// Scalar backfill: a persisted config created before the flag shipped inherits the registry
|
|
183
183
|
// opt-in, while an explicit user `false` keeps overriding registry `true`.
|
|
184
184
|
...(provider.parallelToolCalls === undefined && registryEntry.parallelToolCalls !== undefined ? { parallelToolCalls: registryEntry.parallelToolCalls } : {}),
|
|
185
|
+
...(provider.promptCacheKey === undefined && registryEntry.promptCacheKey !== undefined ? { promptCacheKey: registryEntry.promptCacheKey } : {}),
|
|
185
186
|
...(modelContextWindows ? { modelContextWindows } : {}),
|
|
186
187
|
...(modelInputModalities ? { modelInputModalities } : {}),
|
|
187
188
|
...(modelMaxInputTokens ? { modelMaxInputTokens } : {}),
|
|
@@ -208,7 +209,7 @@ function activeProviderEntries(config: OcxConfig): [string, OcxProviderConfig][]
|
|
|
208
209
|
|
|
209
210
|
export class NoEnabledOpenAiProviderError extends Error {
|
|
210
211
|
constructor(modelId: string) {
|
|
211
|
-
super(`No enabled
|
|
212
|
+
super(`No enabled OpenAI provider for model: ${modelId}. Run 'ocx init' to configure a provider, or check that your config has an enabled 'openai' provider.`);
|
|
212
213
|
this.name = "NoEnabledOpenAiProviderError";
|
|
213
214
|
}
|
|
214
215
|
}
|
package/src/server/auth-cors.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { providerDestinationConfigError } from "../lib/destination-policy";
|
|
|
10
10
|
import { getProviderRegistryEntry, providerCodexAccountMode } from "../providers/registry";
|
|
11
11
|
import { providerConfigSeed } from "../providers/derive";
|
|
12
12
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
13
|
+
import { openRouterRoutingConfigError } from "../providers/openrouter-routing";
|
|
13
14
|
|
|
14
15
|
let _corsOrigin = "http://localhost:10100";
|
|
15
16
|
export function setCorsOrigin(port: number): void { _corsOrigin = `http://localhost:${port}`; }
|
|
@@ -226,6 +227,8 @@ export function providerManagementConfigError(name: unknown, provider: unknown):
|
|
|
226
227
|
if (headersError) return `provider ${name} ${headersError}`;
|
|
227
228
|
const maxInputError = positiveIntegerRecordConfigError(raw.modelMaxInputTokens, "modelMaxInputTokens");
|
|
228
229
|
if (maxInputError) return `provider ${name} ${maxInputError}`;
|
|
230
|
+
const openRouterError = openRouterRoutingConfigError(typed);
|
|
231
|
+
if (openRouterError) return `provider ${name} ${openRouterError}`;
|
|
229
232
|
if (typed.authMode === "local") {
|
|
230
233
|
// "local" bypasses key-requirement enforcement (api-keys/key-failover treat non-oauth/
|
|
231
234
|
// forward as key auth; openai-chat skips credential checks for local). Only providers
|
|
@@ -290,6 +293,8 @@ export function safeConfigDTO(config: OcxConfig): unknown {
|
|
|
290
293
|
"models",
|
|
291
294
|
"contextWindow",
|
|
292
295
|
"modelContextWindows",
|
|
296
|
+
"openRouterRouting",
|
|
297
|
+
"modelOpenRouterRouting",
|
|
293
298
|
"reasoningEfforts",
|
|
294
299
|
"modelReasoningEfforts",
|
|
295
300
|
"noVisionModels",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { readFileSync } from "node:fs";
|
|
2
3
|
import type { CatalogModel } from "../codex/catalog";
|
|
3
4
|
import { invalidateCodexModelsCache, nativeModelRows } from "../codex/catalog";
|
|
@@ -46,7 +47,7 @@ import {
|
|
|
46
47
|
setDebugSettings,
|
|
47
48
|
type DebugFlag,
|
|
48
49
|
} from "../lib/debug-settings";
|
|
49
|
-
import type { OcxClaudeCodeConfig, OcxConfig, OcxProviderConfig } from "../types";
|
|
50
|
+
import type { OcxClaudeCodeConfig, OcxConfig, OcxCustomModel, OcxProviderConfig } from "../types";
|
|
50
51
|
import { drainAndShutdown } from "./lifecycle";
|
|
51
52
|
import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "./request-log";
|
|
52
53
|
import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../usage/cost";
|
|
@@ -798,9 +799,26 @@ export async function handleManagementAPI(req: Request, url: URL, config: OcxCon
|
|
|
798
799
|
native: true,
|
|
799
800
|
...(row.contextWindow !== undefined ? { contextWindow: row.contextWindow } : {}),
|
|
800
801
|
}));
|
|
801
|
-
|
|
802
|
+
const customModels = (config.customModels ?? []).map(cm => {
|
|
803
|
+
const namespaced = routedSlug(cm.provider, cm.modelId);
|
|
804
|
+
return {
|
|
805
|
+
provider: cm.provider,
|
|
806
|
+
id: cm.modelId,
|
|
807
|
+
namespaced,
|
|
808
|
+
disabled: [...disabled].some(stored => slugEquals(stored, cm.provider, cm.modelId)),
|
|
809
|
+
custom: true,
|
|
810
|
+
customId: cm.id,
|
|
811
|
+
displayName: cm.displayName,
|
|
812
|
+
...(cm.contextWindow ? { contextWindow: cm.contextWindow } : {}),
|
|
813
|
+
...(cm.inputModalities ? { inputModalities: cm.inputModalities } : {}),
|
|
814
|
+
};
|
|
815
|
+
});
|
|
816
|
+
// Custom metadata wins when a live/static routed row resolves to the same Codex-facing slug.
|
|
817
|
+
const customNamespaced = new Set(customModels.map(c => c.namespaced));
|
|
818
|
+
const dedupedRouted = models.map(m => {
|
|
802
819
|
// Codex-facing slug (one "/", slug-codec); disabledModels compares tolerate both forms.
|
|
803
820
|
const namespaced = routedSlug(m.provider, m.id);
|
|
821
|
+
if (customNamespaced.has(namespaced)) return null;
|
|
804
822
|
const contextCap = providerContextCap(config, m.provider);
|
|
805
823
|
return {
|
|
806
824
|
...m,
|
|
@@ -808,7 +826,8 @@ export async function handleManagementAPI(req: Request, url: URL, config: OcxCon
|
|
|
808
826
|
disabled: [...disabled].some(stored => slugEquals(stored, m.provider, m.id)),
|
|
809
827
|
...(contextCap !== undefined ? { contextCap, contextCapped: m.contextCapped === true } : {}),
|
|
810
828
|
};
|
|
811
|
-
})
|
|
829
|
+
}).filter(Boolean);
|
|
830
|
+
return jsonResponse([...native, ...dedupedRouted, ...customModels]);
|
|
812
831
|
}
|
|
813
832
|
|
|
814
833
|
if (url.pathname === "/api/provider-context-caps" && req.method === "GET") {
|
|
@@ -880,6 +899,96 @@ export async function handleManagementAPI(req: Request, url: URL, config: OcxCon
|
|
|
880
899
|
return jsonResponse({ ok: true, disabled });
|
|
881
900
|
}
|
|
882
901
|
|
|
902
|
+
if (url.pathname === "/api/custom-models" && req.method === "GET") {
|
|
903
|
+
return jsonResponse(config.customModels ?? []);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
if (url.pathname === "/api/custom-models" && req.method === "POST") {
|
|
907
|
+
let body: { provider?: unknown; modelId?: unknown; displayName?: unknown; contextWindow?: unknown; inputModalities?: unknown };
|
|
908
|
+
try { body = await req.json(); } catch { return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
909
|
+
const provider = typeof body.provider === "string" ? body.provider.trim() : "";
|
|
910
|
+
const modelId = typeof body.modelId === "string" ? body.modelId.trim() : "";
|
|
911
|
+
if (!provider || !modelId) return jsonResponse({ error: "provider and modelId are required" }, 400);
|
|
912
|
+
if (modelId.includes("/")) return jsonResponse({ error: "modelId must not contain /" }, 400);
|
|
913
|
+
if (!isValidProviderName(provider)) return jsonResponse({ error: "invalid provider name" }, 400);
|
|
914
|
+
if (!hasOwnProvider(config.providers, provider)) return jsonResponse({ error: "provider not configured" }, 404);
|
|
915
|
+
const displayName = typeof body.displayName === "string" && body.displayName.trim() ? body.displayName.trim() : undefined;
|
|
916
|
+
if (displayName?.includes("/")) return jsonResponse({ error: "displayName must not contain /" }, 400);
|
|
917
|
+
const contextWindow = typeof body.contextWindow === "number" && body.contextWindow > 0 ? Math.floor(body.contextWindow) : undefined;
|
|
918
|
+
const inputModalities = Array.isArray(body.inputModalities) ? body.inputModalities.filter((m): m is string => typeof m === "string") : undefined;
|
|
919
|
+
const existing = config.customModels ?? [];
|
|
920
|
+
const newSlug = routedSlug(provider, modelId);
|
|
921
|
+
if (existing.some(cm => routedSlug(cm.provider, cm.modelId) === newSlug)) {
|
|
922
|
+
return jsonResponse({ error: "duplicate model" }, 409);
|
|
923
|
+
}
|
|
924
|
+
const entry: OcxCustomModel = {
|
|
925
|
+
id: randomUUID(),
|
|
926
|
+
provider,
|
|
927
|
+
modelId,
|
|
928
|
+
...(displayName ? { displayName } : {}),
|
|
929
|
+
...(contextWindow ? { contextWindow } : {}),
|
|
930
|
+
...(inputModalities && inputModalities.length > 0 ? { inputModalities } : {}),
|
|
931
|
+
addedAt: new Date().toISOString(),
|
|
932
|
+
};
|
|
933
|
+
config.customModels = [...existing, entry];
|
|
934
|
+
const { saveConfig: save } = await import("../config");
|
|
935
|
+
save(config);
|
|
936
|
+
await refreshCodexCatalogBestEffort();
|
|
937
|
+
return jsonResponse(entry, 201);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
const customPutMatch = url.pathname.match(/^\/api\/custom-models\/([^/]+)$/);
|
|
941
|
+
if (customPutMatch && req.method === "PUT") {
|
|
942
|
+
let id: string;
|
|
943
|
+
try { id = decodeURIComponent(customPutMatch[1]); } catch { return jsonResponse({ error: "invalid id encoding" }, 400); }
|
|
944
|
+
let body: { displayName?: unknown; contextWindow?: unknown; inputModalities?: unknown; modelId?: unknown };
|
|
945
|
+
try { body = await req.json(); } catch { return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
946
|
+
const list = config.customModels ?? [];
|
|
947
|
+
const idx = list.findIndex(cm => cm.id === id);
|
|
948
|
+
if (idx === -1) return jsonResponse({ error: "not found" }, 404);
|
|
949
|
+
const cm = { ...list[idx] };
|
|
950
|
+
if (typeof body.modelId === "string" && body.modelId.trim()) {
|
|
951
|
+
if (body.modelId.includes("/")) return jsonResponse({ error: "modelId must not contain /" }, 400);
|
|
952
|
+
cm.modelId = body.modelId.trim();
|
|
953
|
+
}
|
|
954
|
+
if (body.displayName !== undefined) {
|
|
955
|
+
const dn = typeof body.displayName === "string" ? body.displayName.trim() : "";
|
|
956
|
+
if (dn.includes("/")) return jsonResponse({ error: "displayName must not contain /" }, 400);
|
|
957
|
+
cm.displayName = dn || undefined;
|
|
958
|
+
}
|
|
959
|
+
if (body.contextWindow !== undefined) {
|
|
960
|
+
cm.contextWindow = typeof body.contextWindow === "number" && body.contextWindow > 0 ? Math.floor(body.contextWindow) : undefined;
|
|
961
|
+
}
|
|
962
|
+
if (body.inputModalities !== undefined) {
|
|
963
|
+
cm.inputModalities = Array.isArray(body.inputModalities) ? body.inputModalities.filter((m): m is string => typeof m === "string") : undefined;
|
|
964
|
+
}
|
|
965
|
+
const updatedSlug = routedSlug(cm.provider, cm.modelId);
|
|
966
|
+
if (list.some((other, i) => i !== idx && routedSlug(other.provider, other.modelId) === updatedSlug)) {
|
|
967
|
+
return jsonResponse({ error: "duplicate model" }, 409);
|
|
968
|
+
}
|
|
969
|
+
list[idx] = cm;
|
|
970
|
+
config.customModels = list;
|
|
971
|
+
const { saveConfig: save } = await import("../config");
|
|
972
|
+
save(config);
|
|
973
|
+
await refreshCodexCatalogBestEffort();
|
|
974
|
+
return jsonResponse(cm);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const customDelMatch = url.pathname.match(/^\/api\/custom-models\/([^/]+)$/);
|
|
978
|
+
if (customDelMatch && req.method === "DELETE") {
|
|
979
|
+
let id: string;
|
|
980
|
+
try { id = decodeURIComponent(customDelMatch[1]); } catch { return jsonResponse({ error: "invalid id encoding" }, 400); }
|
|
981
|
+
const list = config.customModels ?? [];
|
|
982
|
+
const idx = list.findIndex(cm => cm.id === id);
|
|
983
|
+
if (idx === -1) return jsonResponse({ error: "not found" }, 404);
|
|
984
|
+
list.splice(idx, 1);
|
|
985
|
+
config.customModels = list.length > 0 ? list : undefined;
|
|
986
|
+
const { saveConfig: save } = await import("../config");
|
|
987
|
+
save(config);
|
|
988
|
+
await refreshCodexCatalogBestEffort();
|
|
989
|
+
return jsonResponse({ ok: true });
|
|
990
|
+
}
|
|
991
|
+
|
|
883
992
|
// multi_agent_v2 surface toggle. GET reports the flag + the agents.max_threads
|
|
884
993
|
// boot conflict; PUT flips it via the official `codex features` CLI and RESYNCS
|
|
885
994
|
// the catalog so multi-agent surface metadata stays fresh. The catalog build
|
|
@@ -1375,18 +1484,18 @@ export async function handleManagementAPI(req: Request, url: URL, config: OcxCon
|
|
|
1375
1484
|
}
|
|
1376
1485
|
}
|
|
1377
1486
|
// addAccount / reauth forces a fresh browser identity (skips local-CLI token import).
|
|
1378
|
-
const { url: authUrl, instructions } = await startLoginFlow(provider, {
|
|
1487
|
+
const { url: authUrl, instructions, deviceCode } = await startLoginFlow(provider, {
|
|
1379
1488
|
forceLogin: body.addAccount === true || reauth,
|
|
1380
1489
|
...(accountId ? { reauthAccountId: accountId } : {}),
|
|
1381
1490
|
});
|
|
1382
1491
|
upsertOAuthProvider(config, provider); // mutate LIVE config — routing sees it without restart
|
|
1383
|
-
if (authUrl) {
|
|
1492
|
+
if (authUrl && !deviceCode) {
|
|
1384
1493
|
// Open the browser server-side (the proxy runs on the user's machine) — the GUI's
|
|
1385
1494
|
// window.open is popup-blocked because it runs after an await, not a direct click.
|
|
1386
1495
|
const { openUrl } = await import("../lib/open-url");
|
|
1387
1496
|
openUrl(authUrl);
|
|
1388
1497
|
}
|
|
1389
|
-
return jsonResponse({ url: authUrl, instructions });
|
|
1498
|
+
return jsonResponse({ url: authUrl, instructions, deviceCode });
|
|
1390
1499
|
} catch (err) {
|
|
1391
1500
|
return jsonResponse({ error: err instanceof Error ? err.message : String(err) }, 409);
|
|
1392
1501
|
}
|
|
@@ -1454,6 +1563,20 @@ export async function handleManagementAPI(req: Request, url: URL, config: OcxCon
|
|
|
1454
1563
|
clearProviderQuotaCache();
|
|
1455
1564
|
return jsonResponse({ ok: true, provider, activeAccountId: body.accountId });
|
|
1456
1565
|
}
|
|
1566
|
+
if (url.pathname === "/api/oauth/accounts/alias" && req.method === "PUT") {
|
|
1567
|
+
const body = await req.json().catch(() => ({})) as { provider?: unknown; accountId?: unknown; alias?: unknown };
|
|
1568
|
+
const provider = typeof body.provider === "string" ? body.provider.trim().toLowerCase() : "";
|
|
1569
|
+
const accountId = typeof body.accountId === "string" ? body.accountId.trim() : "";
|
|
1570
|
+
const alias = typeof body.alias === "string" ? body.alias.trim() : "";
|
|
1571
|
+
if (!isPublicOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400);
|
|
1572
|
+
if (!accountId) return jsonResponse({ error: "missing accountId" }, 400);
|
|
1573
|
+
if (typeof body.alias !== "string" || alias.length > 80 || /[\x00-\x1f\x7f]/.test(alias)) {
|
|
1574
|
+
return jsonResponse({ error: "alias must be at most 80 printable characters" }, 400);
|
|
1575
|
+
}
|
|
1576
|
+
const { setAccountAlias } = await import("../oauth/store");
|
|
1577
|
+
if (!(await setAccountAlias(provider, accountId, alias || undefined))) return jsonResponse({ error: "account not found" }, 404);
|
|
1578
|
+
return jsonResponse({ ok: true, provider, accountId, alias: alias || null });
|
|
1579
|
+
}
|
|
1457
1580
|
if (url.pathname === "/api/oauth/accounts" && req.method === "DELETE") {
|
|
1458
1581
|
const provider = (url.searchParams.get("provider") ?? "").trim().toLowerCase();
|
|
1459
1582
|
const id = url.searchParams.get("id") ?? "";
|
|
@@ -1507,6 +1630,20 @@ export async function handleManagementAPI(req: Request, url: URL, config: OcxCon
|
|
|
1507
1630
|
clearKeyCooldowns(name); // manual key management resets 429 cooldown state
|
|
1508
1631
|
return jsonResponse({ ok: true, name, activeId: body.id });
|
|
1509
1632
|
}
|
|
1633
|
+
if (url.pathname === "/api/providers/keys/alias" && req.method === "PUT") {
|
|
1634
|
+
const body = await req.json().catch(() => ({})) as { name?: unknown; id?: unknown; alias?: unknown };
|
|
1635
|
+
const name = typeof body.name === "string" ? body.name.trim() : "";
|
|
1636
|
+
const id = typeof body.id === "string" ? body.id.trim() : "";
|
|
1637
|
+
const alias = typeof body.alias === "string" ? body.alias.trim() : "";
|
|
1638
|
+
if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
|
|
1639
|
+
if (!id) return jsonResponse({ error: "missing id" }, 400);
|
|
1640
|
+
if (typeof body.alias !== "string" || alias.length > 80 || /[\x00-\x1f\x7f]/.test(alias)) {
|
|
1641
|
+
return jsonResponse({ error: "alias must be at most 80 printable characters" }, 400);
|
|
1642
|
+
}
|
|
1643
|
+
const { setProviderApiKeyLabel } = await import("../providers/api-keys");
|
|
1644
|
+
if (!setProviderApiKeyLabel(config, name, id, alias || undefined)) return jsonResponse({ error: "key not found" }, 404);
|
|
1645
|
+
return jsonResponse({ ok: true, name, id, alias: alias || null });
|
|
1646
|
+
}
|
|
1510
1647
|
if (url.pathname === "/api/providers/keys" && req.method === "DELETE") {
|
|
1511
1648
|
const name = (url.searchParams.get("name") ?? "").trim();
|
|
1512
1649
|
const id = url.searchParams.get("id") ?? "";
|
package/src/server/responses.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
} from "../config";
|
|
7
7
|
import { parseRequest } from "../responses/parser";
|
|
8
8
|
import { buildCompactV1Output, COMPACT_PROMPT, decodeCompactionSummary, extractCompactUserMessages } from "../responses/compaction";
|
|
9
|
-
import { FORWARD_HEADERS } from "../adapters/openai-responses";
|
|
9
|
+
import { FORWARD_HEADERS, sanitizeReasoningInputContent } from "../adapters/openai-responses";
|
|
10
10
|
import { expandPreviousResponseInput, previousResponseConversationId, rememberResponseState } from "../responses/state";
|
|
11
11
|
import { routeModel } from "../router";
|
|
12
12
|
import {
|
|
@@ -590,6 +590,15 @@ function createChildPassthroughCallbackGate(options: HandleResponsesOptions) {
|
|
|
590
590
|
};
|
|
591
591
|
}
|
|
592
592
|
|
|
593
|
+
export function buildComboChildHeaders(parentHeaders: HeadersInit): Headers {
|
|
594
|
+
const childHeaders = new Headers(parentHeaders);
|
|
595
|
+
// Combo children re-serialize already-decoded JSON. Keeping transport metadata from
|
|
596
|
+
// the parent would make the child decoder treat plain JSON as compressed bytes.
|
|
597
|
+
childHeaders.delete("content-length");
|
|
598
|
+
childHeaders.delete("content-encoding");
|
|
599
|
+
return childHeaders;
|
|
600
|
+
}
|
|
601
|
+
|
|
593
602
|
async function handleComboResponses(
|
|
594
603
|
req: Request,
|
|
595
604
|
rawBody: unknown,
|
|
@@ -630,8 +639,7 @@ async function handleComboResponses(
|
|
|
630
639
|
comboDefaultEffort(config, comboId),
|
|
631
640
|
supportedLadderFor({ provider: targetRoute.provider, modelId: targetRoute.modelId }),
|
|
632
641
|
);
|
|
633
|
-
const childHeaders =
|
|
634
|
-
childHeaders.delete("content-length");
|
|
642
|
+
const childHeaders = buildComboChildHeaders(req.headers);
|
|
635
643
|
const childRequest = new Request(req.url, {
|
|
636
644
|
method: req.method,
|
|
637
645
|
headers: childHeaders,
|
|
@@ -1787,7 +1795,11 @@ export async function handleResponsesCompact(
|
|
|
1787
1795
|
}
|
|
1788
1796
|
const base = (compactProvider.baseUrl ?? "").replace(/\/$/, "");
|
|
1789
1797
|
if (compactProvider.apiKey) headers.set("authorization", `Bearer ${resolveEnvValue(compactProvider.apiKey)}`);
|
|
1790
|
-
const { reasoning: _reasoning, ...
|
|
1798
|
+
const { reasoning: _reasoning, ...compactBodyRaw } = raw as typeof raw & { reasoning?: unknown };
|
|
1799
|
+
// The regular /v1/responses path applies sanitizeReasoningInputContent via the adapter's
|
|
1800
|
+
// buildRequest, but the compact endpoint forwards directly. Apply the same sanitizer here
|
|
1801
|
+
// so routed-model reasoning items (reasoning_text content) don't 400 the ChatGPT backend.
|
|
1802
|
+
const compactBody = sanitizeReasoningInputContent(compactBodyRaw) as typeof compactBodyRaw;
|
|
1791
1803
|
const compactUrl = `${base}/responses/compact`;
|
|
1792
1804
|
const compactThreadId = req.headers.get("x-codex-parent-thread-id");
|
|
1793
1805
|
const connectMs = config.connectTimeoutMs ?? 200_000;
|