@bitkyc08/opencodex 2.7.23 → 2.7.24
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.ko.md +37 -7
- package/README.md +52 -11
- package/README.zh-CN.md +36 -7
- package/bin/ocx.mjs +5 -3
- package/gui/dist/assets/index-BzhyTAco.js +40 -0
- package/gui/dist/assets/index-Dq3eZ1cU.css +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/opencode.svg +1 -1
- package/package.json +5 -2
- package/src/adapters/anthropic-image-normalize.ts +70 -29
- package/src/adapters/cursor/transport-retry.ts +20 -1
- package/src/adapters/run-turn-queue.ts +40 -0
- package/src/codex/auth-api.ts +10 -1
- package/src/codex/auth-context.ts +33 -7
- package/src/codex/catalog.ts +357 -23
- package/src/codex/routing.ts +10 -4
- package/src/combos/failover.ts +102 -0
- package/src/combos/index.ts +37 -0
- package/src/combos/request.ts +31 -0
- package/src/combos/resolve.ts +171 -0
- package/src/combos/types.ts +203 -0
- package/src/config.ts +280 -11
- package/src/lib/errors.ts +86 -24
- package/src/lib/upstream-retry.ts +8 -4
- package/src/oauth/index.ts +7 -1
- package/src/oauth/key-providers.ts +2 -32
- package/src/oauth/login-cli.ts +4 -3
- package/src/oauth/token-guardian.ts +38 -3
- package/src/providers/derive.ts +27 -2
- package/src/providers/kiro-models.ts +8 -3
- package/src/providers/label.ts +3 -1
- package/src/providers/openai-sidecar.ts +94 -0
- package/src/providers/openai-tier-startup.ts +27 -0
- package/src/providers/openai-tiers.ts +283 -0
- package/src/providers/openai-virtual-models.ts +82 -0
- package/src/providers/quota.ts +344 -24
- package/src/providers/registry.ts +112 -20
- package/src/reasoning-effort.ts +12 -11
- package/src/router.ts +80 -36
- package/src/server/auth-cors.ts +85 -9
- package/src/server/images.ts +31 -75
- package/src/server/index.ts +45 -86
- package/src/server/management-api.ts +273 -21
- package/src/server/request-log.ts +221 -20
- package/src/server/responses.ts +594 -75
- package/src/server/search.ts +22 -37
- package/src/types.ts +49 -1
- package/src/update/index.ts +50 -6
- package/src/update/job.ts +21 -4
- package/src/usage/log.ts +124 -1
- package/src/usage/summary.ts +147 -56
- package/src/vision/index.ts +20 -19
- package/src/web-search/index.ts +15 -17
- package/gui/dist/assets/index-Bk_GgFrh.css +0 -1
- package/gui/dist/assets/index-DQjt6Hly.js +0 -40
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OcxProviderConfig } from "../types";
|
|
1
|
+
import type { CodexAccountMode, OcxProviderConfig } from "../types";
|
|
2
2
|
import { KIRO_MODELS, KIRO_MODEL_CONTEXT_WINDOWS, KIRO_MODEL_REASONING_EFFORTS } from "./kiro-models";
|
|
3
3
|
import { ANTIGRAVITY_MODELS, ANTIGRAVITY_MODEL_CONTEXT_WINDOWS } from "./antigravity-models";
|
|
4
4
|
import {
|
|
@@ -18,10 +18,16 @@ export interface ProviderRegistryEntry {
|
|
|
18
18
|
adapter: string;
|
|
19
19
|
baseUrl: string;
|
|
20
20
|
authKind: ProviderAuthKind;
|
|
21
|
+
codexAccountMode?: CodexAccountMode;
|
|
21
22
|
/** OAuth preset may explicitly honor a persisted API-key billing mode. */
|
|
22
23
|
allowKeyAuthOverride?: boolean;
|
|
23
24
|
allowPrivateNetworkByDefault?: boolean;
|
|
24
25
|
keyOptional?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Free-tier pricing (no paid subscription required). Distinct from `keyOptional`:
|
|
28
|
+
* free tiers may still require an API key (e.g. NVIDIA NIM free credits).
|
|
29
|
+
*/
|
|
30
|
+
freeTier?: boolean;
|
|
25
31
|
allowBaseUrlOverride?: boolean;
|
|
26
32
|
/** Static headers merged into every upstream request for this provider. */
|
|
27
33
|
staticHeaders?: Record<string, string>;
|
|
@@ -38,6 +44,7 @@ export interface ProviderRegistryEntry {
|
|
|
38
44
|
modelInputModalities?: Record<string, string[]>;
|
|
39
45
|
reasoningEfforts?: string[];
|
|
40
46
|
modelReasoningEfforts?: Record<string, string[]>;
|
|
47
|
+
modelDefaultReasoningEfforts?: Record<string, string>;
|
|
41
48
|
reasoningEffortMap?: Record<string, string>;
|
|
42
49
|
modelReasoningEffortMap?: Record<string, Record<string, string>>;
|
|
43
50
|
noVisionModels?: string[];
|
|
@@ -53,6 +60,8 @@ export interface ProviderRegistryEntry {
|
|
|
53
60
|
thinkingBudgetModels?: string[];
|
|
54
61
|
escapeBuiltinToolNames?: boolean;
|
|
55
62
|
oauthId?: string;
|
|
63
|
+
virtualModels?: Record<string, { wireModelId: string; reasoningMode: "pro" }>;
|
|
64
|
+
modelMaxInputTokens?: Record<string, number>;
|
|
56
65
|
jawcodeBundle?: string;
|
|
57
66
|
extraMetadataAliases?: string[];
|
|
58
67
|
metadataModelIdNormalize?: MetadataModelIdNormalize;
|
|
@@ -63,9 +72,10 @@ export interface ProviderRegistryEntry {
|
|
|
63
72
|
|
|
64
73
|
export type ProviderConfigSeed = Pick<
|
|
65
74
|
OcxProviderConfig,
|
|
66
|
-
"adapter" | "baseUrl" | "authMode" | "keyOptional" | "modelSuffixBracketStrip" | "defaultModel" | "models"
|
|
75
|
+
"adapter" | "baseUrl" | "authMode" | "keyOptional" | "freeTier" | "modelSuffixBracketStrip" | "defaultModel" | "models"
|
|
67
76
|
| "liveModels" | "contextWindow" | "modelContextWindows" | "modelInputModalities"
|
|
68
|
-
| "
|
|
77
|
+
| "modelMaxInputTokens"
|
|
78
|
+
| "reasoningEfforts" | "modelReasoningEfforts" | "modelDefaultReasoningEfforts" | "reasoningEffortMap" | "modelReasoningEffortMap"
|
|
69
79
|
| "noVisionModels" | "noReasoningModels" | "noTemperatureModels" | "noTopPModels" | "noPenaltyModels"
|
|
70
80
|
| "autoToolChoiceOnlyModels" | "preserveReasoningContentModels" | "thinkingToggleModels" | "thinkingBudgetModels" | "escapeBuiltinToolNames"
|
|
71
81
|
| "googleMode" | "project" | "location" | "headers"
|
|
@@ -92,13 +102,29 @@ const MINIMAX_MODELS = [
|
|
|
92
102
|
const MINIMAX_MODEL_CONTEXT_WINDOWS: Record<string, number> = Object.fromEntries(
|
|
93
103
|
MINIMAX_MODELS.map(id => [id, id === "MiniMax-M3" ? 1_000_000 : 204_800]),
|
|
94
104
|
);
|
|
95
|
-
const OPENAI_GPT56_MODELS = ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"];
|
|
96
|
-
const
|
|
105
|
+
const OPENAI_GPT56_MODELS = ["gpt-5.6", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"];
|
|
106
|
+
const OPENAI_GPT56_PRO_MODELS = ["gpt-5.6-sol-pro", "gpt-5.6-terra-pro", "gpt-5.6-luna-pro"];
|
|
107
|
+
const OPENAI_API_GPT56_CONTEXT_WINDOW = 1_050_000;
|
|
108
|
+
const OPENAI_CODEX_GPT56_CONTEXT_WINDOW = 372_000;
|
|
97
109
|
const OPENAI_GPT56_CONTEXT_WINDOWS = {
|
|
98
|
-
"gpt-5.6-sol":
|
|
99
|
-
"gpt-5.6-terra":
|
|
100
|
-
"gpt-5.6-luna":
|
|
110
|
+
"gpt-5.6-sol": OPENAI_CODEX_GPT56_CONTEXT_WINDOW,
|
|
111
|
+
"gpt-5.6-terra": OPENAI_CODEX_GPT56_CONTEXT_WINDOW,
|
|
112
|
+
"gpt-5.6-luna": OPENAI_CODEX_GPT56_CONTEXT_WINDOW,
|
|
101
113
|
};
|
|
114
|
+
const OPENAI_API_GPT56_CONTEXT_WINDOWS: Record<string, number> = {
|
|
115
|
+
...Object.fromEntries([...OPENAI_GPT56_MODELS, ...OPENAI_GPT56_PRO_MODELS].map(id => [id, OPENAI_API_GPT56_CONTEXT_WINDOW])),
|
|
116
|
+
"gpt-5.5": OPENAI_API_GPT56_CONTEXT_WINDOW,
|
|
117
|
+
};
|
|
118
|
+
const OPENAI_API_GPT56_MAX_INPUT_TOKENS: Record<string, number> = {
|
|
119
|
+
...Object.fromEntries([...OPENAI_GPT56_MODELS, ...OPENAI_GPT56_PRO_MODELS].map(id => [id, 922_000])),
|
|
120
|
+
"gpt-5.5": 922_000,
|
|
121
|
+
};
|
|
122
|
+
const OPENAI_API_GPT56_VIRTUAL_MODELS: Record<string, { wireModelId: string; reasoningMode: "pro" }> = {
|
|
123
|
+
"gpt-5.6-sol-pro": { wireModelId: "gpt-5.6-sol", reasoningMode: "pro" },
|
|
124
|
+
"gpt-5.6-terra-pro": { wireModelId: "gpt-5.6-terra", reasoningMode: "pro" },
|
|
125
|
+
"gpt-5.6-luna-pro": { wireModelId: "gpt-5.6-luna", reasoningMode: "pro" },
|
|
126
|
+
};
|
|
127
|
+
const OPENAI_API_GPT56_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
102
128
|
const OPENROUTER_GPT56_MODELS = OPENAI_GPT56_MODELS.map(id => `openai/${id}`);
|
|
103
129
|
// OpenRouter's live /endpoints routes report 1,050,000; keep this separate from the
|
|
104
130
|
// unverified OpenAI API-key seed. Evidence: devlog/_plan/260710_provider_hardening/003_research_aggregators.md.
|
|
@@ -161,8 +187,23 @@ const KIMI_CODING_MODELS = [...KIMI_CODING_K3_MODELS, ...KIMI_LEGACY_API_MODELS,
|
|
|
161
187
|
const KIMI_THINKING_MODELS = KIMI_CODING_MODELS;
|
|
162
188
|
const KIMI_CODING_NO_REASONING_MODELS = KIMI_CODING_MODELS.filter(id => !KIMI_CODING_K3_MODELS.includes(id));
|
|
163
189
|
const KIMI_API_NO_REASONING_MODELS = KIMI_API_MODELS.filter(id => id !== "kimi-k3");
|
|
190
|
+
const KIMI_CODING_K3_REASONING_EFFORTS = ["low", "high", "max"];
|
|
191
|
+
const KIMI_CODING_K3_REASONING_EFFORT_MAP: Record<string, string> = {
|
|
192
|
+
none: "none",
|
|
193
|
+
low: "low",
|
|
194
|
+
medium: "high",
|
|
195
|
+
high: "high",
|
|
196
|
+
xhigh: "max",
|
|
197
|
+
max: "max",
|
|
198
|
+
};
|
|
164
199
|
const KIMI_CODING_REASONING_EFFORTS = Object.fromEntries(
|
|
165
|
-
KIMI_CODING_MODELS.map(id => [id, KIMI_CODING_K3_MODELS.includes(id) ?
|
|
200
|
+
KIMI_CODING_MODELS.map(id => [id, KIMI_CODING_K3_MODELS.includes(id) ? KIMI_CODING_K3_REASONING_EFFORTS : []]),
|
|
201
|
+
);
|
|
202
|
+
const KIMI_CODING_DEFAULT_REASONING_EFFORTS = Object.fromEntries(
|
|
203
|
+
KIMI_CODING_K3_MODELS.map(id => [id, "max"]),
|
|
204
|
+
);
|
|
205
|
+
const KIMI_CODING_REASONING_EFFORT_MAPS = Object.fromEntries(
|
|
206
|
+
KIMI_CODING_K3_MODELS.map(id => [id, KIMI_CODING_K3_REASONING_EFFORT_MAP]),
|
|
166
207
|
);
|
|
167
208
|
const KIMI_API_REASONING_EFFORTS = Object.fromEntries(
|
|
168
209
|
KIMI_API_MODELS.map(id => [id, id === "kimi-k3" ? ["max"] : []]),
|
|
@@ -221,12 +262,13 @@ const UMANS_MODEL_INPUT_MODALITIES: Record<string, string[]> = Object.fromEntrie
|
|
|
221
262
|
export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
222
263
|
{
|
|
223
264
|
id: "openai",
|
|
224
|
-
label: "OpenAI (
|
|
265
|
+
label: "OpenAI (Codex login)",
|
|
225
266
|
adapter: "openai-responses",
|
|
226
267
|
baseUrl: "https://chatgpt.com/backend-api/codex",
|
|
227
268
|
authKind: "forward",
|
|
269
|
+
codexAccountMode: "pool",
|
|
228
270
|
featured: true,
|
|
229
|
-
note: "
|
|
271
|
+
note: "Codex login account pool (default) or Direct main-account mode via codexAccountMode",
|
|
230
272
|
},
|
|
231
273
|
{
|
|
232
274
|
id: "cursor",
|
|
@@ -270,20 +312,21 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
270
312
|
// 260709 refresh: lineup + metadata from official docs.x.ai (grok-4.5 announced 07-08);
|
|
271
313
|
// grok-composer-2.5-fast kept as account-verified (absent from public docs). Evidence:
|
|
272
314
|
// devlog/model_update/260709_model_refresh/001_xai_lineup.md.
|
|
273
|
-
|
|
315
|
+
// grok-4.20-multi-agent-0309 is intentionally absent: the OAuth chat-completions
|
|
316
|
+
// transport returns 400 ("Multi Agent requests are not allowed on chat completions").
|
|
317
|
+
models: ["grok-4.5", "grok-4.3", "grok-4.20-0309-reasoning", "grok-4.20-0309-non-reasoning", "grok-build-0.1", "grok-composer-2.5-fast"],
|
|
274
318
|
defaultModel: "grok-4.5",
|
|
275
319
|
noReasoningModels: ["grok-4.20-0309-non-reasoning", "grok-build-0.1", "grok-composer-2.5-fast"],
|
|
276
320
|
// Replay assistant reasoning_content for grok reasoning models: xAI documents dropped
|
|
277
321
|
// reasoning_content as the top cause of prompt-cache misses on multi-turn conversations
|
|
278
322
|
// (docs.x.ai prompt-caching/multi-turn, verified 2026-07-13 — devlog/_plan/260713_grok_caching).
|
|
279
323
|
// Models that never emit reasoning simply have no thinking parts to replay (no-op).
|
|
280
|
-
preserveReasoningContentModels: ["grok-4.5", "grok-4.3", "grok-4.20-
|
|
324
|
+
preserveReasoningContentModels: ["grok-4.5", "grok-4.3", "grok-4.20-0309-reasoning"],
|
|
281
325
|
// grok-4.5 reasoning is always-on with low/medium/high control (no off tier upstream).
|
|
282
326
|
modelReasoningEfforts: { "grok-4.5": ["low", "medium", "high"] },
|
|
283
327
|
modelContextWindows: {
|
|
284
328
|
"grok-4.5": 500_000,
|
|
285
329
|
"grok-4.3": 1_000_000,
|
|
286
|
-
"grok-4.20-multi-agent-0309": 1_000_000,
|
|
287
330
|
"grok-4.20-0309-reasoning": 1_000_000,
|
|
288
331
|
"grok-4.20-0309-non-reasoning": 1_000_000,
|
|
289
332
|
"grok-build-0.1": 256_000,
|
|
@@ -335,9 +378,11 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
335
378
|
defaultModel: "kimi-k2.7-code",
|
|
336
379
|
modelContextWindows: KIMI_CODING_MODEL_CONTEXT_WINDOWS,
|
|
337
380
|
modelInputModalities: KIMI_CODING_MODEL_INPUT_MODALITIES,
|
|
338
|
-
// K3 accepts
|
|
381
|
+
// K3 accepts low/high/max; Codex aliases are normalized by the model-scoped wire map.
|
|
339
382
|
noReasoningModels: KIMI_CODING_NO_REASONING_MODELS,
|
|
340
383
|
modelReasoningEfforts: KIMI_CODING_REASONING_EFFORTS,
|
|
384
|
+
modelDefaultReasoningEfforts: KIMI_CODING_DEFAULT_REASONING_EFFORTS,
|
|
385
|
+
modelReasoningEffortMap: KIMI_CODING_REASONING_EFFORT_MAPS,
|
|
341
386
|
noTemperatureModels: KIMI_LOCKED_PARAMETER_MODELS,
|
|
342
387
|
noTopPModels: KIMI_LOCKED_PARAMETER_MODELS,
|
|
343
388
|
noPenaltyModels: KIMI_LOCKED_PARAMETER_MODELS,
|
|
@@ -354,11 +399,35 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
354
399
|
note: "Import-first: reuses your installed kiro-cli login (no browser). Experimental third-party harness — see Kiro ToS.",
|
|
355
400
|
models: KIRO_MODELS,
|
|
356
401
|
defaultModel: "kiro-auto",
|
|
402
|
+
// Kiro speaks CodeWhisperer wire, not OpenAI-style GET /models. Keep the static
|
|
403
|
+
// catalog authoritative so a spurious 2xx from runtime.../models cannot drop seeded ids
|
|
404
|
+
// (e.g. newly listed GPT-5.6 tiers) via live-discovery reconciliation.
|
|
405
|
+
liveModels: false,
|
|
357
406
|
// Per-model context metadata is maintained next to the Kiro model list.
|
|
358
407
|
modelContextWindows: KIRO_MODEL_CONTEXT_WINDOWS,
|
|
359
408
|
modelReasoningEfforts: KIRO_MODEL_REASONING_EFFORTS,
|
|
360
409
|
},
|
|
361
|
-
{
|
|
410
|
+
{
|
|
411
|
+
id: "openai-apikey",
|
|
412
|
+
label: "OpenAI API",
|
|
413
|
+
adapter: "openai-responses",
|
|
414
|
+
baseUrl: "https://api.openai.com/v1",
|
|
415
|
+
authKind: "key",
|
|
416
|
+
featured: true,
|
|
417
|
+
dashboardUrl: "https://platform.openai.com/api-keys",
|
|
418
|
+
defaultModel: "gpt-5.5",
|
|
419
|
+
models: ["gpt-5.5", ...OPENAI_GPT56_MODELS, ...OPENAI_GPT56_PRO_MODELS],
|
|
420
|
+
liveModels: true,
|
|
421
|
+
modelContextWindows: OPENAI_API_GPT56_CONTEXT_WINDOWS,
|
|
422
|
+
modelMaxInputTokens: OPENAI_API_GPT56_MAX_INPUT_TOKENS,
|
|
423
|
+
modelInputModalities: Object.fromEntries(
|
|
424
|
+
["gpt-5.5", ...OPENAI_GPT56_MODELS, ...OPENAI_GPT56_PRO_MODELS].map(id => [id, ["text", "image"]]),
|
|
425
|
+
),
|
|
426
|
+
modelReasoningEfforts: Object.fromEntries(
|
|
427
|
+
[...OPENAI_GPT56_MODELS, ...OPENAI_GPT56_PRO_MODELS].map(id => [id, OPENAI_API_GPT56_REASONING_EFFORTS]),
|
|
428
|
+
),
|
|
429
|
+
virtualModels: OPENAI_API_GPT56_VIRTUAL_MODELS,
|
|
430
|
+
},
|
|
362
431
|
{
|
|
363
432
|
id: "umans",
|
|
364
433
|
label: "Umans AI Coding Plan",
|
|
@@ -387,17 +456,22 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
387
456
|
id: "opencode-go", label: "opencode go", adapter: "openai-chat", baseUrl: "https://opencode.ai/zen/go/v1",
|
|
388
457
|
authKind: "key", featured: true, dashboardUrl: "https://opencode.ai/auth", defaultModel: "kimi-k2.7-code",
|
|
389
458
|
jawcodeBundle: "opencode-go", note: "GLM, DeepSeek, Kimi, Qwen, MiMo…",
|
|
459
|
+
modelContextWindows: { "kimi-k3": KIMI_K3_STANDARD_CONTEXT_WINDOW },
|
|
460
|
+
modelInputModalities: { "kimi-k3": ["text", "image"] },
|
|
390
461
|
modelReasoningEfforts: {
|
|
391
462
|
"glm-5.2": ZAI_GLM_52_REASONING_EFFORTS,
|
|
463
|
+
"kimi-k3": KIMI_CODING_K3_REASONING_EFFORTS,
|
|
392
464
|
"kimi-k2.7-code": [],
|
|
393
465
|
"kimi-k2.7-code-highspeed": [],
|
|
394
466
|
...Object.fromEntries(OPENCODE_GO_THINKING_TOGGLE_MODELS.map(id => [id, THINKING_TOGGLE_EFFORTS])),
|
|
395
467
|
...Object.fromEntries(OPENCODE_GO_THINKING_BUDGET_MODELS.map(id => [id, THINKING_BUDGET_EFFORTS])),
|
|
396
468
|
...Object.fromEntries(DEEPSEEK_THINKING_MODELS.map(id => [id, DEEPSEEK_THINKING_EFFORTS])),
|
|
397
469
|
},
|
|
470
|
+
modelDefaultReasoningEfforts: { "kimi-k3": "max" },
|
|
398
471
|
// glm-5.2 uses identity labels now that `max` is a native Codex level (no alias map);
|
|
399
472
|
// the thinking-toggle map is a REAL wire alias (effort -> enabled/disabled) and stays.
|
|
400
473
|
modelReasoningEffortMap: {
|
|
474
|
+
"kimi-k3": KIMI_CODING_K3_REASONING_EFFORT_MAP,
|
|
401
475
|
...Object.fromEntries(OPENCODE_GO_THINKING_TOGGLE_MODELS.map(id => [id, THINKING_TOGGLE_MAP])),
|
|
402
476
|
...Object.fromEntries(DEEPSEEK_THINKING_MODELS.map(id => [id, DEEPSEEK_THINKING_REASONING_MAP])),
|
|
403
477
|
},
|
|
@@ -414,12 +488,12 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
414
488
|
"minimax-m2.5", "minimax-m2.7",
|
|
415
489
|
"qwen3.7-max",
|
|
416
490
|
],
|
|
417
|
-
noTemperatureModels: ["kimi-k2.7-code", "kimi-k2.7-code-highspeed"],
|
|
418
|
-
noTopPModels: ["kimi-k2.7-code", "kimi-k2.7-code-highspeed"],
|
|
419
|
-
noPenaltyModels: ["kimi-k2.7-code", "kimi-k2.7-code-highspeed"],
|
|
491
|
+
noTemperatureModels: ["kimi-k3", "kimi-k2.7-code", "kimi-k2.7-code-highspeed"],
|
|
492
|
+
noTopPModels: ["kimi-k3", "kimi-k2.7-code", "kimi-k2.7-code-highspeed"],
|
|
493
|
+
noPenaltyModels: ["kimi-k3", "kimi-k2.7-code", "kimi-k2.7-code-highspeed"],
|
|
420
494
|
autoToolChoiceOnlyModels: ["kimi-k2.7-code", "kimi-k2.7-code-highspeed"],
|
|
421
495
|
// Issue #78: DeepSeek V4 thinking mode requires reasoning_content replay on tool-call turns.
|
|
422
|
-
preserveReasoningContentModels: ["glm-5.2", "kimi-k2.7-code", "kimi-k2.7-code-highspeed", ...DEEPSEEK_THINKING_MODELS],
|
|
496
|
+
preserveReasoningContentModels: ["glm-5.2", "kimi-k3", "kimi-k2.7-code", "kimi-k2.7-code-highspeed", ...DEEPSEEK_THINKING_MODELS],
|
|
423
497
|
},
|
|
424
498
|
{
|
|
425
499
|
id: "neuralwatt",
|
|
@@ -544,10 +618,13 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
544
618
|
// its working reasoning_effort. Future kimi ids must be appended individually.
|
|
545
619
|
{
|
|
546
620
|
id: "nvidia", label: "NVIDIA NIM", baseUrl: "https://integrate.api.nvidia.com/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://build.nvidia.com",
|
|
621
|
+
// Free pricing, but an API key is still required (free key from build.nvidia.com).
|
|
622
|
+
freeTier: true,
|
|
547
623
|
parallelToolCalls: false,
|
|
548
624
|
noReasoningModels: NVIDIA_NIM_KIMI_MODELS,
|
|
549
625
|
modelReasoningEfforts: Object.fromEntries(NVIDIA_NIM_KIMI_MODELS.map(id => [id, []])),
|
|
550
626
|
preserveReasoningContentModels: NVIDIA_NIM_KIMI_THINKING_MODELS,
|
|
627
|
+
note: "Free tier on NVIDIA NIM — API key still required (get a free key at build.nvidia.com).",
|
|
551
628
|
},
|
|
552
629
|
{ id: "venice", label: "Venice", baseUrl: "https://api.venice.ai/api/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://venice.ai/settings/api" },
|
|
553
630
|
// 260710 GLM-5.2 context and path-specific ids: Tier-2 evidence in
|
|
@@ -628,6 +705,8 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
628
705
|
modelInputModalities: KIMI_CODING_MODEL_INPUT_MODALITIES,
|
|
629
706
|
noReasoningModels: KIMI_CODING_NO_REASONING_MODELS,
|
|
630
707
|
modelReasoningEfforts: KIMI_CODING_REASONING_EFFORTS,
|
|
708
|
+
modelDefaultReasoningEfforts: KIMI_CODING_DEFAULT_REASONING_EFFORTS,
|
|
709
|
+
modelReasoningEffortMap: KIMI_CODING_REASONING_EFFORT_MAPS,
|
|
631
710
|
noTemperatureModels: KIMI_LOCKED_PARAMETER_MODELS,
|
|
632
711
|
noTopPModels: KIMI_LOCKED_PARAMETER_MODELS,
|
|
633
712
|
noPenaltyModels: KIMI_LOCKED_PARAMETER_MODELS,
|
|
@@ -682,6 +761,19 @@ export function getProviderRegistryEntry(id: string): ProviderRegistryEntry | un
|
|
|
682
761
|
return PROVIDER_REGISTRY.find(entry => entry.id === id);
|
|
683
762
|
}
|
|
684
763
|
|
|
764
|
+
/**
|
|
765
|
+
* Effective Codex account mode for a provider. For canonical `openai`, a valid persisted
|
|
766
|
+
* `codexAccountMode` on the provider config wins and a missing/invalid value defaults to
|
|
767
|
+
* `"pool"`. Other providers keep registry-only metadata (there is no mode for `openai-apikey`).
|
|
768
|
+
*/
|
|
769
|
+
export function providerCodexAccountMode(id: string, provider?: OcxProviderConfig): CodexAccountMode | undefined {
|
|
770
|
+
const registryMode = getProviderRegistryEntry(id)?.codexAccountMode;
|
|
771
|
+
if (id !== "openai") return registryMode;
|
|
772
|
+
const persisted = provider?.codexAccountMode;
|
|
773
|
+
if (persisted === "pool" || persisted === "direct") return persisted;
|
|
774
|
+
return registryMode ?? "pool";
|
|
775
|
+
}
|
|
776
|
+
|
|
685
777
|
/**
|
|
686
778
|
* Effective Google wire mode for a provider: config value, else registry backfill (a saved
|
|
687
779
|
* key-login config may omit `googleMode` — mirrors the router's backfill), else "ai-studio"
|
package/src/reasoning-effort.ts
CHANGED
|
@@ -58,23 +58,24 @@ export function sanitizeCodexReasoningEfforts(efforts: readonly string[] | undef
|
|
|
58
58
|
export function configuredReasoningEfforts(provider: OcxProviderConfig, modelId: string): string[] | undefined {
|
|
59
59
|
if (modelInList(provider.noReasoningModels, modelId)) return [];
|
|
60
60
|
const modelEfforts = modelRecordValue(provider.modelReasoningEfforts, modelId);
|
|
61
|
-
if (modelEfforts !== undefined) return
|
|
62
|
-
if (provider.reasoningEfforts !== undefined) return
|
|
61
|
+
if (modelEfforts !== undefined) return healMappedTiers(provider, modelId, sanitizeCodexReasoningEfforts(modelEfforts) ?? []);
|
|
62
|
+
if (provider.reasoningEfforts !== undefined) return healMappedTiers(provider, modelId, sanitizeCodexReasoningEfforts(provider.reasoningEfforts) ?? []);
|
|
63
63
|
return undefined;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
|
-
* Stale-ladder self-heal:
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* maps (xhigh -> "enabled") never match, so binary-toggle models stay two-step.
|
|
67
|
+
* Stale-ladder self-heal: a registry wire map is authoritative evidence of the upstream tiers
|
|
68
|
+
* it can emit. Merge Codex-native map values into an older persisted ladder so newly documented
|
|
69
|
+
* tiers appear without rewriting the user's config. Non-Codex values such as enabled/disabled
|
|
70
|
+
* and Kimi's none sentinel are ignored here; they remain request-only wire aliases.
|
|
72
71
|
*/
|
|
73
|
-
function
|
|
74
|
-
if (efforts.
|
|
72
|
+
function healMappedTiers(provider: OcxProviderConfig, modelId: string, efforts: string[]): string[] {
|
|
73
|
+
if (efforts.length === 0) return efforts;
|
|
75
74
|
const wireMap = reasoningEffortMapFor(provider, modelId);
|
|
76
|
-
if (wireMap
|
|
77
|
-
|
|
75
|
+
if (!wireMap) return efforts;
|
|
76
|
+
const mappedTiers = Object.values(wireMap).filter(isCodexReasoningEffort);
|
|
77
|
+
if (mappedTiers.length === 0) return efforts;
|
|
78
|
+
return sanitizeCodexReasoningEfforts([...efforts, ...mappedTiers]) ?? efforts;
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
function requestToCodexEffort(requested: string): string | undefined {
|
package/src/router.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import type { OcxConfig, OcxProviderConfig } from "./types";
|
|
1
|
+
import type { CodexAccountMode, OcxConfig, OcxProviderConfig } from "./types";
|
|
2
|
+
import { COMBO_NAMESPACE, tryPickComboModel, type ComboPick } from "./combos";
|
|
2
3
|
import { hasOwnProvider, resolveEnvValue } from "./config";
|
|
3
4
|
import { assertProviderDestinationAllowed } from "./lib/destination-policy";
|
|
4
|
-
import { PROVIDER_REGISTRY } from "./providers/registry";
|
|
5
|
+
import { PROVIDER_REGISTRY, providerCodexAccountMode } from "./providers/registry";
|
|
6
|
+
import { LEGACY_CHATGPT_PROVIDER_ID, LEGACY_OPENAI_MULTI_PROVIDER_ID, OPENAI_API_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID } from "./providers/openai-tiers";
|
|
5
7
|
|
|
6
|
-
interface RouteResult {
|
|
8
|
+
export interface RouteResult {
|
|
7
9
|
providerName: string;
|
|
8
10
|
provider: OcxProviderConfig;
|
|
9
11
|
modelId: string;
|
|
12
|
+
codexAccountMode?: CodexAccountMode;
|
|
13
|
+
combo?: ComboPick;
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
const MODEL_PROVIDER_PATTERNS: Array<{ providerNames: string[]; prefixes: string[] }> = [
|
|
@@ -16,12 +20,6 @@ const MODEL_PROVIDER_PATTERNS: Array<{ providerNames: string[]; prefixes: string
|
|
|
16
20
|
"claude-", "claude-sonnet-", "claude-opus-", "claude-haiku-",
|
|
17
21
|
],
|
|
18
22
|
},
|
|
19
|
-
{
|
|
20
|
-
providerNames: ["openai", "chatgpt", "openai-apikey"],
|
|
21
|
-
prefixes: [
|
|
22
|
-
"gpt-", "o1-", "o3-", "o4-",
|
|
23
|
-
],
|
|
24
|
-
},
|
|
25
23
|
{
|
|
26
24
|
providerNames: ["groq"],
|
|
27
25
|
prefixes: [
|
|
@@ -67,6 +65,18 @@ function mergeRecordFill<T>(
|
|
|
67
65
|
return { ...(seed ?? {}), ...(user ?? {}) };
|
|
68
66
|
}
|
|
69
67
|
|
|
68
|
+
function mergePositiveNumberCaps(
|
|
69
|
+
seed: Record<string, number> | undefined,
|
|
70
|
+
user: Record<string, number> | undefined,
|
|
71
|
+
): Record<string, number> | undefined {
|
|
72
|
+
if (!seed && !user) return undefined;
|
|
73
|
+
const out = { ...(seed ?? {}) };
|
|
74
|
+
for (const [key, value] of Object.entries(user ?? {})) {
|
|
75
|
+
out[key] = typeof out[key] === "number" ? Math.min(out[key]!, value) : value;
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
|
|
70
80
|
function mergeStringArrayRecord(
|
|
71
81
|
seed: Record<string, string[]> | undefined,
|
|
72
82
|
user: Record<string, string[]> | undefined,
|
|
@@ -95,8 +105,14 @@ function routedProviderConfig(providerName: string, provider: OcxProviderConfig)
|
|
|
95
105
|
const reasoningEffortMap = mergeRecord(registryEntry.reasoningEffortMap, provider.reasoningEffortMap);
|
|
96
106
|
const modelReasoningEffortMap = mergeNestedRecord(registryEntry.modelReasoningEffortMap, provider.modelReasoningEffortMap);
|
|
97
107
|
const modelReasoningEfforts = mergeStringArrayRecord(registryEntry.modelReasoningEfforts, provider.modelReasoningEfforts);
|
|
98
|
-
const
|
|
108
|
+
const modelDefaultReasoningEfforts = mergeRecordFill(registryEntry.modelDefaultReasoningEfforts, provider.modelDefaultReasoningEfforts);
|
|
109
|
+
const modelContextWindows = providerName === OPENAI_API_PROVIDER_ID
|
|
110
|
+
? mergePositiveNumberCaps(registryEntry.modelContextWindows, provider.modelContextWindows)
|
|
111
|
+
: mergeRecordFill(registryEntry.modelContextWindows, provider.modelContextWindows);
|
|
99
112
|
const modelInputModalities = mergeRecordFill(registryEntry.modelInputModalities, provider.modelInputModalities);
|
|
113
|
+
const modelMaxInputTokens = providerName === OPENAI_API_PROVIDER_ID
|
|
114
|
+
? mergePositiveNumberCaps(registryEntry.modelMaxInputTokens, provider.modelMaxInputTokens)
|
|
115
|
+
: mergeRecordFill(registryEntry.modelMaxInputTokens, provider.modelMaxInputTokens);
|
|
100
116
|
const noVisionModels = mergeStringArray(registryEntry.noVisionModels, provider.noVisionModels);
|
|
101
117
|
const noReasoningModels = mergeStringArray(registryEntry.noReasoningModels, provider.noReasoningModels);
|
|
102
118
|
const noTemperatureModels = mergeStringArray(registryEntry.noTemperatureModels, provider.noTemperatureModels);
|
|
@@ -140,7 +156,9 @@ function routedProviderConfig(providerName: string, provider: OcxProviderConfig)
|
|
|
140
156
|
...(provider.parallelToolCalls === undefined && registryEntry.parallelToolCalls !== undefined ? { parallelToolCalls: registryEntry.parallelToolCalls } : {}),
|
|
141
157
|
...(modelContextWindows ? { modelContextWindows } : {}),
|
|
142
158
|
...(modelInputModalities ? { modelInputModalities } : {}),
|
|
159
|
+
...(modelMaxInputTokens ? { modelMaxInputTokens } : {}),
|
|
143
160
|
...(modelReasoningEfforts ? { modelReasoningEfforts } : {}),
|
|
161
|
+
...(modelDefaultReasoningEfforts ? { modelDefaultReasoningEfforts } : {}),
|
|
144
162
|
...(reasoningEffortMap ? { reasoningEffortMap } : {}),
|
|
145
163
|
...(modelReasoningEffortMap ? { modelReasoningEffortMap } : {}),
|
|
146
164
|
...(noVisionModels ? { noVisionModels } : {}),
|
|
@@ -156,10 +174,44 @@ function routedProviderConfig(providerName: string, provider: OcxProviderConfig)
|
|
|
156
174
|
}
|
|
157
175
|
|
|
158
176
|
function activeProviderEntries(config: OcxConfig): [string, OcxProviderConfig][] {
|
|
159
|
-
return Object.entries(config.providers)
|
|
177
|
+
return Object.entries(config.providers)
|
|
178
|
+
.filter(([name, provider]) => name !== LEGACY_CHATGPT_PROVIDER_ID && provider.disabled !== true);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export class NoEnabledOpenAiProviderError extends Error {
|
|
182
|
+
constructor(modelId: string) {
|
|
183
|
+
super(`No enabled canonical OpenAI provider for model: ${modelId}`);
|
|
184
|
+
this.name = "NoEnabledOpenAiProviderError";
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function isBareOpenAiFamilyModel(modelId: string): boolean {
|
|
189
|
+
return !modelId.includes("/") && /^(?:gpt-|o1-|o3-|o4-)/.test(modelId);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function routeResult(providerName: string, provider: OcxProviderConfig, modelId: string): RouteResult {
|
|
193
|
+
const codexAccountMode = providerCodexAccountMode(providerName, provider);
|
|
194
|
+
return {
|
|
195
|
+
providerName,
|
|
196
|
+
provider: routedProviderConfig(providerName, provider),
|
|
197
|
+
modelId,
|
|
198
|
+
...(codexAccountMode ? { codexAccountMode } : {}),
|
|
199
|
+
};
|
|
160
200
|
}
|
|
161
201
|
|
|
162
202
|
export function routeModel(config: OcxConfig, modelId: string): RouteResult {
|
|
203
|
+
const preservePhysicalComboProvider =
|
|
204
|
+
hasOwnProvider(config.providers, COMBO_NAMESPACE)
|
|
205
|
+
&& Object.keys(config.combos ?? {}).length === 0;
|
|
206
|
+
if (!preservePhysicalComboProvider) {
|
|
207
|
+
const combo = tryPickComboModel(config, modelId);
|
|
208
|
+
if (combo) {
|
|
209
|
+
const concrete = `${combo.target.provider}/${combo.target.model}`;
|
|
210
|
+
const routed = routeModel(config, concrete);
|
|
211
|
+
return { ...routed, combo };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
163
215
|
// 0. Explicit "<provider>/<model>" namespace (e.g. "opencode-go/deepseek-v4-pro").
|
|
164
216
|
// Only triggers when the prefix matches a CONFIGURED provider, so genuine
|
|
165
217
|
// slash-containing model ids (e.g. "anthropic/claude-...") fall through when
|
|
@@ -167,24 +219,25 @@ export function routeModel(config: OcxConfig, modelId: string): RouteResult {
|
|
|
167
219
|
const slash = modelId.indexOf("/");
|
|
168
220
|
if (slash > 0) {
|
|
169
221
|
const provName = modelId.slice(0, slash);
|
|
222
|
+
if (provName === LEGACY_CHATGPT_PROVIDER_ID || provName === LEGACY_OPENAI_MULTI_PROVIDER_ID) {
|
|
223
|
+
throw new Error(`No provider configured for model: ${modelId}`);
|
|
224
|
+
}
|
|
170
225
|
if (hasOwnProvider(config.providers, provName)) {
|
|
171
226
|
const prov = config.providers[provName];
|
|
172
227
|
if (prov.disabled === true) throw new Error(`Provider is disabled: ${provName}`);
|
|
173
|
-
return
|
|
174
|
-
providerName: provName,
|
|
175
|
-
provider: routedProviderConfig(provName, prov),
|
|
176
|
-
modelId: modelId.slice(slash + 1),
|
|
177
|
-
};
|
|
228
|
+
return routeResult(provName, prov, modelId.slice(slash + 1));
|
|
178
229
|
}
|
|
179
230
|
}
|
|
180
231
|
|
|
232
|
+
if (isBareOpenAiFamilyModel(modelId)) {
|
|
233
|
+
const provider = config.providers[OPENAI_CODEX_PROVIDER_ID];
|
|
234
|
+
if (provider && provider.disabled !== true) return routeResult(OPENAI_CODEX_PROVIDER_ID, provider, modelId);
|
|
235
|
+
throw new NoEnabledOpenAiProviderError(modelId);
|
|
236
|
+
}
|
|
237
|
+
|
|
181
238
|
for (const [provName, prov] of activeProviderEntries(config)) {
|
|
182
239
|
if (prov.defaultModel === modelId) {
|
|
183
|
-
return
|
|
184
|
-
providerName: provName,
|
|
185
|
-
provider: routedProviderConfig(provName, prov),
|
|
186
|
-
modelId,
|
|
187
|
-
};
|
|
240
|
+
return routeResult(provName, prov, modelId);
|
|
188
241
|
}
|
|
189
242
|
}
|
|
190
243
|
|
|
@@ -193,22 +246,17 @@ export function routeModel(config: OcxConfig, modelId: string): RouteResult {
|
|
|
193
246
|
|
|
194
247
|
for (const [provName, prov] of activeProviderEntries(config)) {
|
|
195
248
|
if (prov.models && Array.isArray(prov.models) && (prov.models as string[]).includes(modelId)) {
|
|
196
|
-
return
|
|
197
|
-
providerName: provName,
|
|
198
|
-
provider: routedProviderConfig(provName, prov),
|
|
199
|
-
modelId,
|
|
200
|
-
};
|
|
249
|
+
return routeResult(provName, prov, modelId);
|
|
201
250
|
}
|
|
202
251
|
}
|
|
203
252
|
|
|
253
|
+
if (config.defaultProvider === LEGACY_CHATGPT_PROVIDER_ID) {
|
|
254
|
+
throw new Error(`No provider configured for model: ${modelId}`);
|
|
255
|
+
}
|
|
204
256
|
if (hasOwnProvider(config.providers, config.defaultProvider)) {
|
|
205
257
|
const defaultProv = config.providers[config.defaultProvider];
|
|
206
258
|
if (defaultProv.disabled === true) throw new Error(`Default provider is disabled: ${config.defaultProvider}`);
|
|
207
|
-
return
|
|
208
|
-
providerName: config.defaultProvider,
|
|
209
|
-
provider: routedProviderConfig(config.defaultProvider, defaultProv),
|
|
210
|
-
modelId,
|
|
211
|
-
};
|
|
259
|
+
return routeResult(config.defaultProvider, defaultProv, modelId);
|
|
212
260
|
}
|
|
213
261
|
|
|
214
262
|
throw new Error(`No provider configured for model: ${modelId}`);
|
|
@@ -222,11 +270,7 @@ function routeByKnownModelPattern(config: OcxConfig, modelId: string): RouteResu
|
|
|
222
270
|
);
|
|
223
271
|
if (matchingProvider) {
|
|
224
272
|
const [provName, prov] = matchingProvider;
|
|
225
|
-
return
|
|
226
|
-
providerName: provName,
|
|
227
|
-
provider: routedProviderConfig(provName, prov),
|
|
228
|
-
modelId,
|
|
229
|
-
};
|
|
273
|
+
return routeResult(provName, prov, modelId);
|
|
230
274
|
}
|
|
231
275
|
}
|
|
232
276
|
}
|
package/src/server/auth-cors.ts
CHANGED
|
@@ -2,11 +2,13 @@ import { timingSafeEqual } from "node:crypto";
|
|
|
2
2
|
import { formatErrorResponse } from "../bridge";
|
|
3
3
|
import {
|
|
4
4
|
codexAutoStartEnabled,
|
|
5
|
+
positiveIntegerRecordConfigError,
|
|
5
6
|
providerBaseUrlConfigError,
|
|
6
7
|
providerHeadersConfigError,
|
|
7
8
|
} from "../config";
|
|
8
9
|
import { providerDestinationConfigError } from "../lib/destination-policy";
|
|
9
|
-
import { getProviderRegistryEntry } from "../providers/registry";
|
|
10
|
+
import { getProviderRegistryEntry, providerCodexAccountMode } from "../providers/registry";
|
|
11
|
+
import { providerConfigSeed } from "../providers/derive";
|
|
10
12
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
11
13
|
|
|
12
14
|
let _corsOrigin = "http://localhost:10100";
|
|
@@ -139,6 +141,18 @@ export function isProxyAdmissionSecret(token: string, config: OcxConfig): boolea
|
|
|
139
141
|
return false;
|
|
140
142
|
}
|
|
141
143
|
|
|
144
|
+
export class ForwardAdmissionCredentialError extends Error {
|
|
145
|
+
constructor() {
|
|
146
|
+
super("OpenCodex admission credentials cannot be forwarded upstream");
|
|
147
|
+
this.name = "ForwardAdmissionCredentialError";
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function validateForwardAdmissionCredential(headers: Headers, config: OcxConfig): void {
|
|
152
|
+
const bearer = headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
|
|
153
|
+
if (bearer && isProxyAdmissionSecret(bearer, config)) throw new ForwardAdmissionCredentialError();
|
|
154
|
+
}
|
|
155
|
+
|
|
142
156
|
export function hasValidApiAuth(req: Request, config: OcxConfig): boolean {
|
|
143
157
|
if (!isApiAuthRequired(config)) return true;
|
|
144
158
|
const actual = req.headers.get("x-opencodex-api-key")?.trim()
|
|
@@ -155,18 +169,77 @@ export function requireApiAuth(req: Request, config: OcxConfig, kind: "managemen
|
|
|
155
169
|
return formatErrorResponse(401, "authentication_error", "opencodex API key required");
|
|
156
170
|
}
|
|
157
171
|
|
|
158
|
-
|
|
159
|
-
|
|
172
|
+
/**
|
|
173
|
+
* Admission for OpenAI Responses transports whose Authorization header belongs to
|
|
174
|
+
* Codex Direct. Remote binds must use the dedicated proxy header so the two bearer
|
|
175
|
+
* domains can never be confused.
|
|
176
|
+
*/
|
|
177
|
+
export function requireResponsesApiAuth(req: Request, config: OcxConfig): Response | null {
|
|
178
|
+
if (!isApiAuthRequired(config)) return null;
|
|
179
|
+
const actual = req.headers.get("x-opencodex-api-key")?.trim();
|
|
180
|
+
if (actual && isProxyAdmissionSecret(actual, config)) return null;
|
|
181
|
+
return formatErrorResponse(401, "authentication_error", "opencodex API key required");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const FORBIDDEN_PROVIDER_RUNTIME_FIELDS = [
|
|
185
|
+
"virtualModels", "codexAuthContext", "selectedForwardHeaders",
|
|
186
|
+
"sidecarOutcomeRecorder", "_codexAccountOverride", "_codexAccountRequired",
|
|
187
|
+
] as const;
|
|
188
|
+
|
|
189
|
+
function sameCanonicalProviderSeed(actual: Record<string, unknown>, expected: OcxProviderConfig): boolean {
|
|
190
|
+
const actualKeys = Object.keys(actual).sort();
|
|
191
|
+
const expectedKeys = Object.keys(expected).sort();
|
|
192
|
+
if (actualKeys.length !== expectedKeys.length || actualKeys.some((key, i) => key !== expectedKeys[i])) return false;
|
|
193
|
+
return actualKeys.every(key => JSON.stringify(actual[key]) === JSON.stringify((expected as unknown as Record<string, unknown>)[key]));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function providerManagementConfigError(name: unknown, provider: unknown): string | null {
|
|
197
|
+
if (typeof name !== "string" || !provider || typeof provider !== "object" || Array.isArray(provider)) {
|
|
198
|
+
return "provider must be a plain object";
|
|
199
|
+
}
|
|
200
|
+
const raw = provider as Record<string, unknown>;
|
|
201
|
+
for (const field of FORBIDDEN_PROVIDER_RUNTIME_FIELDS) {
|
|
202
|
+
if (Object.hasOwn(raw, field)) return `provider ${name} must not include runtime field "${field}"`;
|
|
203
|
+
}
|
|
204
|
+
if (name === "chatgpt") return "provider chatgpt is reserved for internal credential compatibility";
|
|
205
|
+
if (name === "openai-multi") return "provider openai-multi is reserved for legacy config migration";
|
|
206
|
+
if (name === "openai") {
|
|
207
|
+
const entry = getProviderRegistryEntry(name);
|
|
208
|
+
const seed = entry ? providerConfigSeed(entry) : undefined;
|
|
209
|
+
if (!Object.hasOwn(raw, "codexAccountMode") || (raw.codexAccountMode !== "pool" && raw.codexAccountMode !== "direct")) {
|
|
210
|
+
return "provider openai codexAccountMode must be pool or direct";
|
|
211
|
+
}
|
|
212
|
+
if (seed) seed.codexAccountMode = raw.codexAccountMode;
|
|
213
|
+
const canonical = seed && sameCanonicalProviderSeed(raw, seed);
|
|
214
|
+
if (!canonical) {
|
|
215
|
+
return `provider ${name} must equal the canonical built-in provider seed`;
|
|
216
|
+
}
|
|
217
|
+
} else if (Object.hasOwn(raw, "codexAccountMode")) {
|
|
218
|
+
return `provider ${name} must not include codexAccountMode`;
|
|
219
|
+
}
|
|
220
|
+
const typed = provider as unknown as OcxProviderConfig;
|
|
221
|
+
const baseUrlError = providerBaseUrlConfigError(typed.baseUrl);
|
|
160
222
|
if (baseUrlError) return `provider ${name} ${baseUrlError}`;
|
|
161
|
-
const destinationError = providerDestinationConfigError(name,
|
|
223
|
+
const destinationError = providerDestinationConfigError(name, typed);
|
|
162
224
|
if (destinationError) return `provider ${name} ${destinationError}`;
|
|
163
|
-
const headersError = providerHeadersConfigError(
|
|
225
|
+
const headersError = providerHeadersConfigError(typed.headers);
|
|
164
226
|
if (headersError) return `provider ${name} ${headersError}`;
|
|
165
|
-
|
|
227
|
+
const maxInputError = positiveIntegerRecordConfigError(raw.modelMaxInputTokens, "modelMaxInputTokens");
|
|
228
|
+
if (maxInputError) return `provider ${name} ${maxInputError}`;
|
|
229
|
+
if (typed.authMode === "local") {
|
|
230
|
+
// "local" bypasses key-requirement enforcement (api-keys/key-failover treat non-oauth/
|
|
231
|
+
// forward as key auth; openai-chat skips credential checks for local). Only providers
|
|
232
|
+
// whose registry entry is genuinely local (Ollama/vLLM/LM Studio) may claim it.
|
|
233
|
+
const entry = getProviderRegistryEntry(name);
|
|
234
|
+
if (entry && entry.authKind !== "local") {
|
|
235
|
+
return `provider ${name} cannot use authMode "local" — its registry entry requires ${entry.authKind} auth`;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (typed.authMode === "forward") {
|
|
166
239
|
const normalizedName = name.trim().toLowerCase();
|
|
167
|
-
const base =
|
|
168
|
-
const isBuiltInChatGptForward =
|
|
169
|
-
&&
|
|
240
|
+
const base = typed.baseUrl.replace(/\/+$/, "");
|
|
241
|
+
const isBuiltInChatGptForward = normalizedName === "openai"
|
|
242
|
+
&& typed.adapter === "openai-responses"
|
|
170
243
|
&& base === "https://chatgpt.com/backend-api/codex";
|
|
171
244
|
if (isBuiltInChatGptForward) return null;
|
|
172
245
|
return `provider ${name} uses reserved authMode "forward"; configure ChatGPT passthrough via the built-in provider`;
|
|
@@ -212,6 +285,7 @@ export function safeConfigDTO(config: OcxConfig): unknown {
|
|
|
212
285
|
"allowPrivateNetwork",
|
|
213
286
|
"authMode",
|
|
214
287
|
"keyOptional",
|
|
288
|
+
"freeTier",
|
|
215
289
|
"liveModels",
|
|
216
290
|
"models",
|
|
217
291
|
"contextWindow",
|
|
@@ -231,6 +305,8 @@ export function safeConfigDTO(config: OcxConfig): unknown {
|
|
|
231
305
|
}
|
|
232
306
|
const registryNote = getProviderRegistryEntry(name)?.note;
|
|
233
307
|
if (typeof registryNote === "string" && registryNote.trim()) dto.note = registryNote;
|
|
308
|
+
const codexAccountMode = providerCodexAccountMode(name, provider);
|
|
309
|
+
if (codexAccountMode) dto.codexAccountMode = codexAccountMode;
|
|
234
310
|
providers[name] = dto;
|
|
235
311
|
}
|
|
236
312
|
return {
|