@bitkyc08/opencodex 2.6.26-preview.20260705 → 2.6.28-preview.20260707

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.
Files changed (118) hide show
  1. package/README.md +1 -0
  2. package/bin/ocx.mjs +4 -4
  3. package/gui/dist/assets/index-ByGC8-Bm.css +1 -0
  4. package/gui/dist/assets/index-CkV5xFA8.js +15 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +4 -4
  7. package/src/adapters/anthropic-image-guard.ts +195 -0
  8. package/src/adapters/anthropic.ts +85 -14
  9. package/src/adapters/cursor/cursor-errors.ts +2 -2
  10. package/src/adapters/cursor/live-transport.ts +1 -1
  11. package/src/adapters/cursor/transport-retry.ts +2 -2
  12. package/src/adapters/google-errors.ts +1 -1
  13. package/src/adapters/google-http.ts +1 -1
  14. package/src/adapters/google-truncation.ts +1 -1
  15. package/src/adapters/google.ts +1 -1
  16. package/src/adapters/kiro-errors.ts +1 -1
  17. package/src/adapters/kiro-retry.ts +1 -1
  18. package/src/adapters/kiro-truncation.ts +1 -1
  19. package/src/adapters/kiro.ts +1 -1
  20. package/src/adapters/openai-chat.ts +12 -2
  21. package/src/adapters/openai-responses.ts +126 -3
  22. package/src/bridge.ts +164 -7
  23. package/src/{doctor.ts → cli/doctor.ts} +21 -4
  24. package/src/{cli-help.ts → cli/help.ts} +1 -1
  25. package/src/cli/index.ts +584 -0
  26. package/src/{init.ts → cli/init.ts} +6 -6
  27. package/src/{cli-models.ts → cli/models.ts} +2 -2
  28. package/src/{cli-provider.ts → cli/provider.ts} +12 -8
  29. package/src/{star-prompt.ts → cli/star-prompt.ts} +1 -1
  30. package/src/{cli-status.ts → cli/status.ts} +7 -7
  31. package/src/cli.ts +9 -575
  32. package/src/{codex-account-label.ts → codex/account-label.ts} +1 -1
  33. package/src/{codex-account-lifecycle.ts → codex/account-lifecycle.ts} +6 -6
  34. package/src/{codex-account-store.ts → codex/account-store.ts} +2 -2
  35. package/src/{codex-account-usability.ts → codex/account-usability.ts} +4 -4
  36. package/src/{codex-auth-api.ts → codex/auth-api.ts} +18 -18
  37. package/src/{codex-auth-collision.ts → codex/auth-collision.ts} +4 -4
  38. package/src/{codex-auth-context.ts → codex/auth-context.ts} +9 -9
  39. package/src/{codex-catalog.ts → codex/catalog.ts} +49 -20
  40. package/src/codex/history-migration-guardian.ts +102 -0
  41. package/src/{codex-history-provider.ts → codex/history-provider.ts} +111 -7
  42. package/src/{codex-home.ts → codex/home.ts} +1 -1
  43. package/src/{codex-inject.ts → codex/inject.ts} +204 -26
  44. package/src/{codex-journal.ts → codex/journal.ts} +2 -2
  45. package/src/{codex-main-account.ts → codex/main-account.ts} +2 -2
  46. package/src/{model-cache.ts → codex/model-cache.ts} +1 -1
  47. package/src/{codex-paths.ts → codex/paths.ts} +2 -2
  48. package/src/{codex-plugins-doctor.ts → codex/plugins-doctor.ts} +2 -2
  49. package/src/{codex-refresh.ts → codex/refresh.ts} +4 -4
  50. package/src/{codex-routing.ts → codex/routing.ts} +8 -8
  51. package/src/{codex-shim.ts → codex/shim.ts} +6 -5
  52. package/src/{codex-sync.ts → codex/sync.ts} +4 -4
  53. package/src/{codex-websocket-registry.ts → codex/websocket-registry.ts} +1 -1
  54. package/src/config.ts +2 -0
  55. package/src/generated/jawcode-model-metadata.ts +2 -0
  56. package/src/{bun-runtime.ts → lib/bun-runtime.ts} +1 -1
  57. package/src/{crash-guard.ts → lib/crash-guard.ts} +1 -1
  58. package/src/{process-control.ts → lib/process-control.ts} +1 -1
  59. package/src/{service-secrets.ts → lib/service-secrets.ts} +1 -1
  60. package/src/oauth/callback-server.ts +1 -1
  61. package/src/oauth/google-antigravity.ts +7 -4
  62. package/src/oauth/index.ts +67 -16
  63. package/src/oauth/login-cli.ts +2 -2
  64. package/src/oauth/store.ts +236 -20
  65. package/src/oauth/token-guardian.ts +24 -20
  66. package/src/oauth/types.ts +16 -0
  67. package/src/providers/api-keys.ts +121 -0
  68. package/src/{provider-context-cap.ts → providers/context-cap.ts} +1 -1
  69. package/src/providers/derive.ts +2 -0
  70. package/src/providers/key-failover.ts +145 -0
  71. package/src/{provider-label.ts → providers/label.ts} +1 -1
  72. package/src/{provider-quota.ts → providers/quota.ts} +12 -7
  73. package/src/providers/registry.ts +66 -4
  74. package/src/responses/compaction.ts +117 -0
  75. package/src/responses/parser.ts +89 -13
  76. package/src/responses/reasoning-envelope.ts +52 -0
  77. package/src/responses/schema.ts +15 -3
  78. package/src/responses/state.ts +117 -2
  79. package/src/router.ts +2 -0
  80. package/src/server/auth-cors.ts +231 -0
  81. package/src/server/index.ts +523 -0
  82. package/src/server/lifecycle.ts +73 -0
  83. package/src/server/management-api.ts +628 -0
  84. package/src/{proxy-liveness.ts → server/proxy-liveness.ts} +1 -1
  85. package/src/server/relay.ts +534 -0
  86. package/src/server/request-decompress.ts +46 -0
  87. package/src/server/request-log.ts +310 -0
  88. package/src/server/responses.ts +775 -0
  89. package/src/{ws-bridge.ts → server/ws-bridge.ts} +44 -13
  90. package/src/service.ts +19 -11
  91. package/src/types.ts +27 -0
  92. package/src/{update.ts → update/index.ts} +5 -5
  93. package/src/{update-job.ts → update/job.ts} +7 -6
  94. package/src/{update-notify.ts → update/notify.ts} +3 -3
  95. package/src/{usage-debug.ts → usage/debug.ts} +3 -3
  96. package/src/{usage-log.ts → usage/log.ts} +3 -3
  97. package/src/{usage-summary.ts → usage/summary.ts} +3 -3
  98. package/src/{usage-totals.ts → usage/totals.ts} +1 -1
  99. package/src/vision/describe.ts +3 -3
  100. package/src/vision/index.ts +21 -1
  101. package/src/web-search/executor.ts +4 -4
  102. package/src/web-search/index.ts +1 -1
  103. package/src/web-search/loop.ts +84 -24
  104. package/gui/dist/assets/index-BcHhxo1I.css +0 -1
  105. package/gui/dist/assets/index-DCC1q_Jx.js +0 -15
  106. package/src/server.ts +0 -2501
  107. /package/src/{codex-account-runtime-state.ts → codex/account-runtime-state.ts} +0 -0
  108. /package/src/{codex-quota.ts → codex/quota.ts} +0 -0
  109. /package/src/{abort.ts → lib/abort.ts} +0 -0
  110. /package/src/{debug.ts → lib/debug.ts} +0 -0
  111. /package/src/{errors.ts → lib/errors.ts} +0 -0
  112. /package/src/{open-url.ts → lib/open-url.ts} +0 -0
  113. /package/src/{privacy.ts → lib/privacy.ts} +0 -0
  114. /package/src/{redact.ts → lib/redact.ts} +0 -0
  115. /package/src/{sidecar-tracker.ts → lib/sidecar-tracker.ts} +0 -0
  116. /package/src/{upstream-retry.ts → lib/upstream-retry.ts} +0 -0
  117. /package/src/{win-paths.ts → lib/win-paths.ts} +0 -0
  118. /package/src/{ports.ts → server/ports.ts} +0 -0
