@bitkyc08/opencodex 2.6.20 → 2.6.21
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 +5 -0
- package/README.md +5 -0
- package/README.zh-CN.md +5 -0
- package/gui/dist/assets/{index-iWo2gxQ2.js → index-npFbiPU_.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +201 -9
- package/src/adapters/google-http.ts +1 -1
- package/src/adapters/kiro-retry.ts +1 -1
- package/src/server/adapter-resolve.ts +2 -2
- package/src/server.ts +3 -3
- package/src/types.ts +3 -1
package/gui/dist/index.html
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
} catch (e) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-npFbiPU_.js"></script>
|
|
20
20
|
<link rel="stylesheet" crossorigin href="/assets/index-DDcEW0Cm.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
package/package.json
CHANGED
|
@@ -42,10 +42,192 @@ const OUTPUT_HEADROOM = 8192;
|
|
|
42
42
|
/** Minimum visible-output room kept below `max_tokens` (so `max_tokens > budget_tokens` always holds). */
|
|
43
43
|
const OUTPUT_FLOOR = 4096;
|
|
44
44
|
const COMPAT_TOOL_PREFIX = "cx_";
|
|
45
|
-
|
|
45
|
+
type CacheControl = { type: "ephemeral"; ttl?: "1h" | "5m" };
|
|
46
|
+
const MAX_CACHE_BREAKPOINTS = 4;
|
|
46
47
|
|
|
47
|
-
function
|
|
48
|
-
|
|
48
|
+
function resolveCacheControl(retention: "none" | "short" | "long" | undefined): CacheControl | undefined {
|
|
49
|
+
const r = retention ?? "short";
|
|
50
|
+
if (r === "none") return undefined;
|
|
51
|
+
return r === "long" ? { type: "ephemeral", ttl: "1h" } : { type: "ephemeral" };
|
|
52
|
+
}
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Prompt-caching breakpoint placement (ported from jawcode)
|
|
55
|
+
//
|
|
56
|
+
// Strategy: place cache_control breakpoints on up to 4 locations in order
|
|
57
|
+
// of stability (most stable first), so Anthropic's cumulative prefix cuts
|
|
58
|
+
// maximise cache hits across turns:
|
|
59
|
+
// 1. tools (last block) — changes rarely
|
|
60
|
+
// 2. system (last block) — changes rarely
|
|
61
|
+
// 3. penultimate user message — stable across the current turn
|
|
62
|
+
// 4. last user message — the new turn's content
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
function applyCacheControlToLast<T extends Record<string, unknown>>(blocks: T[], cc: CacheControl): void {
|
|
66
|
+
if (blocks.length === 0) return;
|
|
67
|
+
const i = blocks.length - 1;
|
|
68
|
+
blocks[i] = { ...blocks[i], cache_control: cc };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function applyCacheControlToLastText(blocks: Array<Record<string, unknown>>, cc: CacheControl): void {
|
|
72
|
+
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
73
|
+
if (blocks[i].type === "text") {
|
|
74
|
+
blocks[i] = { ...blocks[i], cache_control: cc };
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
applyCacheControlToLast(blocks, cc);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
type PromptCachingOptions = {
|
|
82
|
+
maxExplicitBreakpoints?: number;
|
|
83
|
+
skipLastUser?: boolean;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/** Place explicit cache_control breakpoints on the built Anthropic body. */
|
|
87
|
+
function applyPromptCaching(
|
|
88
|
+
body: Record<string, unknown>,
|
|
89
|
+
cc: CacheControl | undefined,
|
|
90
|
+
options: PromptCachingOptions = {},
|
|
91
|
+
): void {
|
|
92
|
+
if (!cc) return;
|
|
93
|
+
const explicitLimit = options.maxExplicitBreakpoints ?? MAX_CACHE_BREAKPOINTS;
|
|
94
|
+
if (explicitLimit <= 0) return;
|
|
95
|
+
|
|
96
|
+
const messages = body.messages as Array<Record<string, unknown>> | undefined;
|
|
97
|
+
|
|
98
|
+
// Skip if external breakpoints are already present on messages.
|
|
99
|
+
if (messages) {
|
|
100
|
+
for (const msg of messages) {
|
|
101
|
+
if (Array.isArray(msg.content)) {
|
|
102
|
+
if ((msg.content as Array<Record<string, unknown>>).some(b => b.cache_control != null)) return;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let used = 0;
|
|
108
|
+
|
|
109
|
+
// 1. tools
|
|
110
|
+
const tools = body.tools as Array<Record<string, unknown>> | undefined;
|
|
111
|
+
if (tools && tools.length > 0) {
|
|
112
|
+
applyCacheControlToLast(tools, cc);
|
|
113
|
+
used++;
|
|
114
|
+
}
|
|
115
|
+
if (used >= explicitLimit) return;
|
|
116
|
+
|
|
117
|
+
// 2. system
|
|
118
|
+
const system = body.system as Array<Record<string, unknown>> | undefined;
|
|
119
|
+
if (system && system.length > 0) {
|
|
120
|
+
applyCacheControlToLast(system, cc);
|
|
121
|
+
used++;
|
|
122
|
+
}
|
|
123
|
+
if (used >= explicitLimit || !messages) return;
|
|
124
|
+
|
|
125
|
+
// Locate user-role message indexes.
|
|
126
|
+
const userIdxs: number[] = [];
|
|
127
|
+
for (let i = 0; i < messages.length; i++) {
|
|
128
|
+
if (messages[i].role === "user") userIdxs.push(i);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 3. penultimate user message
|
|
132
|
+
if (userIdxs.length >= 2) {
|
|
133
|
+
const msg = messages[userIdxs[userIdxs.length - 2]];
|
|
134
|
+
if (typeof msg.content === "string") {
|
|
135
|
+
msg.content = [{ type: "text", text: msg.content, cache_control: cc }];
|
|
136
|
+
} else if (Array.isArray(msg.content) && msg.content.length > 0) {
|
|
137
|
+
applyCacheControlToLastText(msg.content as Array<Record<string, unknown>>, cc);
|
|
138
|
+
}
|
|
139
|
+
used++;
|
|
140
|
+
}
|
|
141
|
+
if (used >= explicitLimit || options.skipLastUser) return;
|
|
142
|
+
|
|
143
|
+
// 4. last user message
|
|
144
|
+
if (userIdxs.length >= 1) {
|
|
145
|
+
const msg = messages[userIdxs[userIdxs.length - 1]];
|
|
146
|
+
if (typeof msg.content === "string") {
|
|
147
|
+
msg.content = [{ type: "text", text: msg.content, cache_control: cc }];
|
|
148
|
+
} else if (Array.isArray(msg.content) && msg.content.length > 0) {
|
|
149
|
+
applyCacheControlToLastText(msg.content as Array<Record<string, unknown>>, cc);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// Breakpoint cap enforcement — strip excess beyond the 4-breakpoint limit
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
function countBreakpoints(body: Record<string, unknown>): number {
|
|
159
|
+
let total = 0;
|
|
160
|
+
const count = (blocks: Array<Record<string, unknown>> | undefined) => {
|
|
161
|
+
if (!blocks) return;
|
|
162
|
+
for (const b of blocks) if (b.cache_control) total++;
|
|
163
|
+
};
|
|
164
|
+
count(body.tools as Array<Record<string, unknown>> | undefined);
|
|
165
|
+
count(body.system as Array<Record<string, unknown>> | undefined);
|
|
166
|
+
const messages = body.messages as Array<Record<string, unknown>> | undefined;
|
|
167
|
+
if (messages) {
|
|
168
|
+
for (const msg of messages) {
|
|
169
|
+
if (Array.isArray(msg.content)) count(msg.content as Array<Record<string, unknown>>);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return total;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function enforceCacheControlLimit(body: Record<string, unknown>, limit = MAX_CACHE_BREAKPOINTS): void {
|
|
176
|
+
const total = countBreakpoints(body);
|
|
177
|
+
if (total <= limit) return;
|
|
178
|
+
let excess = total - limit;
|
|
179
|
+
// Strip from messages first (least stable), then system, then tools.
|
|
180
|
+
const messages = body.messages as Array<Record<string, unknown>> | undefined;
|
|
181
|
+
if (messages) {
|
|
182
|
+
for (const msg of messages) {
|
|
183
|
+
if (excess <= 0) break;
|
|
184
|
+
if (!Array.isArray(msg.content)) continue;
|
|
185
|
+
for (const block of msg.content as Array<Record<string, unknown>>) {
|
|
186
|
+
if (excess <= 0) break;
|
|
187
|
+
if (block.cache_control) { delete block.cache_control; excess--; }
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const stripBlocks = (blocks: Array<Record<string, unknown>> | undefined) => {
|
|
192
|
+
if (!blocks) return;
|
|
193
|
+
for (const b of blocks) {
|
|
194
|
+
if (excess <= 0) break;
|
|
195
|
+
if (b.cache_control) { delete b.cache_control; excess--; }
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
if (excess > 0) stripBlocks(body.system as Array<Record<string, unknown>> | undefined);
|
|
199
|
+
if (excess > 0) stripBlocks(body.tools as Array<Record<string, unknown>> | undefined);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// TTL ordering — Anthropic requires 1-hour breakpoints before 5-minute ones
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
function normalizeTtlOrdering(body: Record<string, unknown>): void {
|
|
207
|
+
const allBlocks: Array<Record<string, unknown>> = [];
|
|
208
|
+
const collect = (blocks: Array<Record<string, unknown>> | undefined) => {
|
|
209
|
+
if (!blocks) return;
|
|
210
|
+
for (const b of blocks) if (b.cache_control) allBlocks.push(b);
|
|
211
|
+
};
|
|
212
|
+
collect(body.tools as Array<Record<string, unknown>> | undefined);
|
|
213
|
+
collect(body.system as Array<Record<string, unknown>> | undefined);
|
|
214
|
+
const messages = body.messages as Array<Record<string, unknown>> | undefined;
|
|
215
|
+
if (messages) {
|
|
216
|
+
for (const msg of messages) {
|
|
217
|
+
if (Array.isArray(msg.content)) collect(msg.content as Array<Record<string, unknown>>);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Walk forward: once we see a 5-min (no ttl / ttl:"5m"), any subsequent 1h must be demoted.
|
|
221
|
+
let seenShort = false;
|
|
222
|
+
for (const b of allBlocks) {
|
|
223
|
+
const cc = b.cache_control as CacheControl;
|
|
224
|
+
if (cc.ttl !== "1h") {
|
|
225
|
+
seenShort = true;
|
|
226
|
+
} else if (seenShort) {
|
|
227
|
+
// 1h after a short → demote to default (5m)
|
|
228
|
+
delete cc.ttl;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
49
231
|
}
|
|
50
232
|
|
|
51
233
|
function isLikelyRealAnthropicThinkingSignature(signature: string | undefined): signature is string {
|
|
@@ -241,12 +423,10 @@ function toolsToAnthropicFormat(parsed: OcxParsedRequest, toolNames: { toWire: (
|
|
|
241
423
|
description: t.description,
|
|
242
424
|
input_schema: t.parameters,
|
|
243
425
|
}));
|
|
244
|
-
const last = converted.length - 1;
|
|
245
|
-
converted[last] = withPromptCache(converted[last]);
|
|
246
426
|
return converted;
|
|
247
427
|
}
|
|
248
428
|
|
|
249
|
-
export function createAnthropicAdapter(provider: OcxProviderConfig): ProviderAdapter {
|
|
429
|
+
export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetention?: "none" | "short" | "long"): ProviderAdapter {
|
|
250
430
|
const isOAuth = provider.authMode === "oauth";
|
|
251
431
|
const toolNames = buildToolNameTransforms(provider);
|
|
252
432
|
return {
|
|
@@ -262,15 +442,14 @@ export function createAnthropicAdapter(provider: OcxProviderConfig): ProviderAda
|
|
|
262
442
|
stream: parsed.stream,
|
|
263
443
|
max_tokens: parsed.options.maxOutputTokens ?? DEFAULT_MAX_TOKENS,
|
|
264
444
|
};
|
|
265
|
-
if (usesNativeAnthropicEndpoint(provider)) body.cache_control = EPHEMERAL_CACHE_CONTROL;
|
|
266
445
|
if (isOAuth) {
|
|
267
446
|
// Claude OAuth (Pro/Max) requires the first system block to be the Claude Code identity.
|
|
268
447
|
body.system = [
|
|
269
448
|
{ type: "text", text: CLAUDE_CODE_SYSTEM_INSTRUCTION },
|
|
270
|
-
...(system ? [
|
|
449
|
+
...(system ? [{ type: "text", text: system }] : []),
|
|
271
450
|
];
|
|
272
451
|
} else if (system) {
|
|
273
|
-
body.system = [
|
|
452
|
+
body.system = [{ type: "text", text: system }];
|
|
274
453
|
}
|
|
275
454
|
if (tools) body.tools = tools;
|
|
276
455
|
if (parsed.options.temperature !== undefined) body.temperature = parsed.options.temperature;
|
|
@@ -327,6 +506,19 @@ export function createAnthropicAdapter(provider: OcxProviderConfig): ProviderAda
|
|
|
327
506
|
}
|
|
328
507
|
if (provider.headers) Object.assign(headers, provider.headers);
|
|
329
508
|
|
|
509
|
+
// Prompt caching: native Anthropic supports top-level automatic caching, which
|
|
510
|
+
// follows the moving final block across turns. Keep one breakpoint slot free for it.
|
|
511
|
+
const cc = resolveCacheControl(cacheRetention);
|
|
512
|
+
const automaticPromptCaching = cc && usesNativeAnthropicEndpoint(provider);
|
|
513
|
+
if (automaticPromptCaching) body.cache_control = cc;
|
|
514
|
+
const explicitLimit = automaticPromptCaching ? MAX_CACHE_BREAKPOINTS - 1 : MAX_CACHE_BREAKPOINTS;
|
|
515
|
+
applyPromptCaching(body, cc, {
|
|
516
|
+
maxExplicitBreakpoints: explicitLimit,
|
|
517
|
+
skipLastUser: !!automaticPromptCaching,
|
|
518
|
+
});
|
|
519
|
+
enforceCacheControlLimit(body, explicitLimit);
|
|
520
|
+
normalizeTtlOrdering(body);
|
|
521
|
+
|
|
330
522
|
return { url, method: "POST", headers, body: JSON.stringify(body) };
|
|
331
523
|
},
|
|
332
524
|
|
|
@@ -46,7 +46,7 @@ async function normalizeFinalGoogleError(label: string, res: Response): Promise<
|
|
|
46
46
|
* error body. `label` is the provider-facing prefix used in error messages.
|
|
47
47
|
*/
|
|
48
48
|
export async function fetchGoogleWithRetry(label: string, request: AdapterRequest, ctx: AdapterFetchContext = {}): Promise<Response> {
|
|
49
|
-
const timeoutMs = ctx.timeoutMs ??
|
|
49
|
+
const timeoutMs = ctx.timeoutMs ?? 200_000;
|
|
50
50
|
let lastError: unknown;
|
|
51
51
|
for (let attempt = 0; attempt < GOOGLE_RETRY_ATTEMPTS; attempt++) {
|
|
52
52
|
if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
|
|
@@ -46,7 +46,7 @@ async function normalizeFinalKiroHttpError(res: Response): Promise<Response> {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
export async function fetchKiroWithRetry(request: AdapterRequest, ctx: AdapterFetchContext = {}): Promise<Response> {
|
|
49
|
-
const timeoutMs = ctx.timeoutMs ??
|
|
49
|
+
const timeoutMs = ctx.timeoutMs ?? 200_000;
|
|
50
50
|
let lastError: unknown;
|
|
51
51
|
for (let attempt = 0; attempt < KIRO_RETRY_ATTEMPTS; attempt++) {
|
|
52
52
|
if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
|
|
@@ -23,12 +23,12 @@ export function resolveWireProtocolOverride(providerName: string, modelId: strin
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/** Build the provider adapter for a resolved provider config. */
|
|
26
|
-
export function resolveAdapter(providerConfig: OcxProviderConfig) {
|
|
26
|
+
export function resolveAdapter(providerConfig: OcxProviderConfig, cacheRetention?: "none" | "short" | "long") {
|
|
27
27
|
switch (providerConfig.adapter) {
|
|
28
28
|
case "openai-chat":
|
|
29
29
|
return createOpenAIChatAdapter(providerConfig);
|
|
30
30
|
case "anthropic":
|
|
31
|
-
return createAnthropicAdapter(providerConfig);
|
|
31
|
+
return createAnthropicAdapter(providerConfig, cacheRetention);
|
|
32
32
|
case "openai-responses":
|
|
33
33
|
return createResponsesPassthroughAdapter(providerConfig);
|
|
34
34
|
case "google":
|
package/src/server.ts
CHANGED
|
@@ -356,7 +356,7 @@ async function handleResponses(
|
|
|
356
356
|
}
|
|
357
357
|
|
|
358
358
|
const adapterProvider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider);
|
|
359
|
-
const adapter = resolveAdapter(adapterProvider);
|
|
359
|
+
const adapter = resolveAdapter(adapterProvider, config.cacheRetention);
|
|
360
360
|
const recordTerminalOutcomes = options.recordTerminalOutcomes !== false;
|
|
361
361
|
|
|
362
362
|
if ("passthrough" in adapter && adapter.passthrough) {
|
|
@@ -366,7 +366,7 @@ async function handleResponses(
|
|
|
366
366
|
// whose cancel() aborts the upstream — preventing leaked connections (RC2, passthrough path).
|
|
367
367
|
const upstream = new AbortController();
|
|
368
368
|
linkAbortSignal(upstream, options.abortSignal);
|
|
369
|
-
const connectMs = config.connectTimeoutMs ??
|
|
369
|
+
const connectMs = config.connectTimeoutMs ?? 200_000;
|
|
370
370
|
let upstreamResponse: Response;
|
|
371
371
|
try {
|
|
372
372
|
upstreamResponse = await fetchWithResetRetry(
|
|
@@ -570,7 +570,7 @@ async function handleResponses(
|
|
|
570
570
|
|
|
571
571
|
const upstream = new AbortController();
|
|
572
572
|
const cleanupUpstreamAbort = linkAbortSignal(upstream, options.abortSignal);
|
|
573
|
-
const connectMs = config.connectTimeoutMs ??
|
|
573
|
+
const connectMs = config.connectTimeoutMs ?? 200_000;
|
|
574
574
|
|
|
575
575
|
const request = await adapter.buildRequest(parsed, { headers: selectedForwardHeaders });
|
|
576
576
|
if (typeof request.usageLog?.inputTokens === "number") {
|
package/src/types.ts
CHANGED
|
@@ -245,7 +245,7 @@ export interface OcxConfig {
|
|
|
245
245
|
proxy?: string;
|
|
246
246
|
/** Upstream stall timeout (seconds). After this many seconds of no upstream data, emits response.incomplete. Default 90. Min 1. */
|
|
247
247
|
stallTimeoutSec?: number;
|
|
248
|
-
/** Connect timeout (ms) for upstream fetch — covers DNS, TCP, TLS, and response header. Default
|
|
248
|
+
/** Connect timeout (ms) for upstream fetch — covers DNS, TCP, TLS, and response header. Default 200000. */
|
|
249
249
|
connectTimeoutMs?: number;
|
|
250
250
|
/** Graceful shutdown drain timeout (ms). Active turns are aborted after this deadline. Default 5000. */
|
|
251
251
|
shutdownTimeoutMs?: number;
|
|
@@ -262,6 +262,8 @@ export interface OcxConfig {
|
|
|
262
262
|
syncResumeHistory?: boolean;
|
|
263
263
|
/** Freshness window (ms) for the per-provider live `/models` cache. Defaults to 5 min. */
|
|
264
264
|
modelCacheTtlMs?: number;
|
|
265
|
+
/** Anthropic prompt-cache retention: "short" = 5-min ephemeral (default), "long" = 1-hour extended, "none" = disabled. */
|
|
266
|
+
cacheRetention?: "none" | "short" | "long";
|
|
265
267
|
/** Web-search sidecar: route web_search for non-OpenAI models through a gpt-mini via ChatGPT passthrough. */
|
|
266
268
|
webSearchSidecar?: OcxWebSearchSidecarConfig;
|
|
267
269
|
/** Vision sidecar: describe images via a gpt vision model so text-only models can "see" them. */
|