@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.
@@ -16,7 +16,7 @@
16
16
  } catch (e) {}
17
17
  })();
18
18
  </script>
19
- <script type="module" crossorigin src="/assets/index-iWo2gxQ2.js"></script>
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitkyc08/opencodex",
3
- "version": "2.6.20",
3
+ "version": "2.6.21",
4
4
  "description": "Universal provider proxy for OpenAI Codex — use any LLM with Codex CLI/App/SDK",
5
5
  "type": "module",
6
6
  "main": "./bin/package-main.mjs",
@@ -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
- const EPHEMERAL_CACHE_CONTROL = { type: "ephemeral" } as const;
45
+ type CacheControl = { type: "ephemeral"; ttl?: "1h" | "5m" };
46
+ const MAX_CACHE_BREAKPOINTS = 4;
46
47
 
47
- function withPromptCache<T extends Record<string, unknown>>(block: T): T & { cache_control: typeof EPHEMERAL_CACHE_CONTROL } {
48
- return { ...block, cache_control: EPHEMERAL_CACHE_CONTROL };
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 ? [withPromptCache({ type: "text", text: system })] : []),
449
+ ...(system ? [{ type: "text", text: system }] : []),
271
450
  ];
272
451
  } else if (system) {
273
- body.system = [withPromptCache({ type: "text", text: 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 ?? 100_000;
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 ?? 100_000;
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 ?? 100_000;
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 ?? 100_000;
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 30000. */
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. */