@@ -16,8 +16,8 @@
16
16
  } catch (e) {}
17
17
  })();
18
18
  </script>
19
- <script type="module" crossorigin src="/assets/index-DCC1q_Jx.js"></script>
20
- <link rel="stylesheet" crossorigin href="/assets/index-BcHhxo1I.css">
19
+ <script type="module" crossorigin src="/assets/index-CkV5xFA8.js"></script>
20
+ <link rel="stylesheet" crossorigin href="/assets/index-ByGC8-Bm.css">
21
21
  </head>
22
22
  <body>
23
23
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitkyc08/opencodex",
3
- "version": "2.6.26-preview.20260705",
3
+ "version": "2.6.28-preview.20260707",
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",
@@ -32,10 +32,10 @@
32
32
  "bun"
33
33
  ],
34
34
  "scripts": {
35
- "dev": "bun run src/cli.ts start",
36
- "dev:proxy": "bun run src/cli.ts start",
35
+ "dev": "bun run src/cli/index.ts start",
36
+ "dev:proxy": "bun run src/cli/index.ts start",
37
37
  "dev:gui": "cd gui && bun run dev",
38
- "start": "bun run src/cli.ts start",
38
+ "start": "bun run src/cli/index.ts start",
39
39
  "test": "bun test ./tests/",
40
40
  "typecheck": "bun x tsc --noEmit",
41
41
  "privacy:scan": "bun scripts/privacy-scan.ts",
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Anthropic per-request image limits (docs.anthropic.com Vision, verified 2026-07-06):
3
+ * - <= 20 images: each image may be up to 8000px on a side.
4
+ * - > 20 images ("many-image request"): each image is capped at 2000px per side;
5
+ * one offender 400s the whole request:
6
+ * "At least one of the image dimensions exceed max allowed size for many-image requests: 2000 pixels"
7
+ * - Hard cap: 100 images per request.
8
+ *
9
+ * Codex threads accumulate screenshots in history, so long sessions cross 20 images
10
+ * easily and any single retina capture (>2000px wide) kills every later turn. Bun has
11
+ * no native resizer and we do not want a decoder dependency, so instead of downscaling
12
+ * we keep the request under the 20-image threshold (restoring the 8000px allowance) by
13
+ * textifying the OLDEST image blocks. Newest screenshots are the ones the model needs.
14
+ */
15
+
16
+ export const MANY_IMAGE_THRESHOLD = 20;
17
+ export const MANY_IMAGE_MAX_DIMENSION = 2000;
18
+ export const ABSOLUTE_MAX_DIMENSION = 8000;
19
+ export const MAX_IMAGES_PER_REQUEST = 100;
20
+
21
+ const OMITTED_TEXT = "[image omitted: Anthropic request exceeded the 20-image limit for large images; older screenshots were dropped]";
22
+ const OVERSIZED_TEXT = "[image omitted: exceeds Anthropic's 8000px per-side limit]";
23
+
24
+ interface ImageDimensions { width: number; height: number }
25
+
26
+ /** Read big-endian u16/u32 helpers over a byte array. */
27
+ function u16be(b: Uint8Array, o: number): number { return (b[o] << 8) | b[o + 1]; }
28
+ function u32be(b: Uint8Array, o: number): number { return ((b[o] << 24) | (b[o + 1] << 16) | (b[o + 2] << 8) | b[o + 3]) >>> 0; }
29
+ function u16le(b: Uint8Array, o: number): number { return b[o] | (b[o + 1] << 8); }
30
+ function u24le(b: Uint8Array, o: number): number { return b[o] | (b[o + 1] << 8) | (b[o + 2] << 16); }
31
+
32
+ function pngDimensions(b: Uint8Array): ImageDimensions | null {
33
+ // 8-byte signature + IHDR chunk (len+type at 8..16, data at 16).
34
+ if (b.length < 24) return null;
35
+ if (b[0] !== 0x89 || b[1] !== 0x50 || b[2] !== 0x4e || b[3] !== 0x47) return null;
36
+ return { width: u32be(b, 16), height: u32be(b, 20) };
37
+ }
38
+
39
+ function jpegDimensions(b: Uint8Array): ImageDimensions | null {
40
+ if (b.length < 4 || b[0] !== 0xff || b[1] !== 0xd8) return null;
41
+ let o = 2;
42
+ while (o + 9 < b.length) {
43
+ if (b[o] !== 0xff) { o++; continue; }
44
+ const marker = b[o + 1];
45
+ // Skip padding/fill bytes and standalone markers (RSTn, TEM) which have no length.
46
+ if (marker === 0xff) { o++; continue; }
47
+ if (marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) { o += 2; continue; }
48
+ // SOF0-SOF15 carry dimensions, excluding DHT(0xc4)/JPG(0xc8)/DAC(0xcc).
49
+ if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
50
+ return { height: u16be(b, o + 5), width: u16be(b, o + 7) };
51
+ }
52
+ if (marker === 0xd9 || marker === 0xda) return null; // EOI / start of scan: no SOF found
53
+ const len = u16be(b, o + 2);
54
+ if (len < 2) return null;
55
+ o += 2 + len;
56
+ }
57
+ return null;
58
+ }
59
+
60
+ function gifDimensions(b: Uint8Array): ImageDimensions | null {
61
+ if (b.length < 10) return null;
62
+ if (b[0] !== 0x47 || b[1] !== 0x49 || b[2] !== 0x46) return null;
63
+ return { width: u16le(b, 6), height: u16le(b, 8) };
64
+ }
65
+
66
+ function webpDimensions(b: Uint8Array): ImageDimensions | null {
67
+ if (b.length < 30) return null;
68
+ if (b[0] !== 0x52 || b[1] !== 0x49 || b[2] !== 0x46 || b[3] !== 0x46) return null; // RIFF
69
+ if (b[8] !== 0x57 || b[9] !== 0x45 || b[10] !== 0x42 || b[11] !== 0x50) return null; // WEBP
70
+ const fourcc = String.fromCharCode(b[12], b[13], b[14], b[15]);
71
+ if (fourcc === "VP8X") {
72
+ return { width: u24le(b, 24) + 1, height: u24le(b, 27) + 1 };
73
+ }
74
+ if (fourcc === "VP8 ") {
75
+ // Lossy: frame tag at 20, sync code 9d 01 2a, then 14-bit width/height.
76
+ if (b[23] !== 0x9d || b[24] !== 0x01 || b[25] !== 0x2a) return null;
77
+ return { width: u16le(b, 26) & 0x3fff, height: u16le(b, 28) & 0x3fff };
78
+ }
79
+ if (fourcc === "VP8L") {
80
+ if (b[20] !== 0x2f) return null;
81
+ // VP8L stores 14-bit width-1 / height-1 little-endian-bit-packed.
82
+ const raw = b[21] | (b[22] << 8) | (b[23] << 16) | (b[24] << 24);
83
+ const width = (raw & 0x3fff) + 1;
84
+ const height = ((raw >> 14) & 0x3fff) + 1;
85
+ return { width, height };
86
+ }
87
+ return null;
88
+ }
89
+
90
+ /**
91
+ * Sniff pixel dimensions from the first bytes of a base64 image. Returns null when the
92
+ * format is unrecognized or the header is malformed — callers must treat null as
93
+ * "unknown, assume within limits" so we never drop an image we cannot prove oversized.
94
+ */
95
+ export function sniffImageDimensions(base64: string): ImageDimensions | null {
96
+ // ~48KB of decoded header is enough for every sniffer above, including JPEGs whose
97
+ // SOF sits behind large EXIF/APPn segments (segments are skipped by length).
98
+ const slice = base64.slice(0, 65536);
99
+ let bytes: Uint8Array;
100
+ try {
101
+ bytes = Uint8Array.from(atob(slice.length % 4 === 0 ? slice : slice.slice(0, slice.length - (slice.length % 4))), c => c.charCodeAt(0));
102
+ } catch {
103
+ return null;
104
+ }
105
+ return pngDimensions(bytes) ?? jpegDimensions(bytes) ?? gifDimensions(bytes) ?? webpDimensions(bytes) ?? null;
106
+ }
107
+
108
+ interface ImageBlockRef {
109
+ /** The array holding the block (message content or tool_result content). */
110
+ container: unknown[];
111
+ index: number;
112
+ base64: string | null;
113
+ }
114
+
115
+ function isImageBlock(block: unknown): block is { type: "image"; source: Record<string, unknown> } {
116
+ return typeof block === "object" && block !== null && (block as { type?: unknown }).type === "image";
117
+ }
118
+
119
+ /** Collect refs to every image block in wire order (oldest first), descending into tool_result content. */
120
+ function collectImageRefs(messages: unknown[]): ImageBlockRef[] {
121
+ const refs: ImageBlockRef[] = [];
122
+ const scanArray = (arr: unknown[]): void => {
123
+ for (let i = 0; i < arr.length; i++) {
124
+ const block = arr[i];
125
+ if (isImageBlock(block)) {
126
+ const source = block.source as { type?: unknown; data?: unknown };
127
+ refs.push({
128
+ container: arr,
129
+ index: i,
130
+ base64: source?.type === "base64" && typeof source.data === "string" ? source.data : null,
131
+ });
132
+ } else if (typeof block === "object" && block !== null && (block as { type?: unknown }).type === "tool_result") {
133
+ const content = (block as { content?: unknown }).content;
134
+ if (Array.isArray(content)) scanArray(content);
135
+ }
136
+ }
137
+ };
138
+ for (const msg of messages) {
139
+ const content = (msg as { content?: unknown })?.content;
140
+ if (Array.isArray(content)) scanArray(content);
141
+ }
142
+ return refs;
143
+ }
144
+
145
+ function textify(ref: ImageBlockRef, text: string): void {
146
+ ref.container[ref.index] = { type: "text", text };
147
+ }
148
+
149
+ /**
150
+ * Enforce Anthropic image limits on already-built wire messages (mutates in place).
151
+ * Policy: unconditionally textify >8000px images; when the request would be a
152
+ * many-image request (>20) with at least one image over 2000px, textify oldest
153
+ * images until <=20 so the 8000px allowance applies; always cap at 100 images.
154
+ */
155
+ export function enforceAnthropicImageLimits(messages: unknown[]): void {
156
+ const refs = collectImageRefs(messages);
157
+ if (refs.length === 0) return;
158
+
159
+ const dims = refs.map(r => (r.base64 ? sniffImageDimensions(r.base64) : null));
160
+ const live = new Set<number>(refs.keys());
161
+
162
+ // Rule 1: images over the absolute 8000px cap are invalid in any request.
163
+ for (let i = 0; i < refs.length; i++) {
164
+ const d = dims[i];
165
+ if (d && (d.width > ABSOLUTE_MAX_DIMENSION || d.height > ABSOLUTE_MAX_DIMENSION)) {
166
+ textify(refs[i], OVERSIZED_TEXT);
167
+ live.delete(i);
168
+ }
169
+ }
170
+
171
+ // Rule 2: many-image requests cap each image at 2000px. Keep the request at <=20
172
+ // images (dropping oldest first) whenever a surviving image exceeds that cap OR has
173
+ // unknown dimensions (URL sources and unsniffable formats): one unverifiable offender
174
+ // 400s the whole request upstream, so unknown counts as risky, not as safe.
175
+ const hasRiskyForMany = [...live].some(i => {
176
+ const d = dims[i];
177
+ return d === null || d.width > MANY_IMAGE_MAX_DIMENSION || d.height > MANY_IMAGE_MAX_DIMENSION;
178
+ });
179
+ if (hasRiskyForMany && live.size > MANY_IMAGE_THRESHOLD) {
180
+ for (const i of [...live]) {
181
+ if (live.size <= MANY_IMAGE_THRESHOLD) break;
182
+ textify(refs[i], OMITTED_TEXT);
183
+ live.delete(i);
184
+ }
185
+ }
186
+
187
+ // Rule 3: hard cap of 100 images per request regardless of size.
188
+ if (live.size > MAX_IMAGES_PER_REQUEST) {
189
+ for (const i of [...live]) {
190
+ if (live.size <= MAX_IMAGES_PER_REQUEST) break;
191
+ textify(refs[i], OMITTED_TEXT);
192
+ live.delete(i);
193
+ }
194
+ }
195
+ }
@@ -1,5 +1,5 @@
1
1
  import type { ProviderAdapter } from "./base";
