@bitkyc08/opencodex 2.7.33 → 2.7.34
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 +1 -1
- package/README.ko.md +1 -1
- package/README.md +21 -10
- package/README.ru.md +1 -1
- package/README.zh-CN.md +1 -1
- package/gui/dist/assets/index-BkmJJgg6.js +52 -0
- package/gui/dist/assets/index-Sg-7L_oZ.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +13 -6
- package/src/adapters/cursor/discovery.ts +39 -4
- package/src/adapters/cursor/exec-policy.ts +11 -13
- package/src/adapters/cursor/live-transport.ts +22 -4
- package/src/adapters/cursor/protobuf-events.ts +140 -8
- package/src/adapters/cursor/protobuf-request.ts +15 -0
- package/src/adapters/cursor/request-builder.ts +10 -5
- package/src/adapters/cursor/transport.ts +3 -2
- package/src/adapters/cursor/types.ts +14 -0
- package/src/adapters/kiro-constants.ts +12 -0
- package/src/adapters/kiro-errors.ts +111 -2
- package/src/adapters/kiro-events.ts +154 -35
- package/src/adapters/kiro-retry.ts +116 -32
- package/src/adapters/kiro-tools.ts +30 -20
- package/src/adapters/kiro-wire.ts +47 -6
- package/src/adapters/kiro.ts +891 -228
- package/src/adapters/openai-chat.ts +12 -5
- package/src/adapters/openai-responses.ts +7 -2
- package/src/bridge.ts +109 -26
- package/src/claude/outbound.ts +27 -4
- package/src/cli/index.ts +1 -1
- package/src/codex/catalog.ts +375 -33
- package/src/combos/index.ts +3 -0
- package/src/combos/request.ts +4 -4
- package/src/combos/resolve.ts +2 -2
- package/src/combos/types.ts +104 -2
- package/src/config.ts +70 -1
- package/src/lib/eventstream-decoder.ts +9 -0
- package/src/oauth/index.ts +3 -1
- package/src/oauth/kiro-credentials.ts +48 -20
- package/src/oauth/login-cli.ts +2 -0
- package/src/providers/derive.ts +8 -0
- package/src/providers/kiro-models.ts +2 -2
- package/src/providers/openai-sidecar.ts +28 -1
- package/src/providers/registry.ts +39 -2
- package/src/responses/parser.ts +22 -10
- package/src/responses/schema.ts +1 -0
- package/src/responses/state.ts +50 -10
- package/src/router.ts +15 -3
- package/src/server/auth-cors.ts +7 -0
- package/src/server/claude-messages.ts +6 -0
- package/src/server/index.ts +6 -3
- package/src/server/management-api.ts +187 -43
- package/src/server/ports.ts +4 -2
- package/src/server/request-log.ts +3 -2
- package/src/server/responses-item-id-repair.ts +281 -0
- package/src/server/responses.ts +274 -73
- package/src/types.ts +109 -16
- package/src/update/job.ts +81 -1
- package/src/vision/describe.ts +2 -1
- package/src/web-search/executor.ts +2 -1
- package/src/web-search/loop.ts +9 -1
- package/src/web-search/progress-stream.ts +12 -10
- package/gui/dist/assets/index-D6Fcl4yM.css +0 -1
- package/gui/dist/assets/index-d63HMU0x.js +0 -52
|
@@ -53,6 +53,8 @@ export interface ProviderRegistryEntry {
|
|
|
53
53
|
contextWindow?: number;
|
|
54
54
|
modelContextWindows?: Record<string, number>;
|
|
55
55
|
modelInputModalities?: Record<string, string[]>;
|
|
56
|
+
defaultMaxOutputTokens?: number;
|
|
57
|
+
modelMaxOutputTokens?: Record<string, number>;
|
|
56
58
|
reasoningEfforts?: string[];
|
|
57
59
|
modelReasoningEfforts?: Record<string, string[]>;
|
|
58
60
|
modelDefaultReasoningEfforts?: Record<string, string>;
|
|
@@ -87,7 +89,7 @@ export type ProviderConfigSeed = Pick<
|
|
|
87
89
|
OcxProviderConfig,
|
|
88
90
|
"adapter" | "baseUrl" | "authMode" | "keyOptional" | "freeTier" | "modelSuffixBracketStrip" | "defaultModel" | "models"
|
|
89
91
|
| "liveModels" | "contextWindow" | "modelContextWindows" | "modelInputModalities"
|
|
90
|
-
| "modelMaxInputTokens"
|
|
92
|
+
| "modelMaxInputTokens" | "defaultMaxOutputTokens" | "modelMaxOutputTokens"
|
|
91
93
|
| "reasoningEfforts" | "modelReasoningEfforts" | "modelDefaultReasoningEfforts" | "reasoningEffortMap" | "modelReasoningEffortMap"
|
|
92
94
|
| "noVisionModels" | "noReasoningModels" | "noTemperatureModels" | "noTopPModels" | "noPenaltyModels"
|
|
93
95
|
| "autoToolChoiceOnlyModels" | "preserveReasoningContentModels" | "thinkingToggleModels" | "thinkingBudgetModels" | "escapeBuiltinToolNames"
|
|
@@ -219,6 +221,13 @@ const ALIBABA_INTL_TOKEN_PLAN_MODELS = [
|
|
|
219
221
|
const ALIBABA_INTL_TOKEN_PLAN_QWEN_MODELS = [
|
|
220
222
|
"qwen3.8-max-preview", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-plus", "qwen3.6-flash",
|
|
221
223
|
];
|
|
224
|
+
|
|
225
|
+
// 260722 Tencent Cloud Coding Plan. The plan's model set is explicitly dynamic; these are the
|
|
226
|
+
// current documented ids and live discovery remains enabled so successful /models responses win.
|
|
227
|
+
// Tencent marks every Coding Plan model as text-only input and restricts plan keys to interactive
|
|
228
|
+
// coding tools (not custom application backends or non-interactive batch automation).
|
|
229
|
+
// Evidence: https://cloud.tencent.cn/document/product/1823/130092
|
|
230
|
+
const TENCENT_CODING_PLAN_MODELS = ["tc-code-latest", "glm-5", "kimi-k2.5", "minimax-m2.5"];
|
|
222
231
|
const ALIBABA_INTL_TOKEN_PLAN_INPUT_MODALITIES: Record<string, string[]> = {
|
|
223
232
|
"qwen3.8-max-preview": ["text", "image"],
|
|
224
233
|
"qwen3.7-max": ["text", "image"],
|
|
@@ -343,7 +352,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
343
352
|
authKind: "oauth",
|
|
344
353
|
featured: false,
|
|
345
354
|
dashboardPreset: true,
|
|
346
|
-
note: "Experimental Cursor bridge. Live transport and live model discovery are enabled after a standalone PKCE browser login via 'ocx login cursor'; native read/write/delete/shell/fetch execution
|
|
355
|
+
note: "Experimental Cursor bridge. Live transport and live model discovery are enabled after a standalone PKCE browser login via 'ocx login cursor'; native read/write/delete/shell/fetch execution is disabled by default and request text such as Codex sandbox markers never authorizes it. Set \"nativeLocalExec\": \"on\" on providers.cursor in ~/.opencodex/config.json (dashboard: Providers → Cursor → Edit JSON) only for a trusted local experiment where every data-plane caller is trusted. \"off\" denies all, \"codex-sandbox\" is accepted for backwards compatibility but fails closed, and legacy \"unsafeAllowNativeLocalExec\": true still means explicit operator opt-in.",
|
|
347
356
|
models: cursorModelIds(CURSOR_STATIC_MODELS),
|
|
348
357
|
liveModels: true,
|
|
349
358
|
defaultModel: "auto",
|
|
@@ -743,6 +752,20 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
743
752
|
},
|
|
744
753
|
{ id: "nanogpt", label: "NanoGPT", baseUrl: "https://nano-gpt.com/api/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://nano-gpt.com/api" },
|
|
745
754
|
{ id: "synthetic", label: "Synthetic", baseUrl: "https://api.synthetic.new/openai/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://synthetic.new" },
|
|
755
|
+
// SiliconFlow publishes an OpenAI-compatible chat endpoint and a dynamic model catalog. Do not
|
|
756
|
+
// freeze reasoning controls here: enable_thinking/thinking_budget support and limits vary by
|
|
757
|
+
// model, so live metadata or an explicit user override must own those capabilities.
|
|
758
|
+
// Evidence: https://docs.siliconflow.cn/en/api-reference/chat-completions/chat-completions
|
|
759
|
+
{
|
|
760
|
+
id: "siliconflow",
|
|
761
|
+
label: "SiliconFlow",
|
|
762
|
+
baseUrl: "https://api.siliconflow.cn/v1",
|
|
763
|
+
adapter: "openai-chat",
|
|
764
|
+
authKind: "key",
|
|
765
|
+
dashboardUrl: "https://cloud.siliconflow.cn/account/ak",
|
|
766
|
+
liveModels: true,
|
|
767
|
+
note: "OpenAI-compatible live model catalog; reasoning controls vary by model.",
|
|
768
|
+
},
|
|
746
769
|
// Qwen Cloud: token plan is the preset default; GUI offers pay-as-you-go + custom via baseUrlChoices.
|
|
747
770
|
// Formerly `qwen-portal` / portal.qwen.ai — that host is outdated.
|
|
748
771
|
{
|
|
@@ -756,6 +779,20 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
756
779
|
dashboardUrl: "https://docs.qwencloud.com",
|
|
757
780
|
note: "Pick token plan, pay as you go, or a custom compatible-mode base URL",
|
|
758
781
|
},
|
|
782
|
+
{
|
|
783
|
+
id: "tencent-coding-plan",
|
|
784
|
+
label: "Tencent Cloud Coding Plan",
|
|
785
|
+
baseUrl: "https://api.lkeap.cloud.tencent.com/coding/v3",
|
|
786
|
+
adapter: "openai-chat",
|
|
787
|
+
authKind: "key",
|
|
788
|
+
dashboardUrl: "https://console.cloud.tencent.com/tokenhub/codingplan",
|
|
789
|
+
defaultModel: "tc-code-latest",
|
|
790
|
+
models: TENCENT_CODING_PLAN_MODELS,
|
|
791
|
+
liveModels: true,
|
|
792
|
+
modelInputModalities: Object.fromEntries(TENCENT_CODING_PLAN_MODELS.map(id => [id, ["text"]])),
|
|
793
|
+
noVisionModels: TENCENT_CODING_PLAN_MODELS,
|
|
794
|
+
note: "Coding tools only. Tencent forbids general API automation, custom backends, and non-interactive batch use.",
|
|
795
|
+
},
|
|
759
796
|
// 2026-07-10: docs unverified; model data frozen. Evidence: devlog/_plan/260710_provider_hardening/002_research_cn.md.
|
|
760
797
|
{ id: "qianfan", label: "Qianfan (Baidu)", baseUrl: "https://qianfan.baidubce.com/v2", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://console.bce.baidu.com/iam/#/iam/apikey/list" },
|
|
761
798
|
// 2026-07-10: docs unverified; model data frozen. Evidence: devlog/_plan/260710_provider_hardening/002_research_cn.md.
|
package/src/responses/parser.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
import { namespacedToolName } from "../types";
|
|
14
14
|
import { responsesRequestSchema } from "./schema";
|
|
15
15
|
import { compactionItemToText } from "./compaction";
|
|
16
|
+
import { previousResponseReplayPrefixLength } from "./state";
|
|
16
17
|
import { decodeReasoningEnvelope } from "./reasoning-envelope";
|
|
17
18
|
import { extractHostedWebSearch, WEB_SEARCH_TOOL_NAME } from "../web-search/synthetic-tool";
|
|
18
19
|
|
|
@@ -198,6 +199,10 @@ function outputToToolResultContent(output: string | unknown[] | undefined): stri
|
|
|
198
199
|
return parts;
|
|
199
200
|
}
|
|
200
201
|
|
|
202
|
+
function toolOutputContainsEncryptedContent(output: string | unknown[] | undefined): boolean {
|
|
203
|
+
return Array.isArray(output) && output.some(raw => isObj(raw) && raw.type === "encrypted_content");
|
|
204
|
+
}
|
|
205
|
+
|
|
201
206
|
/**
|
|
202
207
|
* codex-rs ImageDetail allows "original", but chat-completions providers only accept
|
|
203
208
|
* auto|low|high on image_url.detail — degrade "original" to "high" (the codex default).
|
|
@@ -220,6 +225,7 @@ function findToolById(messages: OcxMessage[], callId: string): { name: string; n
|
|
|
220
225
|
const REASONING_EFFORTS = new Set(["none", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
221
226
|
|
|
222
227
|
export function parseRequest(body: unknown): OcxParsedRequest {
|
|
228
|
+
const replayedInputPrefixLength = previousResponseReplayPrefixLength(body);
|
|
223
229
|
const parsed = responsesRequestSchema.safeParse(body);
|
|
224
230
|
if (!parsed.success) {
|
|
225
231
|
throw new Error(`responses parse error: ${parsed.error.message}`);
|
|
@@ -248,6 +254,7 @@ export function parseRequest(body: unknown): OcxParsedRequest {
|
|
|
248
254
|
// Remote compaction v2: the input tail carries `{type:"compaction_trigger"}` and Codex expects a
|
|
249
255
|
// synthetic `{type:"compaction"}` output item (src/responses/compaction.ts). Flagged for the server.
|
|
250
256
|
let compactionRequest = false;
|
|
257
|
+
let contextCompactionBoundary = false;
|
|
251
258
|
|
|
252
259
|
if (typeof data.instructions === "string" && data.instructions.length > 0) {
|
|
253
260
|
systemPrompt.push(data.instructions);
|
|
@@ -256,7 +263,8 @@ export function parseRequest(body: unknown): OcxParsedRequest {
|
|
|
256
263
|
if (typeof data.input === "string") {
|
|
257
264
|
messages.push({ role: "user", content: data.input, timestamp: now });
|
|
258
265
|
} else if (data.input) {
|
|
259
|
-
for (
|
|
266
|
+
for (let inputIndex = 0; inputIndex < data.input.length; inputIndex++) {
|
|
267
|
+
const item = data.input[inputIndex];
|
|
260
268
|
const effectiveType = (item as { type?: string }).type ?? ("role" in item ? "message" : undefined);
|
|
261
269
|
|
|
262
270
|
if (effectiveType === "compaction_trigger") {
|
|
@@ -281,7 +289,10 @@ export function parseRequest(body: unknown): OcxParsedRequest {
|
|
|
281
289
|
// the routed model keeps the compacted context; real OpenAI-encrypted blobs degrade to a note.
|
|
282
290
|
// `context_compaction` (encrypted_content optional) is codex-rs's local-compaction marker;
|
|
283
291
|
// with no payload it is a pure marker (the summary follows as its own user message), so it
|
|
284
|
-
// is dropped silently. It must NOT flag _compactionRequest.
|
|
292
|
+
// is dropped silently. It must NOT flag _compactionRequest. Only a marker newly appended in
|
|
293
|
+
// this request starts a provider-private context epoch; markers inside the prefix restored by
|
|
294
|
+
// previous_response_id were already acknowledged on the turn that introduced them.
|
|
295
|
+
if (inputIndex >= replayedInputPrefixLength) contextCompactionBoundary = true;
|
|
285
296
|
const encrypted = (item as { encrypted_content?: unknown }).encrypted_content;
|
|
286
297
|
if (effectiveType === "context_compaction" && typeof encrypted !== "string") continue;
|
|
287
298
|
pendingReasoning.length = 0;
|
|
@@ -323,7 +334,7 @@ export function parseRequest(body: unknown): OcxParsedRequest {
|
|
|
323
334
|
}
|
|
324
335
|
|
|
325
336
|
if (effectiveType === "message") {
|
|
326
|
-
const msg = item as { role?: string; content?: unknown };
|
|
337
|
+
const msg = item as { role?: string; content?: unknown; phase?: "commentary" | "final_answer" };
|
|
327
338
|
switch (msg.role) {
|
|
328
339
|
case "system": {
|
|
329
340
|
pendingReasoning.length = 0;
|
|
@@ -346,6 +357,7 @@ export function parseRequest(body: unknown): OcxParsedRequest {
|
|
|
346
357
|
content: pendingReasoning.length > 0
|
|
347
358
|
? [...pendingReasoning.map(entry => entry.part), ...parts]
|
|
348
359
|
: parts,
|
|
360
|
+
...(msg.phase ? { phase: msg.phase } : {}),
|
|
349
361
|
model: data.model,
|
|
350
362
|
timestamp: now,
|
|
351
363
|
});
|
|
@@ -443,13 +455,10 @@ export function parseRequest(body: unknown): OcxParsedRequest {
|
|
|
443
455
|
}
|
|
444
456
|
|
|
445
457
|
if (effectiveType === "web_search_call") {
|
|
446
|
-
// Replayed hosted web-search evidence
|
|
447
|
-
//
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
assistantHolderWithReasoning().content.push({
|
|
451
|
-
type: "text", text: query ? `[web search performed: ${query}]` : "[web search performed]",
|
|
452
|
-
});
|
|
458
|
+
// Replayed hosted web-search evidence has no paired result payload that routed providers can
|
|
459
|
+
// consume. Keep it out of assistant-visible text: the old marker was useful as an internal
|
|
460
|
+
// loop hint, but when no sidecar is available the model can echo it as a fake answer.
|
|
461
|
+
pendingReasoning.length = 0;
|
|
453
462
|
continue;
|
|
454
463
|
}
|
|
455
464
|
|
|
@@ -504,6 +513,7 @@ export function parseRequest(body: unknown): OcxParsedRequest {
|
|
|
504
513
|
role: "toolResult", toolCallId: output.call_id,
|
|
505
514
|
toolName: toolInfo.name, toolNamespace: toolInfo.namespace,
|
|
506
515
|
content: outputToToolResultContent(output.output), isError: false, timestamp: now,
|
|
516
|
+
...(toolOutputContainsEncryptedContent(output.output) ? { containsEncryptedContent: true } : {}),
|
|
507
517
|
});
|
|
508
518
|
continue;
|
|
509
519
|
}
|
|
@@ -518,6 +528,7 @@ export function parseRequest(body: unknown): OcxParsedRequest {
|
|
|
518
528
|
// Same payload shape as function_call_output (codex-rs FunctionCallOutputPayload):
|
|
519
529
|
// string or content items — normalize arrays instead of leaking raw wire blocks.
|
|
520
530
|
content: outputToToolResultContent(output.output), isError: false, timestamp: now,
|
|
531
|
+
...(toolOutputContainsEncryptedContent(output.output) ? { containsEncryptedContent: true } : {}),
|
|
521
532
|
});
|
|
522
533
|
}
|
|
523
534
|
}
|
|
@@ -586,6 +597,7 @@ export function parseRequest(body: unknown): OcxParsedRequest {
|
|
|
586
597
|
...(webSearch ? { _webSearch: webSearch } : {}),
|
|
587
598
|
...(structuredOutput ? { _structuredOutput: true } : {}),
|
|
588
599
|
...(compactionRequest ? { _compactionRequest: true } : {}),
|
|
600
|
+
...(contextCompactionBoundary ? { _contextCompactionBoundary: true } : {}),
|
|
589
601
|
};
|
|
590
602
|
}
|
|
591
603
|
|
package/src/responses/schema.ts
CHANGED
|
@@ -47,6 +47,7 @@ const assistantMessageItemSchema = z.object({
|
|
|
47
47
|
type: z.literal("message").optional(),
|
|
48
48
|
role: z.literal("assistant"),
|
|
49
49
|
content: z.union([z.string(), z.array(outputContentBlockSchema)]).optional(),
|
|
50
|
+
phase: z.enum(["commentary", "final_answer"]).optional(),
|
|
50
51
|
});
|
|
51
52
|
const reasoningItemSchema = z.object({
|
|
52
53
|
type: z.literal("reasoning"),
|
package/src/responses/state.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { atomicWriteFile, getConfigDir } from "../config";
|
|
4
|
+
import type { OcxProviderContinuationState } from "../types";
|
|
4
5
|
|
|
5
6
|
const MAX_STORED_RESPONSES = 1_000;
|
|
6
7
|
const RESPONSE_TTL_MS = 60 * 60 * 1_000;
|
|
@@ -13,11 +14,18 @@ const SNAPSHOT_TOTAL_MAX_BYTES = 24 * 1024 * 1024;
|
|
|
13
14
|
interface StoredResponseState {
|
|
14
15
|
createdAt: number;
|
|
15
16
|
items: unknown[];
|
|
17
|
+
/** v2 provider-keyed continuation metadata. */
|
|
18
|
+
providers?: OcxProviderContinuationState;
|
|
19
|
+
/** v1 Cursor-only metadata, accepted only while loading old snapshots. */
|
|
16
20
|
conversationId?: string;
|
|
17
21
|
cursorCheckpointUsable?: boolean;
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
const states = new Map<string, StoredResponseState>();
|
|
25
|
+
// Expansion provenance must stay proxy-private: a WeakMap distinguishes replayed history from the
|
|
26
|
+
// newly appended input suffix without adding an unknown field that native passthrough could send
|
|
27
|
+
// upstream. The parser uses this boundary to acknowledge historical compaction markers exactly once.
|
|
28
|
+
const replayedInputPrefixLengths = new WeakMap<object, number>();
|
|
21
29
|
let loaded = false;
|
|
22
30
|
let persistTimer: ReturnType<typeof setTimeout> | null = null;
|
|
23
31
|
let pendingPersistPath: string | null = null;
|
|
@@ -44,14 +52,28 @@ function ensureLoaded(): void {
|
|
|
44
52
|
const path = snapshotPath();
|
|
45
53
|
if (!existsSync(path)) return;
|
|
46
54
|
const raw = JSON.parse(readFileSync(path, "utf-8")) as { version?: unknown; states?: unknown };
|
|
47
|
-
if (raw.version !== 1 || !Array.isArray(raw.states)) return;
|
|
55
|
+
if ((raw.version !== 1 && raw.version !== 2) || !Array.isArray(raw.states)) return;
|
|
48
56
|
for (const entry of raw.states) {
|
|
49
57
|
if (!Array.isArray(entry) || entry.length !== 2) continue;
|
|
50
58
|
const [id, state] = entry as [unknown, unknown];
|
|
51
59
|
if (typeof id !== "string" || !state || typeof state !== "object") continue;
|
|
52
60
|
const rec = state as StoredResponseState;
|
|
53
61
|
if (typeof rec.createdAt !== "number" || !Array.isArray(rec.items)) continue;
|
|
54
|
-
|
|
62
|
+
const providers = rec.providers ?? (rec.conversationId
|
|
63
|
+
? {
|
|
64
|
+
cursor: {
|
|
65
|
+
conversationId: rec.conversationId,
|
|
66
|
+
...(rec.cursorCheckpointUsable !== undefined
|
|
67
|
+
? { checkpointUsable: rec.cursorCheckpointUsable }
|
|
68
|
+
: {}),
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
: undefined);
|
|
72
|
+
states.set(id, {
|
|
73
|
+
createdAt: rec.createdAt,
|
|
74
|
+
items: rec.items,
|
|
75
|
+
...(providers ? { providers } : {}),
|
|
76
|
+
});
|
|
55
77
|
}
|
|
56
78
|
pruneResponses();
|
|
57
79
|
} catch {
|
|
@@ -81,7 +103,7 @@ function persistNow(path: string): void {
|
|
|
81
103
|
// mkdirSync's mode only applies on creation — re-harden an existing config dir so the
|
|
82
104
|
// conversation-content snapshot never lands in a group/world-readable directory.
|
|
83
105
|
try { chmodSync(dirname(path), 0o700); } catch { /* best-effort (e.g. Windows) */ }
|
|
84
|
-
atomicWriteFile(path, JSON.stringify({ version:
|
|
106
|
+
atomicWriteFile(path, JSON.stringify({ version: 2, states: entries }));
|
|
85
107
|
} catch {
|
|
86
108
|
/* best-effort: disk trouble must never affect request handling */
|
|
87
109
|
}
|
|
@@ -131,23 +153,36 @@ export function expandPreviousResponseInput(body: unknown): unknown {
|
|
|
131
153
|
pruneResponses();
|
|
132
154
|
const previous = states.get(previousId);
|
|
133
155
|
if (!previous) return body;
|
|
134
|
-
|
|
156
|
+
const expanded = {
|
|
135
157
|
...request,
|
|
136
158
|
input: [...previous.items, ...inputItems(request.input)],
|
|
137
159
|
};
|
|
160
|
+
replayedInputPrefixLengths.set(expanded, previous.items.length);
|
|
161
|
+
return expanded;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Number of leading input items restored from previous_response_id state for this exact body. */
|
|
165
|
+
export function previousResponseReplayPrefixLength(body: unknown): number {
|
|
166
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return 0;
|
|
167
|
+
return replayedInputPrefixLengths.get(body) ?? 0;
|
|
138
168
|
}
|
|
139
169
|
|
|
140
170
|
export function previousResponseConversationId(responseId: string | undefined): string | undefined {
|
|
171
|
+
return previousResponseProviderState(responseId)?.cursor?.conversationId;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function previousResponseProviderState(responseId: string | undefined): OcxProviderContinuationState | undefined {
|
|
141
175
|
if (!responseId) return undefined;
|
|
142
176
|
ensureLoaded();
|
|
143
177
|
pruneResponses();
|
|
144
|
-
|
|
178
|
+
const providers = states.get(responseId)?.providers;
|
|
179
|
+
return providers ? structuredClone(providers) : undefined;
|
|
145
180
|
}
|
|
146
181
|
|
|
147
182
|
export function rememberResponseState(
|
|
148
183
|
requestBody: unknown,
|
|
149
184
|
response: { id?: unknown; output?: unknown; status?: unknown },
|
|
150
|
-
|
|
185
|
+
providerState?: OcxProviderContinuationState | string,
|
|
151
186
|
opts?: { force?: boolean },
|
|
152
187
|
): void {
|
|
153
188
|
if (!requestBody || typeof requestBody !== "object" || Array.isArray(requestBody)) return;
|
|
@@ -161,6 +196,14 @@ export function rememberResponseState(
|
|
|
161
196
|
if (typeof response.id !== "string" || !Array.isArray(response.output)) return;
|
|
162
197
|
if (response.status !== undefined && response.status !== "completed") return;
|
|
163
198
|
ensureLoaded();
|
|
199
|
+
const normalizedProviderState: OcxProviderContinuationState = typeof providerState === "string"
|
|
200
|
+
? { cursor: { conversationId: providerState } }
|
|
201
|
+
: structuredClone(providerState ?? {});
|
|
202
|
+
if (normalizedProviderState.cursor?.conversationId) {
|
|
203
|
+
normalizedProviderState.cursor.checkpointUsable = !response.output.some(item => {
|
|
204
|
+
return !!item && typeof item === "object" && (item as { type?: unknown }).type === "function_call";
|
|
205
|
+
});
|
|
206
|
+
}
|
|
164
207
|
states.set(response.id, {
|
|
165
208
|
createdAt: now(),
|
|
166
209
|
items: [...inputItems(request.input), ...response.output],
|
|
@@ -169,10 +212,7 @@ export function rememberResponseState(
|
|
|
169
212
|
// checkpoint/cache is safe to reuse: a turn that ended with a pending client tool call produced an
|
|
170
213
|
// incomplete agent turn on the Cursor side (we suspended without a real mcpResult), so its
|
|
171
214
|
// checkpoint must not be reused — but the conversation id string itself is still valid.
|
|
172
|
-
...(
|
|
173
|
-
cursorCheckpointUsable: !response.output.some(item => {
|
|
174
|
-
return !!item && typeof item === "object" && (item as { type?: unknown }).type === "function_call";
|
|
175
|
-
}),
|
|
215
|
+
...(Object.keys(normalizedProviderState).length > 0 ? { providers: normalizedProviderState } : {}),
|
|
176
216
|
});
|
|
177
217
|
pruneResponses();
|
|
178
218
|
schedulePersist();
|
package/src/router.ts
CHANGED
|
@@ -49,6 +49,7 @@ export function knownModelIdsForProvider(provName: string, prov: OcxProviderConf
|
|
|
49
49
|
registry?.modelReasoningEfforts,
|
|
50
50
|
registry?.modelDefaultReasoningEfforts,
|
|
51
51
|
registry?.modelReasoningEffortMap,
|
|
52
|
+
registry?.modelMaxOutputTokens,
|
|
52
53
|
]) {
|
|
53
54
|
for (const id of Object.keys(map ?? {})) ids.add(id);
|
|
54
55
|
}
|
|
@@ -141,6 +142,7 @@ function routedProviderConfig(providerName: string, provider: OcxProviderConfig)
|
|
|
141
142
|
const modelMaxInputTokens = providerName === OPENAI_API_PROVIDER_ID
|
|
142
143
|
? mergePositiveNumberCaps(registryEntry.modelMaxInputTokens, provider.modelMaxInputTokens)
|
|
143
144
|
: mergeRecordFill(registryEntry.modelMaxInputTokens, provider.modelMaxInputTokens);
|
|
145
|
+
const modelMaxOutputTokens = mergeRecordFill(registryEntry.modelMaxOutputTokens, provider.modelMaxOutputTokens);
|
|
144
146
|
const noVisionModels = mergeStringArray(registryEntry.noVisionModels, provider.noVisionModels);
|
|
145
147
|
const noReasoningModels = mergeStringArray(registryEntry.noReasoningModels, provider.noReasoningModels);
|
|
146
148
|
const noTemperatureModels = mergeStringArray(registryEntry.noTemperatureModels, provider.noTemperatureModels);
|
|
@@ -183,9 +185,13 @@ function routedProviderConfig(providerName: string, provider: OcxProviderConfig)
|
|
|
183
185
|
// opt-in, while an explicit user `false` keeps overriding registry `true`.
|
|
184
186
|
...(provider.parallelToolCalls === undefined && registryEntry.parallelToolCalls !== undefined ? { parallelToolCalls: registryEntry.parallelToolCalls } : {}),
|
|
185
187
|
...(provider.promptCacheKey === undefined && registryEntry.promptCacheKey !== undefined ? { promptCacheKey: registryEntry.promptCacheKey } : {}),
|
|
188
|
+
...(provider.defaultMaxOutputTokens === undefined && registryEntry.defaultMaxOutputTokens !== undefined
|
|
189
|
+
? { defaultMaxOutputTokens: registryEntry.defaultMaxOutputTokens }
|
|
190
|
+
: {}),
|
|
186
191
|
...(modelContextWindows ? { modelContextWindows } : {}),
|
|
187
192
|
...(modelInputModalities ? { modelInputModalities } : {}),
|
|
188
193
|
...(modelMaxInputTokens ? { modelMaxInputTokens } : {}),
|
|
194
|
+
...(modelMaxOutputTokens ? { modelMaxOutputTokens } : {}),
|
|
189
195
|
...(modelReasoningEfforts ? { modelReasoningEfforts } : {}),
|
|
190
196
|
...(modelDefaultReasoningEfforts ? { modelDefaultReasoningEfforts } : {}),
|
|
191
197
|
...(reasoningEffortMap ? { reasoningEffortMap } : {}),
|
|
@@ -228,15 +234,17 @@ function routeResult(providerName: string, provider: OcxProviderConfig, modelId:
|
|
|
228
234
|
};
|
|
229
235
|
}
|
|
230
236
|
|
|
231
|
-
|
|
237
|
+
function routeModelInternal(config: OcxConfig, modelId: string, bypassCombos: boolean): RouteResult {
|
|
232
238
|
const preservePhysicalComboProvider =
|
|
233
239
|
hasOwnProvider(config.providers, COMBO_NAMESPACE)
|
|
234
240
|
&& Object.keys(config.combos ?? {}).length === 0;
|
|
235
|
-
if (!preservePhysicalComboProvider) {
|
|
241
|
+
if (!bypassCombos && !preservePhysicalComboProvider) {
|
|
236
242
|
const combo = tryPickComboModel(config, modelId);
|
|
237
243
|
if (combo) {
|
|
238
244
|
const concrete = `${combo.target.provider}/${combo.target.model}`;
|
|
239
|
-
|
|
245
|
+
// The selected target is already a concrete provider/model reference. Resolve it without
|
|
246
|
+
// consulting combo aliases again, otherwise an alias that shadows the target can recurse.
|
|
247
|
+
const routed = routeModelInternal(config, concrete, true);
|
|
240
248
|
return { ...routed, combo };
|
|
241
249
|
}
|
|
242
250
|
}
|
|
@@ -300,6 +308,10 @@ export function routeModel(config: OcxConfig, modelId: string): RouteResult {
|
|
|
300
308
|
throw new Error(`No provider configured for model: ${modelId}`);
|
|
301
309
|
}
|
|
302
310
|
|
|
311
|
+
export function routeModel(config: OcxConfig, modelId: string): RouteResult {
|
|
312
|
+
return routeModelInternal(config, modelId, false);
|
|
313
|
+
}
|
|
314
|
+
|
|
303
315
|
function routeByKnownModelPattern(config: OcxConfig, modelId: string): RouteResult | undefined {
|
|
304
316
|
for (const { providerNames, prefixes } of MODEL_PROVIDER_PATTERNS) {
|
|
305
317
|
if (prefixes.some(prefix => modelId.startsWith(prefix))) {
|
package/src/server/auth-cors.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { timingSafeEqual } from "node:crypto";
|
|
|
2
2
|
import { formatErrorResponse } from "../bridge";
|
|
3
3
|
import {
|
|
4
4
|
codexAutoStartEnabled,
|
|
5
|
+
positiveIntegerConfigError,
|
|
5
6
|
positiveIntegerRecordConfigError,
|
|
6
7
|
providerBaseUrlConfigError,
|
|
7
8
|
providerHeadersConfigError,
|
|
@@ -227,6 +228,10 @@ export function providerManagementConfigError(name: unknown, provider: unknown):
|
|
|
227
228
|
if (headersError) return `provider ${name} ${headersError}`;
|
|
228
229
|
const maxInputError = positiveIntegerRecordConfigError(raw.modelMaxInputTokens, "modelMaxInputTokens");
|
|
229
230
|
if (maxInputError) return `provider ${name} ${maxInputError}`;
|
|
231
|
+
const defaultMaxOutputError = positiveIntegerConfigError(raw.defaultMaxOutputTokens, "defaultMaxOutputTokens");
|
|
232
|
+
if (defaultMaxOutputError) return `provider ${name} ${defaultMaxOutputError}`;
|
|
233
|
+
const maxOutputError = positiveIntegerRecordConfigError(raw.modelMaxOutputTokens, "modelMaxOutputTokens");
|
|
234
|
+
if (maxOutputError) return `provider ${name} ${maxOutputError}`;
|
|
230
235
|
const openRouterError = openRouterRoutingConfigError(typed);
|
|
231
236
|
if (openRouterError) return `provider ${name} ${openRouterError}`;
|
|
232
237
|
if (typed.authMode === "local") {
|
|
@@ -293,6 +298,8 @@ export function safeConfigDTO(config: OcxConfig): unknown {
|
|
|
293
298
|
"models",
|
|
294
299
|
"contextWindow",
|
|
295
300
|
"modelContextWindows",
|
|
301
|
+
"defaultMaxOutputTokens",
|
|
302
|
+
"modelMaxOutputTokens",
|
|
296
303
|
"openRouterRouting",
|
|
297
304
|
"modelOpenRouterRouting",
|
|
298
305
|
"reasoningEfforts",
|
|
@@ -729,6 +729,12 @@ export async function handleClaudeMessages(
|
|
|
729
729
|
return anthropicErrorResponse(502, error?.message ?? "upstream request failed", "api_error");
|
|
730
730
|
}
|
|
731
731
|
const message = responsesJsonToAnthropicMessage(json, requestedModel);
|
|
732
|
+
if ((message as Rec).type === "error") {
|
|
733
|
+
return new Response(JSON.stringify(message), {
|
|
734
|
+
status: 529,
|
|
735
|
+
headers: { "Content-Type": "application/json", "Retry-After": "2" },
|
|
736
|
+
});
|
|
737
|
+
}
|
|
732
738
|
if (!stream) {
|
|
733
739
|
return new Response(JSON.stringify(message), { status: 200, headers: { "Content-Type": "application/json" } });
|
|
734
740
|
}
|
package/src/server/index.ts
CHANGED
|
@@ -134,9 +134,12 @@ const WEBSOCKET_IDLE_TIMEOUT_SECONDS = 0;
|
|
|
134
134
|
// Source invariant for tests/passthrough-abort.test.ts after the pure module split:
|
|
135
135
|
// if (isEventStream && upstreamResponse.body) {
|
|
136
136
|
// upstreamResponse.body.tee()
|
|
137
|
+
// const repairConfig = route.provider.responsesItemIdRepair;
|
|
138
|
+
// const repairedBody = hasResponsesItemIdRepair(repairConfig)
|
|
137
139
|
// process.platform === "win32"
|
|
140
|
+
// && !hasResponsesItemIdRepair(repairConfig)
|
|
138
141
|
// ? nativeBody
|
|
139
|
-
// relaySseWithFailedTail(
|
|
142
|
+
// relaySseWithFailedTail(repairedBody, upstream)
|
|
140
143
|
// new Response(clientBody
|
|
141
144
|
// markNativePassthroughSseResponse
|
|
142
145
|
// const body = relayWithAbort(upstreamResponse.body, upstream);
|
|
@@ -251,7 +254,7 @@ export function startServer(port?: number) {
|
|
|
251
254
|
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, config);
|
|
252
255
|
}
|
|
253
256
|
const goModels = await fetchAllModels(config);
|
|
254
|
-
const { applyNativeVisibility, buildCatalogEntries, disabledNativeSlugs, exactComboCatalogSlugs, loadCatalogTemplate, nativeOpenAiSlugs, orderForSubagents, filterCatalogVisibleModels, visibleNativeSlugs } = await import("../codex/catalog");
|
|
257
|
+
const { applyNativeVisibility, buildCatalogEntries, disabledNativeSlugs, exactComboCatalogSlugs, loadCatalogTemplate, nativeOpenAiSlugs, orderForSubagents, filterCatalogVisibleModels, uniqueCatalogModelsForRawPublicList, visibleNativeSlugs } = await import("../codex/catalog");
|
|
255
258
|
const nativeSlugs = nativeOpenAiSlugs();
|
|
256
259
|
const goEnabled = filterCatalogVisibleModels(goModels, config);
|
|
257
260
|
const goOrdered = orderForSubagents(goEnabled, config.subagentModels);
|
|
@@ -301,7 +304,7 @@ export function startServer(port?: number) {
|
|
|
301
304
|
// (pure availability list — disabled natives are omitted entirely).
|
|
302
305
|
const data = [
|
|
303
306
|
...visibleNativeSlugs(config).map(id => ({ id, object: "model", created: 0, owned_by: "openai" })),
|
|
304
|
-
...goOrdered.map(m => ({ id: `${m.provider}/${m.id}`, object: "model", created: 0, owned_by: m.owned_by ?? m.provider })),
|
|
307
|
+
...uniqueCatalogModelsForRawPublicList(goOrdered).map(m => ({ id: m.alias ?? `${m.provider}/${m.id}`, object: "model", created: 0, owned_by: m.owned_by ?? m.provider })),
|
|
305
308
|
];
|
|
306
309
|
return jsonResponse({ object: "list", data }, 200, req, config);
|
|
307
310
|
}
|