@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.
- package/README.md +1 -0
- package/bin/ocx.mjs +4 -4
- package/gui/dist/assets/index-ByGC8-Bm.css +1 -0
- package/gui/dist/assets/index-CkV5xFA8.js +15 -0
- package/gui/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/adapters/anthropic-image-guard.ts +195 -0
- package/src/adapters/anthropic.ts +85 -14
- package/src/adapters/cursor/cursor-errors.ts +2 -2
- package/src/adapters/cursor/live-transport.ts +1 -1
- package/src/adapters/cursor/transport-retry.ts +2 -2
- package/src/adapters/google-errors.ts +1 -1
- package/src/adapters/google-http.ts +1 -1
- package/src/adapters/google-truncation.ts +1 -1
- package/src/adapters/google.ts +1 -1
- package/src/adapters/kiro-errors.ts +1 -1
- package/src/adapters/kiro-retry.ts +1 -1
- package/src/adapters/kiro-truncation.ts +1 -1
- package/src/adapters/kiro.ts +1 -1
- package/src/adapters/openai-chat.ts +12 -2
- package/src/adapters/openai-responses.ts +126 -3
- package/src/bridge.ts +164 -7
- package/src/{doctor.ts → cli/doctor.ts} +21 -4
- package/src/{cli-help.ts → cli/help.ts} +1 -1
- package/src/cli/index.ts +584 -0
- package/src/{init.ts → cli/init.ts} +6 -6
- package/src/{cli-models.ts → cli/models.ts} +2 -2
- package/src/{cli-provider.ts → cli/provider.ts} +12 -8
- package/src/{star-prompt.ts → cli/star-prompt.ts} +1 -1
- package/src/{cli-status.ts → cli/status.ts} +7 -7
- package/src/cli.ts +9 -575
- package/src/{codex-account-label.ts → codex/account-label.ts} +1 -1
- package/src/{codex-account-lifecycle.ts → codex/account-lifecycle.ts} +6 -6
- package/src/{codex-account-store.ts → codex/account-store.ts} +2 -2
- package/src/{codex-account-usability.ts → codex/account-usability.ts} +4 -4
- package/src/{codex-auth-api.ts → codex/auth-api.ts} +18 -18
- package/src/{codex-auth-collision.ts → codex/auth-collision.ts} +4 -4
- package/src/{codex-auth-context.ts → codex/auth-context.ts} +9 -9
- package/src/{codex-catalog.ts → codex/catalog.ts} +49 -20
- package/src/codex/history-migration-guardian.ts +102 -0
- package/src/{codex-history-provider.ts → codex/history-provider.ts} +111 -7
- package/src/{codex-home.ts → codex/home.ts} +1 -1
- package/src/{codex-inject.ts → codex/inject.ts} +204 -26
- package/src/{codex-journal.ts → codex/journal.ts} +2 -2
- package/src/{codex-main-account.ts → codex/main-account.ts} +2 -2
- package/src/{model-cache.ts → codex/model-cache.ts} +1 -1
- package/src/{codex-paths.ts → codex/paths.ts} +2 -2
- package/src/{codex-plugins-doctor.ts → codex/plugins-doctor.ts} +2 -2
- package/src/{codex-refresh.ts → codex/refresh.ts} +4 -4
- package/src/{codex-routing.ts → codex/routing.ts} +8 -8
- package/src/{codex-shim.ts → codex/shim.ts} +6 -5
- package/src/{codex-sync.ts → codex/sync.ts} +4 -4
- package/src/{codex-websocket-registry.ts → codex/websocket-registry.ts} +1 -1
- package/src/config.ts +2 -0
- package/src/generated/jawcode-model-metadata.ts +2 -0
- package/src/{bun-runtime.ts → lib/bun-runtime.ts} +1 -1
- package/src/{crash-guard.ts → lib/crash-guard.ts} +1 -1
- package/src/{process-control.ts → lib/process-control.ts} +1 -1
- package/src/{service-secrets.ts → lib/service-secrets.ts} +1 -1
- package/src/oauth/callback-server.ts +1 -1
- package/src/oauth/google-antigravity.ts +7 -4
- package/src/oauth/index.ts +67 -16
- package/src/oauth/login-cli.ts +2 -2
- package/src/oauth/store.ts +236 -20
- package/src/oauth/token-guardian.ts +24 -20
- package/src/oauth/types.ts +16 -0
- package/src/providers/api-keys.ts +121 -0
- package/src/{provider-context-cap.ts → providers/context-cap.ts} +1 -1
- package/src/providers/derive.ts +2 -0
- package/src/providers/key-failover.ts +145 -0
- package/src/{provider-label.ts → providers/label.ts} +1 -1
- package/src/{provider-quota.ts → providers/quota.ts} +12 -7
- package/src/providers/registry.ts +66 -4
- package/src/responses/compaction.ts +117 -0
- package/src/responses/parser.ts +89 -13
- package/src/responses/reasoning-envelope.ts +52 -0
- package/src/responses/schema.ts +15 -3
- package/src/responses/state.ts +117 -2
- package/src/router.ts +2 -0
- package/src/server/auth-cors.ts +231 -0
- package/src/server/index.ts +523 -0
- package/src/server/lifecycle.ts +73 -0
- package/src/server/management-api.ts +628 -0
- package/src/{proxy-liveness.ts → server/proxy-liveness.ts} +1 -1
- package/src/server/relay.ts +534 -0
- package/src/server/request-decompress.ts +46 -0
- package/src/server/request-log.ts +310 -0
- package/src/server/responses.ts +775 -0
- package/src/{ws-bridge.ts → server/ws-bridge.ts} +44 -13
- package/src/service.ts +19 -11
- package/src/types.ts +27 -0
- package/src/{update.ts → update/index.ts} +5 -5
- package/src/{update-job.ts → update/job.ts} +7 -6
- package/src/{update-notify.ts → update/notify.ts} +3 -3
- package/src/{usage-debug.ts → usage/debug.ts} +3 -3
- package/src/{usage-log.ts → usage/log.ts} +3 -3
- package/src/{usage-summary.ts → usage/summary.ts} +3 -3
- package/src/{usage-totals.ts → usage/totals.ts} +1 -1
- package/src/vision/describe.ts +3 -3
- package/src/vision/index.ts +21 -1
- package/src/web-search/executor.ts +4 -4
- package/src/web-search/index.ts +1 -1
- package/src/web-search/loop.ts +84 -24
- package/gui/dist/assets/index-BcHhxo1I.css +0 -1
- package/gui/dist/assets/index-DCC1q_Jx.js +0 -15
- package/src/server.ts +0 -2501
- /package/src/{codex-account-runtime-state.ts → codex/account-runtime-state.ts} +0 -0
- /package/src/{codex-quota.ts → codex/quota.ts} +0 -0
- /package/src/{abort.ts → lib/abort.ts} +0 -0
- /package/src/{debug.ts → lib/debug.ts} +0 -0
- /package/src/{errors.ts → lib/errors.ts} +0 -0
- /package/src/{open-url.ts → lib/open-url.ts} +0 -0
- /package/src/{privacy.ts → lib/privacy.ts} +0 -0
- /package/src/{redact.ts → lib/redact.ts} +0 -0
- /package/src/{sidecar-tracker.ts → lib/sidecar-tracker.ts} +0 -0
- /package/src/{upstream-retry.ts → lib/upstream-retry.ts} +0 -0
- /package/src/{win-paths.ts → lib/win-paths.ts} +0 -0
- /package/src/{ports.ts → server/ports.ts} +0 -0
package/gui/dist/index.html
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
} catch (e) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
20
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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.
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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,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"]);
|
package/src/adapters/google.ts
CHANGED
|
@@ -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;
|
package/src/adapters/kiro.ts
CHANGED
|
@@ -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)
|
|
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
|
}
|