2
- import { debugDroppedFrame } from "../debug";
2
+ import { debugDroppedFrame } from "../lib/debug";
3
3
  import type {
4
4
  AdapterEvent,
5
5
  OcxAssistantMessage,
@@ -16,6 +16,7 @@ import type {
16
16
  import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName, toolAllowedByChoice } from "../types";
17
17
  import { ANTHROPIC_OAUTH_BETA, CLAUDE_CODE_SYSTEM_INSTRUCTION, applyClaudeToolPrefix, stripClaudeToolPrefix } from "../oauth/anthropic";
18
18
  import { parseDataUrl } from "./image";
19
+ import { enforceAnthropicImageLimits } from "./anthropic-image-guard";
19
20
  import { neutralizeIdentity } from "./identity";
20
21
  import { CLAUDE_CODE_HEADERS, claudeCodeSessionId } from "./client-fingerprint";
21
22
  import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge";
@@ -257,6 +258,36 @@ function reasoningBudget(effort: string): number {
257
258
  }
258
259
  }
259
260
 
261
+ /**
262
+ * Claude families that moved to adaptive thinking: they 400 on `thinking.type: "enabled"`
263
+ * ("Use \"thinking.type.adaptive\" and \"output_config.effort\" to control thinking behavior."),
264
+ * while older families (Haiku 4.5, Sonnet 4.x, Opus <= 4.6) 400 on `adaptive` — so both wire
265
+ * shapes must stay. Verified against api.anthropic.com: sonnet-5, fable-5, opus-4-7 and opus-4-8
266
+ * require adaptive; haiku-4-5 and sonnet-4-5 reject it; opus-4-6/sonnet-4-6 accept both.
267
+ */
268
+ const ADAPTIVE_THINKING_FAMILY_MINIMUMS: Record<string, readonly [major: number, minor: number]> = {
269
+ sonnet: [5, 0],
270
+ opus: [4, 7],
271
+ fable: [0, 0],
272
+ };
273
+
274
+ function usesAdaptiveThinking(modelId: string): boolean {
275
+ // Minor is 1-2 digits with a non-digit lookahead so date-pinned ids ("claude-opus-4-20250514")
276
+ // parse as minor 0 instead of minor 20250514; suffixed ids ("claude-opus-4-8[1m]") still match.
277
+ const match = /^claude-([a-z]+)-(\d+)(?:-(\d{1,2}))?(?!\d)/.exec(modelId);
278
+ if (!match) return false;
279
+ const minimum = ADAPTIVE_THINKING_FAMILY_MINIMUMS[match[1]];
280
+ if (!minimum) return false;
281
+ const major = Number(match[2]);
282
+ const minor = match[3] === undefined ? 0 : Number(match[3]);
283
+ return major > minimum[0] || (major === minimum[0] && minor >= minimum[1]);
284
+ }
285
+
286
+ /** `output_config.effort` accepts low|medium|high|xhigh|max — "minimal" is rejected with a 400. */
287
+ function adaptiveEffort(effort: string): string {
288
+ return effort === "minimal" ? "low" : effort;
289
+ }
290
+
260
291
  function usageFromAnthropic(usage: Record<string, number> | undefined): OcxUsage | undefined {
261
292
  if (!usage) return undefined;
262
293
  const hasCache = usage.cache_read_input_tokens !== undefined || usage.cache_creation_input_tokens !== undefined;
@@ -354,6 +385,11 @@ function messagesToAnthropicFormat(
354
385
  content.push({ type: "text", text: (part as OcxTextContent).text });
355
386
  } else if (part.type === "thinking") {
356
387
  const t = part as OcxThinkingContent;
388
+ // Redacted blocks replay verbatim FIRST (they preceded the visible thinking block
389
+ // in the original stream order preserved by the bridge envelope).
390
+ for (const data of t.redacted ?? []) {
391
+ content.push({ type: "redacted_thinking", data });
392
+ }
357
393
  if (isLikelyRealAnthropicThinkingSignature(t.signature)) {
358
394
  content.push({ type: "thinking", thinking: t.thinking, signature: t.signature });
359
395
  }
@@ -406,6 +442,16 @@ function messagesToAnthropicFormat(
406
442
  }
407
443
  }
408
444
 
445
+ // Newer Anthropic models reject assistant-tail histories as prefill:
446
+ // "This model does not support assistant message prefill. The conversation must end with a user message."
447
+ // previous_response_id expansion with empty new input, interrupted-turn replay, and web-search sidecar
448
+ // first iterations can all reach this; Kiro uses the same "(continue)" nudge precedent (src/adapters/kiro.ts:283).
449
+ if (messages.length === 0) {
450
+ messages.push({ role: "user", content: "(continue)" });
451
+ } else if ((messages[messages.length - 1] as { role?: string }).role === "assistant") {
452
+ messages.push({ role: "user", content: "(continue)" });
453
+ }
454
+
409
455
  return { system, messages };
410
456
  }
411
457
 
@@ -434,6 +480,9 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
434
480
 
435
481
  buildRequest(parsed: OcxParsedRequest) {
436
482
  const { system, messages } = messagesToAnthropicFormat(parsed, toolNames);
483
+ // Anthropic rejects many-image requests (>20 images) carrying any image over
484
+ // 2000px per side; see anthropic-image-guard.ts for the full limit policy.
485
+ enforceAnthropicImageLimits(messages);
437
486
  const tools = toolsToAnthropicFormat(parsed, toolNames);
438
487
 
439
488
  const body: Record<string, unknown> = {
@@ -460,16 +509,23 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
460
509
  // REASONING_EFFORTS). A bare truthy check would treat "none" as truthy and wrongly enable
461
510
  // extended thinking (and strip temperature/top_p), so gate on a real, non-disable effort.
462
511
  if (typeof parsed.options.reasoning === "string" && parsed.options.reasoning !== "none") {
463
- // Anthropic requires max_tokens > thinking.budget_tokens (max_tokens caps thinking +
464
- // visible output) and budget_tokens >= 1024. Codex sends the SAME value for both, which
465
- // 400s ("max_tokens must be greater than thinking.budget_tokens"). Size them so max_tokens
466
- // always exceeds the budget within a model-safe ceiling, reserving room for visible output.
467
- const maxOut = parsed.options.maxOutputTokens ?? DEFAULT_MAX_TOKENS;
468
- const wantBudget = reasoningBudget(parsed.options.reasoning);
469
- const maxTokens = Math.min(REASONING_MAX_TOKENS_CEILING, Math.max(maxOut, wantBudget + OUTPUT_HEADROOM));
470
- const budget = Math.max(MIN_THINKING_BUDGET, Math.min(wantBudget, maxTokens - OUTPUT_FLOOR));
471
- body.max_tokens = maxTokens;
472
- body.thinking = { type: "enabled", budget_tokens: budget };
512
+ if (usesAdaptiveThinking(parsed.modelId)) {
513
+ // Adaptive-thinking models replace the token budget with an effort knob and reject
514
+ // `thinking.type: "enabled"` outright no budget/max_tokens re-sizing needed.
515
+ body.thinking = { type: "adaptive" };
516
+ body.output_config = { effort: adaptiveEffort(parsed.options.reasoning) };
517
+ } else {
518
+ // Anthropic requires max_tokens > thinking.budget_tokens (max_tokens caps thinking +
519
+ // visible output) and budget_tokens >= 1024. Codex sends the SAME value for both, which
520
+ // 400s ("max_tokens must be greater than thinking.budget_tokens"). Size them so max_tokens
521
+ // always exceeds the budget within a model-safe ceiling, reserving room for visible output.
522
+ const maxOut = parsed.options.maxOutputTokens ?? DEFAULT_MAX_TOKENS;
523
+ const wantBudget = reasoningBudget(parsed.options.reasoning);
524
+ const maxTokens = Math.min(REASONING_MAX_TOKENS_CEILING, Math.max(maxOut, wantBudget + OUTPUT_HEADROOM));
525
+ const budget = Math.max(MIN_THINKING_BUDGET, Math.min(wantBudget, maxTokens - OUTPUT_FLOOR));
526
+ body.max_tokens = maxTokens;
527
+ body.thinking = { type: "enabled", budget_tokens: budget };
528
+ }
473
529
  // Extended thinking disallows temperature != 1 and top_p — drop both or the API 400s.
474
530
  delete body.temperature;
475
531
  delete body.top_p;
@@ -577,7 +633,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
577
633
  break;
578
634
  }
579
635
  case "content_block_start": {
580
- const block = data.content_block as { type: string; id?: string; name?: string } | undefined;
636
+ const block = data.content_block as { type: string; id?: string; name?: string; data?: string } | undefined;
581
637
  if (!block) break;
582
638
  currentBlockType = block.type;
583
639
  if (block.type === "tool_use") {
@@ -585,6 +641,10 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
585
641
  currentToolCallName = toolNames.fromWire(block.name ?? "");
586
642
  yield { type: "tool_call_start", id: currentToolCallId, name: currentToolCallName };
587
643
  }
644
+ if (block.type === "redacted_thinking" && typeof block.data === "string") {
645
+ // Opaque redacted block: replay verbatim later or tool-use turns 400.
646
+ yield { type: "redacted_thinking", data: block.data };
647
+ }
588
648
  break;
589
649
  }
590
650
  case "content_block_delta": {
@@ -594,6 +654,10 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
594
654
  yield { type: "text_delta", text: delta.text };
595
655
  } else if (delta.type === "thinking_delta" && typeof delta.thinking === "string") {
596
656
  yield { type: "thinking_delta", thinking: delta.thinking };
657
+ } else if (delta.type === "signature_delta" && typeof delta.signature === "string" && currentBlockType === "thinking") {
658
+ // Arrives once, just before the thinking block's content_block_stop; block-scoped
659
+ // so a stray signature on a non-thinking block can never be captured.
660
+ yield { type: "thinking_signature", signature: delta.signature };
597
661
  } else if (delta.type === "input_json_delta" && typeof delta.partial_json === "string") {
598
662
  yield { type: "tool_call_delta", arguments: delta.partial_json };
599
663
  }
@@ -603,8 +667,8 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
603
667
  if (currentBlockType === "tool_use") {
604
668
  yield { type: "tool_call_end" };
605
669
  currentToolCallId = "";
606
- currentBlockType = "";
607
670
  }
671
+ currentBlockType = "";
608
672
  break;
609
673
  }
610
674
  case "message_delta": {
@@ -634,11 +698,18 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
634
698
  async parseResponse(response: Response): Promise<AdapterEvent[]> {
635
699
  const json = await response.json() as Record<string, unknown>;
636
700
  const events: AdapterEvent[] = [];
637
- const content = json.content as { type: string; text?: string; id?: string; name?: string; input?: unknown }[] | undefined;
701
+ const content = json.content as { type: string; text?: string; id?: string; name?: string; input?: unknown; thinking?: string; signature?: string; data?: string }[] | undefined;
638
702
  if (content) {
639
703
  for (const block of content) {
640
704
  if (block.type === "text" && block.text) {
641
705
  events.push({ type: "text_delta", text: block.text });
706
+ } else if (block.type === "thinking" && typeof block.thinking === "string") {
707
+ events.push({ type: "thinking_delta", thinking: block.thinking });
708
+ if (typeof block.signature === "string" && block.signature) {
709
+ events.push({ type: "thinking_signature", signature: block.signature });
710
+ }
711
+ } else if (block.type === "redacted_thinking" && typeof block.data === "string") {
712
+ events.push({ type: "redacted_thinking", data: block.data });
642
713
  } else if (block.type === "tool_use") {
643
714
  events.push({ type: "tool_call_start", id: block.id ?? "", name: toolNames.fromWire(block.name ?? "") });
644
715
  events.push({ type: "tool_call_delta", arguments: JSON.stringify(block.input ?? {}) });
@@ -1,4 +1,4 @@
1
- import { redactSecretString } from "../../redact";
1
+ import { redactSecretString } from "../../lib/redact";
2
2
 
3
3
  const ABSOLUTE_PATH_PATTERN = /(?:\/Users\/[^ "';,]+|\/home\/[^ "';,]+|[A-Za-z]:\\Users\\[^ "';,]+)/g;
4
4
  // Cursor error messages can contain raw credential key=value pairs beyond what the shared
@@ -13,7 +13,7 @@ function sanitize(value: string): string {
13
13
 
14
14
  /**
15
15
  * Classify a Cursor transport/Connect/gRPC error message into an actionable category.
16
- * The returned prefix string is recognized by `src/errors.ts` `classifyError` keywords,
16
+ * The returned prefix string is recognized by `src/lib/errors.ts` `classifyError` keywords,
17
17
  * so bridge-level error mapping produces the right Codex error type (rate_limit, auth, etc.).
18
18
  */
19
19
  export function classifyCursorError(message: string): string {
@@ -30,7 +30,7 @@ import {
30
30
  type InteractionQuery,
31
31
  type InteractionResponse,
32
32
  } from "./gen/agent_pb";
33
- import { debugProviderDiagnostic } from "../../debug";
33
+ import { debugProviderDiagnostic } from "../../lib/debug";
34
34
  import { mcpArgsFromToolCall } from "./protobuf-events";
35
35
  import { OCX_RESPONSES_TOOL_PROVIDER } from "./tool-definitions";
36
36
  import { cursorUnsafeNativeLocalExecEnabled, handleCursorNativeExec, handleCursorNativeKv, type CursorNativeExecContext } from "./native-exec";
@@ -1,9 +1,9 @@
1
1
  import type { CursorRunRequest, CursorServerMessage } from "./types";
2
2
  import type { CursorTransport, CursorTransportFactory, CursorTransportFactoryInput } from "./transport";
3
- import { abortError, sleepWithAbort } from "../../upstream-retry";
3
+ import { abortError, sleepWithAbort } from "../../lib/upstream-retry";
4
4
 
5
5
  // Compat: historical name for the shared abortable sleep, kept for external callers.
6
- export { sleepWithAbort as abortAwareSleep } from "../../upstream-retry";
6
+ export { sleepWithAbort as abortAwareSleep } from "../../lib/upstream-retry";
7
7
 
8
8
  export const CURSOR_RETRY_ATTEMPTS = 3;
9
9
  export const CURSOR_RETRY_BASE_MS = 250;
@@ -1,4 +1,4 @@
1
- import { redactSecretString } from "../redact";
1
+ import { redactSecretString } from "../lib/redact";
2
2
 
3
3
  const ABSOLUTE_PATH_PATTERN = /(?:\/Users\/[^ "';,]+|\/home\/[^ "';,]+|\/root\/[^ "';,]*|[A-Za-z]:\\Users\\[^ "';,]+)/g;
4
4
 
@@ -1,6 +1,6 @@
1
1
  import type { AdapterFetchContext, AdapterRequest } from "./base";
2
2
  import { isQuotaExhaustedBody, retryableGoogleStatus, safeGoogleHttpErrorMessage } from "./google-errors";
3
- import { abortError, sleepWithAbort } from "../upstream-retry";
3
+ import { abortError, sleepWithAbort } from "../lib/upstream-retry";
4
4
 
5
5
  const GOOGLE_RETRY_ATTEMPTS = 3;
6
6
  const GOOGLE_RETRY_BASE_MS = 250;
@@ -1,4 +1,4 @@
1
- import { redactSecretString } from "../redact";
1
+ import { redactSecretString } from "../lib/redact";
2
2
 
3
3
  /** Gemini/Vertex finishReason values that mean the turn was cut off, not cleanly stopped. */
4
4
  const TRUNCATION_REASONS = new Set(["MAX_TOKENS", "MALFORMED_FUNCTION_CALL"]);
@@ -1,5 +1,5 @@
1
1
  import type { AdapterFetchContext, AdapterRequest, ProviderAdapter } from "./base";
2
- import { debugDroppedFrame } from "../debug";
2
+ import { debugDroppedFrame } from "../lib/debug";
3
3
  import { createHash } from "node:crypto";
4
4
  import type {
5
5
  AdapterEvent,
@@ -1,4 +1,4 @@
1
- import { redactSecretString } from "../redact";
1
+ import { redactSecretString } from "../lib/redact";
2
2
 
3
3
  const ABSOLUTE_PATH_PATTERN = /(?:\/Users\/[^ "';,]+|\/home\/[^ "';,]+|[A-Za-z]:\\Users\\[^ "';,]+)/g;
4
4
  const DETAIL_KEYS = ["__type", "code", "error", "name", "message", "Message", "errorMessage"];
@@ -1,6 +1,6 @@
1
1
  import type { AdapterFetchContext, AdapterRequest } from "./base";
2
2
  import { safeKiroHttpErrorMessage } from "./kiro-errors";
3
- import { abortError, sleepWithAbort } from "../upstream-retry";
3
+ import { abortError, sleepWithAbort } from "../lib/upstream-retry";
4
4
 
5
5
  const KIRO_RETRY_ATTEMPTS = 3;
6
6
  const KIRO_RETRY_BASE_MS = 250;
@@ -1,4 +1,4 @@
1
- import { redactSecretString } from "../redact";
1
+ import { redactSecretString } from "../lib/redact";
2
2
 
3
3
  const REASON_KEYS = ["finish_reason", "finishReason", "stop_reason", "stopReason", "completionReason", "reason"];
4
4
  const TRUNCATION_PATTERN = /length|max[_-]?tokens?|truncat|incomplete|context_length/i;
@@ -1,6 +1,6 @@
1
1
  import { decodeEventStream } from "../lib/eventstream-decoder";
2
2
  import { estimateTokens } from "../lib/token-estimate";
3
- import { debugProviderDiagnostic } from "../debug";
3
+ import { debugProviderDiagnostic } from "../lib/debug";
4
4
  import { resolveKiroApiRegion, resolveKiroProfileArn } from "../oauth/kiro";
5
5
  import { KIRO_MODEL_CONTEXT_WINDOWS, normalizeKiroModelId } from "../providers/kiro-models";
6
6
  import { modelRecordValue } from "../reasoning-effort";
@@ -1,5 +1,5 @@
1
1
  import type { ProviderAdapter } from "./base";
2
- import { debugDroppedFrame } from "../debug";
2
+ import { debugDroppedFrame } from "../lib/debug";
3
3
  import type { AdapterEvent, OcxAssistantMessage, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTextContent, OcxThinkingContent, OcxToolCall, OcxUsage } from "../types";
4
4
  import { isAllowedToolChoice, modelInList, namespacedToolName, resolveToolChoiceWireName, toolAllowedByChoice } from "../types";
5
5
  import { mapReasoningEffort } from "../reasoning-effort";
@@ -195,7 +195,17 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
195
195
  }
196
196
  if (parsed.options.stopSequences !== undefined) body.stop = parsed.options.stopSequences;
197
197
  const reasoningEffort = mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning);
198
- if (reasoningEffort !== undefined) body.reasoning_effort = reasoningEffort;
198
+ if (reasoningEffort !== undefined) {
199
+ if (modelInList(provider.thinkingToggleModels, parsed.modelId)) {
200
+ // Vendor thinking-toggle wire (MiMo v2.x, GLM 5/5.1): the mapped value is the toggle
201
+ // state, sent as `thinking: {type}` — these models ignore/reject reasoning_effort.
202
+ if (reasoningEffort === "enabled" || reasoningEffort === "disabled") {
203
+ body.thinking = { type: reasoningEffort };
204
+ }
205
+ } else {
206
+ body.reasoning_effort = reasoningEffort;
207
+ }
208
+ }
199
209
  if (parsed.options.presencePenalty !== undefined && !modelInList(provider.noPenaltyModels, parsed.modelId)) {
200
210
  body.presence_penalty = parsed.options.presencePenalty;
201
211
  }