@bitkyc08/opencodex 2.8.2-preview.20260731 → 2.9.1
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 +24 -0
- package/bin/ocx.mjs +32 -4
- package/gui/dist/assets/index-CHwf3tTD.css +1 -0
- package/gui/dist/assets/index-CuVjugeE.js +67 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-normalize.ts +114 -20
- package/src/adapters/anthropic.ts +86 -5
- package/src/adapters/azure.ts +3 -3
- package/src/adapters/base.ts +7 -3
- package/src/adapters/cursor/discovery.ts +14 -4
- package/src/adapters/cursor/effort-map.ts +18 -6
- package/src/adapters/cursor/framing.ts +102 -27
- package/src/adapters/cursor/kv-store.ts +30 -3
- package/src/adapters/cursor/live-models.ts +22 -2
- package/src/adapters/cursor/live-transport.ts +245 -49
- package/src/adapters/cursor/mcp-manager.ts +105 -8
- package/src/adapters/cursor/native-exec-mcp.ts +5 -3
- package/src/adapters/cursor/native-exec-shell.ts +296 -14
- package/src/adapters/cursor/native-exec.ts +381 -33
- package/src/adapters/cursor/protobuf-events.ts +28 -1
- package/src/adapters/cursor/protobuf-request.ts +71 -39
- package/src/adapters/cursor/request-builder.ts +2 -2
- package/src/adapters/cursor/transport.ts +2 -0
- package/src/adapters/cursor.ts +13 -2
- package/src/adapters/google-antigravity-replay.ts +184 -17
- package/src/adapters/google.ts +58 -8
- package/src/adapters/kiro-thinking.ts +23 -9
- package/src/adapters/kiro-tools.ts +49 -18
- package/src/adapters/kiro.ts +377 -133
- package/src/adapters/mimo-free.ts +36 -4
- package/src/adapters/openai-chat.ts +89 -14
- package/src/adapters/openai-responses.ts +130 -14
- package/src/adapters/run-turn-queue.ts +7 -1
- package/src/bridge.ts +466 -69
- package/src/chat/outbound.ts +144 -38
- package/src/claude/inbound-debug.ts +53 -8
- package/src/claude/outbound.ts +224 -38
- package/src/cli/agent-driven.ts +34 -1
- package/src/cli/catalog-prewarm.ts +5 -2
- package/src/cli/doctor.ts +12 -0
- package/src/cli/export-command.ts +187 -0
- package/src/cli/help.ts +11 -0
- package/src/cli/index.ts +13 -3
- package/src/cli/opencode.ts +36 -151
- package/src/cli/star-prompt.ts +13 -4
- package/src/cli/status-oauth.ts +12 -2
- package/src/clients/config-export.ts +377 -0
- package/src/codex/account-runtime-state.ts +19 -1
- package/src/codex/account-store.ts +162 -82
- package/src/codex/auth-api.ts +467 -159
- package/src/codex/auth-context.ts +15 -2
- package/src/codex/catalog/aggregation.ts +15 -0
- package/src/codex/catalog/provider-fetch.ts +29 -0
- package/src/codex/catalog/sync.ts +7 -1
- package/src/codex/inject.ts +5 -5
- package/src/codex/main-account-cache.ts +8 -1
- package/src/codex/model-cache.ts +81 -2
- package/src/codex/pool-rotation.ts +39 -0
- package/src/codex/project-config-warnings.ts +12 -1
- package/src/codex/quota.ts +35 -3
- package/src/codex/routing.ts +46 -1
- package/src/codex/shim.ts +10 -4
- package/src/codex/subagent-model-fallback.ts +12 -0
- package/src/codex/websocket-registry.ts +27 -0
- package/src/combos/failover.ts +31 -1
- package/src/combos/request.ts +9 -0
- package/src/combos/resolve.ts +60 -4
- package/src/combos/types.ts +12 -0
- package/src/config.ts +510 -55
- package/src/github/star-state.ts +13 -1
- package/src/images/fulfill.ts +39 -1
- package/src/images/loop.ts +52 -12
- package/src/lib/admission.ts +83 -0
- package/src/lib/app-owned-memory-stores.ts +173 -0
- package/src/lib/app-owned-memory.ts +265 -0
- package/src/lib/bun-stream-caps.ts +31 -7
- package/src/lib/config-ownership.ts +33 -0
- package/src/lib/crash-guard.ts +65 -5
- package/src/lib/debug-log-buffer.ts +47 -6
- package/src/lib/errors.ts +3 -0
- package/src/lib/gcp-adc.ts +40 -2
- package/src/lib/injection-debug-log.ts +26 -2
- package/src/lib/sidecar-tracker.ts +5 -2
- package/src/lib/sse-decoder.ts +257 -37
- package/src/lib/state-store-registrations.ts +109 -0
- package/src/lib/state-store-sweeper.ts +184 -0
- package/src/lib/translator-budget.ts +356 -0
- package/src/lib/windows-secret-acl.ts +33 -12
- package/src/lib/winsw.ts +8 -1
- package/src/oauth/anthropic-routing.ts +31 -7
- package/src/oauth/google-antigravity.ts +2 -1
- package/src/oauth/health.ts +30 -12
- package/src/oauth/index.ts +127 -23
- package/src/oauth/kiro.ts +10 -2
- package/src/oauth/store.ts +165 -18
- package/src/oauth/token-guardian.ts +43 -4
- package/src/oauth/types.ts +2 -1
- package/src/providers/base-url-choices.ts +10 -0
- package/src/providers/derive.ts +12 -0
- package/src/providers/key-failover.ts +12 -0
- package/src/providers/openai-sidecar.ts +4 -1
- package/src/providers/quota.ts +68 -7
- package/src/providers/registry.ts +279 -3
- package/src/responses/parser.ts +5 -1
- package/src/responses/spill-store.ts +394 -0
- package/src/responses/state.ts +520 -102
- package/src/router.ts +18 -1
- package/src/server/adapter-resolve.ts +20 -3
- package/src/server/auth-cors.ts +121 -28
- package/src/server/chat-completions.ts +57 -12
- package/src/server/claude-messages.ts +85 -13
- package/src/server/index.ts +239 -97
- package/src/server/lifecycle.ts +155 -25
- package/src/server/management/agent-settings-routes.ts +75 -32
- package/src/server/management/api-key-usage.ts +167 -0
- package/src/server/management/body.ts +35 -0
- package/src/server/management/combo-routes.ts +5 -1
- package/src/server/management/config-routes.ts +37 -12
- package/src/server/management/logs-usage-routes.ts +41 -21
- package/src/server/management/model-routes.ts +173 -53
- package/src/server/management/oauth-account-routes.ts +115 -26
- package/src/server/management/provider-routes.ts +56 -6
- package/src/server/management/sidebar-routes.ts +50 -1
- package/src/server/management/system-restart.ts +13 -6
- package/src/server/management/system-routes.ts +15 -3
- package/src/server/management/usage-summary-cache.ts +86 -0
- package/src/server/management-api.ts +39 -5
- package/src/server/management-auth.ts +65 -14
- package/src/server/port-reclaim.ts +58 -12
- package/src/server/ports.ts +2 -0
- package/src/server/proxy-liveness.ts +51 -12
- package/src/server/relay-eager.ts +20 -4
- package/src/server/relay.ts +548 -154
- package/src/server/request-decompress.ts +51 -4
- package/src/server/request-log.ts +134 -15
- package/src/server/responses/collaboration.ts +15 -4
- package/src/server/responses/compact.ts +3 -0
- package/src/server/responses/core.ts +241 -66
- package/src/server/responses-image-gen-repair.ts +19 -5
- package/src/server/responses-item-id-repair.ts +23 -5
- package/src/server/sse-payload-rewrite.ts +71 -12
- package/src/server/startup-health-cache.ts +14 -1
- package/src/server/system-env.ts +8 -1
- package/src/server/windows-tcp-drop.ts +15 -5
- package/src/server/ws-bridge.ts +25 -0
- package/src/service.ts +23 -3
- package/src/storage/policy-job.ts +93 -23
- package/src/storage/policy-worker.ts +6 -0
- package/src/storage/restore-job.ts +62 -16
- package/src/storage/restore-worker.ts +6 -0
- package/src/storage/storage-mutation-coordinator.ts +36 -6
- package/src/storage/worker-lifecycle.ts +181 -47
- package/src/tray/windows.ts +45 -18
- package/src/types.ts +34 -6
- package/src/update/index.ts +24 -5
- package/src/update/job.ts +598 -73
- package/src/usage/log.ts +115 -17
- package/src/usage/summary.ts +67 -2
- package/src/vision/index.ts +112 -22
- package/src/web-search/loop.ts +38 -6
- package/src/web-search/progress-stream.ts +14 -3
- package/gui/dist/assets/index-BHsKRFh9.css +0 -1
- package/gui/dist/assets/index-GC0Vlu1Z.js +0 -67
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-CuVjugeE.js"></script>
|
|
20
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CHwf3tTD.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.
|
|
3
|
+
"version": "2.9.1",
|
|
4
4
|
"description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./bin/package-main.mjs",
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
TOTAL_IMAGE_BASE64_BUDGET,
|
|
21
21
|
type ImageBlockRef,
|
|
22
22
|
} from "./anthropic-image-guard";
|
|
23
|
+
import { enforceAppOwnedMemoryBudget } from "../lib/app-owned-memory";
|
|
23
24
|
|
|
24
25
|
/** One ladder position: dimension cap, JPEG quality attempts, per-image base64 cap. */
|
|
25
26
|
export interface TierSpec {
|
|
@@ -98,50 +99,143 @@ type ProcessResult =
|
|
|
98
99
|
* not entry count. Entries are immutable snapshots — demotions write NEW tier-suffixed
|
|
99
100
|
* keys, never mutate stored values.
|
|
100
101
|
*/
|
|
101
|
-
const
|
|
102
|
+
export const IMAGE_NORMALIZE_CACHE_MAX_BYTES = 64 * MiB;
|
|
103
|
+
const CACHE_MAX_ENTRIES = 4_096;
|
|
104
|
+
const CACHE_MAX_ENTRY_BYTES = 20 * MiB;
|
|
102
105
|
// "pass" = validated pass-through; "miss" = this position's ladder cannot meet its hard
|
|
103
106
|
// cap for these bytes (skip straight to the next position — C-gate round 2, blocker 1).
|
|
104
|
-
|
|
107
|
+
type CacheValue = { data: string; mediaType: string } | "pass" | "miss";
|
|
108
|
+
interface CacheEntry {
|
|
109
|
+
value: CacheValue;
|
|
110
|
+
sizeBytes: number;
|
|
111
|
+
metadataBytes: number;
|
|
112
|
+
storedAt: number;
|
|
113
|
+
}
|
|
114
|
+
interface NormalizeCacheLimits {
|
|
115
|
+
maxBytes: number;
|
|
116
|
+
maxEntries: number;
|
|
117
|
+
maxEntryBytes: number;
|
|
118
|
+
}
|
|
119
|
+
const DEFAULT_CACHE_LIMITS: NormalizeCacheLimits = {
|
|
120
|
+
maxBytes: IMAGE_NORMALIZE_CACHE_MAX_BYTES,
|
|
121
|
+
maxEntries: CACHE_MAX_ENTRIES,
|
|
122
|
+
maxEntryBytes: CACHE_MAX_ENTRY_BYTES,
|
|
123
|
+
};
|
|
124
|
+
const cacheEncoder = new TextEncoder();
|
|
125
|
+
const cache = new Map<string, CacheEntry>();
|
|
126
|
+
let cacheLimits = { ...DEFAULT_CACHE_LIMITS };
|
|
105
127
|
let cacheBytes = 0;
|
|
128
|
+
let cacheMetadataBytes = 0;
|
|
129
|
+
let cacheSentinelEntries = 0;
|
|
106
130
|
let encodeCalls = 0;
|
|
107
131
|
|
|
108
|
-
function
|
|
109
|
-
const
|
|
132
|
+
function cacheEntry(key: string, value: CacheValue): CacheEntry {
|
|
133
|
+
const keyBytes = cacheEncoder.encode(key).byteLength;
|
|
134
|
+
const valueBytes = typeof value === "string"
|
|
135
|
+
? cacheEncoder.encode(value).byteLength
|
|
136
|
+
: cacheEncoder.encode(value.mediaType).byteLength + cacheEncoder.encode(value.data).byteLength;
|
|
137
|
+
const metadataBytes = keyBytes + (typeof value === "string"
|
|
138
|
+
? cacheEncoder.encode(value).byteLength
|
|
139
|
+
: cacheEncoder.encode(value.mediaType).byteLength);
|
|
140
|
+
return { value, sizeBytes: keyBytes + valueBytes, metadataBytes, storedAt: Date.now() };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function deleteCacheEntry(key: string): number {
|
|
144
|
+
const entry = cache.get(key);
|
|
145
|
+
if (!entry) return 0;
|
|
146
|
+
cache.delete(key);
|
|
147
|
+
cacheBytes -= entry.sizeBytes;
|
|
148
|
+
cacheMetadataBytes -= entry.metadataBytes;
|
|
149
|
+
if (typeof entry.value === "string") cacheSentinelEntries--;
|
|
150
|
+
return entry.sizeBytes;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function cachePut(key: string, value: CacheValue): boolean {
|
|
154
|
+
const next = cacheEntry(key, value);
|
|
155
|
+
if (
|
|
156
|
+
next.sizeBytes > cacheLimits.maxEntryBytes
|
|
157
|
+
|| next.sizeBytes > cacheLimits.maxBytes
|
|
158
|
+
|| cacheLimits.maxEntries <= 0
|
|
159
|
+
) return false;
|
|
110
160
|
const existing = cache.get(key);
|
|
111
161
|
if (existing !== undefined) {
|
|
112
|
-
|
|
113
|
-
cache.delete(key); // re-insert refreshes recency and prevents double-count on concurrent misses
|
|
162
|
+
deleteCacheEntry(key); // re-insert refreshes recency and prevents double-count on concurrent misses
|
|
114
163
|
}
|
|
115
|
-
while (
|
|
116
|
-
const oldest = cache.keys().next().value
|
|
117
|
-
|
|
118
|
-
cacheBytes -= typeof evicted === "string" || evicted === undefined ? 0 : evicted.data.length;
|
|
119
|
-
cache.delete(oldest);
|
|
164
|
+
while (cache.size + 1 > cacheLimits.maxEntries || cacheBytes + next.sizeBytes > cacheLimits.maxBytes) {
|
|
165
|
+
const oldest = cache.keys().next().value;
|
|
166
|
+
if (oldest === undefined || deleteCacheEntry(oldest) === 0) return false;
|
|
120
167
|
}
|
|
121
|
-
cache.set(key,
|
|
122
|
-
cacheBytes +=
|
|
168
|
+
cache.set(key, next);
|
|
169
|
+
cacheBytes += next.sizeBytes;
|
|
170
|
+
cacheMetadataBytes += next.metadataBytes;
|
|
171
|
+
if (typeof value === "string") cacheSentinelEntries++;
|
|
172
|
+
enforceAppOwnedMemoryBudget();
|
|
173
|
+
return true;
|
|
123
174
|
}
|
|
124
175
|
|
|
125
176
|
/** Read a cache entry, refreshing its recency (true LRU, C-gate round 1 blocker 5). */
|
|
126
|
-
function cacheGet(key: string):
|
|
127
|
-
const
|
|
128
|
-
if (
|
|
177
|
+
function cacheGet(key: string): CacheValue | undefined {
|
|
178
|
+
const entry = cache.get(key);
|
|
179
|
+
if (entry !== undefined) {
|
|
129
180
|
cache.delete(key);
|
|
130
|
-
|
|
181
|
+
entry.storedAt = Date.now();
|
|
182
|
+
cache.set(key, entry);
|
|
131
183
|
}
|
|
132
|
-
return value;
|
|
184
|
+
return entry?.value;
|
|
133
185
|
}
|
|
134
186
|
|
|
135
187
|
/** Test hooks: encoder-invocation counter + cache reset (no production caller). */
|
|
136
|
-
export function getNormalizeStatsForTests(): {
|
|
137
|
-
|
|
188
|
+
export function getNormalizeStatsForTests(): {
|
|
189
|
+
encodeCalls: number;
|
|
190
|
+
cacheEntries: number;
|
|
191
|
+
cacheBytes: number;
|
|
192
|
+
sentinelEntries: number;
|
|
193
|
+
metadataBytes: number;
|
|
194
|
+
oldestAt: number | null;
|
|
195
|
+
} {
|
|
196
|
+
return {
|
|
197
|
+
encodeCalls,
|
|
198
|
+
cacheEntries: cache.size,
|
|
199
|
+
cacheBytes,
|
|
200
|
+
sentinelEntries: cacheSentinelEntries,
|
|
201
|
+
metadataBytes: cacheMetadataBytes,
|
|
202
|
+
oldestAt: cache.values().next().value?.storedAt ?? null,
|
|
203
|
+
};
|
|
138
204
|
}
|
|
139
205
|
export function resetNormalizeStateForTests(): void {
|
|
140
206
|
cache.clear();
|
|
141
207
|
cacheBytes = 0;
|
|
208
|
+
cacheMetadataBytes = 0;
|
|
209
|
+
cacheSentinelEntries = 0;
|
|
142
210
|
encodeCalls = 0;
|
|
143
211
|
}
|
|
144
212
|
|
|
213
|
+
export function setNormalizeCacheLimitsForTests(limits?: Partial<NormalizeCacheLimits>): void {
|
|
214
|
+
resetNormalizeStateForTests();
|
|
215
|
+
cacheLimits = limits ? { ...DEFAULT_CACHE_LIMITS, ...limits } : { ...DEFAULT_CACHE_LIMITS };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function anthropicImageNormalizeRetainedStoreSnapshot(): {
|
|
219
|
+
count: number;
|
|
220
|
+
bytes: number;
|
|
221
|
+
evictableBytes: number;
|
|
222
|
+
pinnedBytes: number;
|
|
223
|
+
oldestAt: number | null;
|
|
224
|
+
} {
|
|
225
|
+
return {
|
|
226
|
+
count: cache.size,
|
|
227
|
+
bytes: cacheBytes,
|
|
228
|
+
evictableBytes: cacheBytes,
|
|
229
|
+
pinnedBytes: 0,
|
|
230
|
+
oldestAt: cache.values().next().value?.storedAt ?? null,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function evictOldestAnthropicImageNormalizeForBudget(): number {
|
|
235
|
+
const oldest = cache.keys().next().value;
|
|
236
|
+
return oldest === undefined ? 0 : deleteCacheEntry(oldest);
|
|
237
|
+
}
|
|
238
|
+
|
|
145
239
|
/** Default encoder: Bun.Image resize-to-fit + JPEG at the given quality. */
|
|
146
240
|
const bunImageEncode: EncodeFn = async (input, spec, quality) => {
|
|
147
241
|
const image = new Bun.Image(input);
|
|
@@ -22,6 +22,7 @@ import { neutralizeIdentity } from "./identity";
|
|
|
22
22
|
import { CLAUDE_CODE_HEADERS, claudeCodeSessionId } from "./client-fingerprint";
|
|
23
23
|
import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge";
|
|
24
24
|
import { decodeServerSentEvents } from "../lib/sse-decoder";
|
|
25
|
+
import { isTranslatorBudgetExceededError, retainTranslatedEventBatch, type TranslatorBudget } from "../lib/translator-budget";
|
|
25
26
|
|
|
26
27
|
/** Map a user content part to an Anthropic content block (text or image source). */
|
|
27
28
|
function toAnthropicContentPart(p: OcxContentPart): unknown {
|
|
@@ -266,6 +267,16 @@ function synthesizeToolUseId(): string {
|
|
|
266
267
|
return `toolu_${crypto.randomUUID().replace(/-/g, "").slice(0, 24)}`;
|
|
267
268
|
}
|
|
268
269
|
|
|
270
|
+
/**
|
|
271
|
+
* A tool_use id that a client can actually echo back. `??` only catches a missing
|
|
272
|
+
* field, so an Anthropic-compatible relay that sends `""` or `" "` produced a
|
|
273
|
+
* call whose id round-trips as blank — the next turn then cannot pair the result
|
|
274
|
+
* with its call. Treat blank as absent and synthesize (#765).
|
|
275
|
+
*/
|
|
276
|
+
function usableToolUseId(id: unknown): string {
|
|
277
|
+
return typeof id === "string" && id.trim() ? id : synthesizeToolUseId();
|
|
278
|
+
}
|
|
279
|
+
|
|
269
280
|
function toolUseArguments(input: unknown): string {
|
|
270
281
|
if (typeof input === "string") {
|
|
271
282
|
const trimmed = input.trim();
|
|
@@ -284,6 +295,23 @@ function toolUseArguments(input: unknown): string {
|
|
|
284
295
|
return JSON.stringify(input ?? {});
|
|
285
296
|
}
|
|
286
297
|
|
|
298
|
+
/**
|
|
299
|
+
* Whether arguments assembled from a stream's `input_json_delta` fragments are usable.
|
|
300
|
+
* A tool block that sent no fragments at all is fine — that is a no-argument call. Anything
|
|
301
|
+
* else has to parse, because unlike the non-stream path the fragments have already been
|
|
302
|
+
* forwarded to the client and cannot be repaired after the fact.
|
|
303
|
+
*/
|
|
304
|
+
function streamedToolArgumentsParse(assembled: string): boolean {
|
|
305
|
+
const trimmed = assembled.trim();
|
|
306
|
+
if (!trimmed) return true;
|
|
307
|
+
try {
|
|
308
|
+
JSON.parse(trimmed);
|
|
309
|
+
return true;
|
|
310
|
+
} catch {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
287
315
|
function anthropicKeyUsesBearer(provider: OcxProviderConfig): boolean {
|
|
288
316
|
return provider.apiKeyTransport === "bearer";
|
|
289
317
|
}
|
|
@@ -756,15 +784,17 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
756
784
|
return { url, method: "POST", headers, body: JSON.stringify(body) };
|
|
757
785
|
},
|
|
758
786
|
|
|
759
|
-
async *parseStream(response: Response): AsyncGenerator<AdapterEvent> {
|
|
787
|
+
async *parseStream(response: Response, budget: TranslatorBudget): AsyncGenerator<AdapterEvent> {
|
|
760
788
|
if (!response.body) {
|
|
761
789
|
yield { type: "error", message: "No response body" };
|
|
762
790
|
return;
|
|
763
791
|
}
|
|
764
792
|
|
|
793
|
+
const budgetEncoder = new TextEncoder();
|
|
765
794
|
let currentBlockType = "";
|
|
766
795
|
let currentToolCallId = "";
|
|
767
796
|
let currentToolCallName = "";
|
|
797
|
+
let currentToolCallJson = "";
|
|
768
798
|
let pendingUsage: Record<string, number> | undefined;
|
|
769
799
|
let pendingStopReason: string | undefined;
|
|
770
800
|
let emittedDone = false;
|
|
@@ -779,7 +809,8 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
779
809
|
};
|
|
780
810
|
};
|
|
781
811
|
|
|
782
|
-
|
|
812
|
+
try {
|
|
813
|
+
for await (const record of decodeServerSentEvents(response.body, { includeComments: true, translatorBudget: budget })) {
|
|
783
814
|
if (record.kind === "comment") {
|
|
784
815
|
yield { type: "heartbeat" };
|
|
785
816
|
continue;
|
|
@@ -806,8 +837,10 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
806
837
|
if (!block) break;
|
|
807
838
|
currentBlockType = block.type;
|
|
808
839
|
if (block.type === "tool_use") {
|
|
809
|
-
currentToolCallId = block.id
|
|
840
|
+
currentToolCallId = usableToolUseId(block.id);
|
|
810
841
|
currentToolCallName = toolNames.fromWire(block.name ?? "");
|
|
842
|
+
currentToolCallJson = "";
|
|
843
|
+
budget.openCall(currentToolCallId);
|
|
811
844
|
yield { type: "tool_call_start", id: currentToolCallId, name: currentToolCallName };
|
|
812
845
|
}
|
|
813
846
|
if (block.type === "redacted_thinking" && typeof block.data === "string") {
|
|
@@ -833,14 +866,43 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
833
866
|
// so a stray signature on a non-thinking block can never be captured.
|
|
834
867
|
yield { type: "thinking_signature", signature: delta.signature };
|
|
835
868
|
} else if (delta.type === "input_json_delta" && typeof delta.partial_json === "string" && currentBlockType === "tool_use") {
|
|
869
|
+
// Forwarded immediately: the bridge maps each delta to a client-visible
|
|
870
|
+
// response.function_call_arguments.delta frame, so withholding fragments until
|
|
871
|
+
// block close would leave a started call showing empty arguments. A copy is kept
|
|
872
|
+
// to validate the assembled payload at content_block_stop.
|
|
873
|
+
const previousBytes = budgetEncoder.encode(currentToolCallJson).byteLength;
|
|
874
|
+
const nextBytes = previousBytes + budgetEncoder.encode(delta.partial_json).byteLength;
|
|
875
|
+
const reservation = budget.reserveTransient(nextBytes, { kind: "tool_args", callId: currentToolCallId });
|
|
876
|
+
try {
|
|
877
|
+
currentToolCallJson += delta.partial_json;
|
|
878
|
+
reservation.commitRetained();
|
|
879
|
+
budget.releaseRetained(previousBytes, { kind: "tool_args", callId: currentToolCallId });
|
|
880
|
+
} catch (error) {
|
|
881
|
+
reservation.release();
|
|
882
|
+
throw error;
|
|
883
|
+
}
|
|
836
884
|
yield { type: "tool_call_delta", arguments: delta.partial_json };
|
|
837
885
|
}
|
|
838
886
|
break;
|
|
839
887
|
}
|
|
840
888
|
case "content_block_stop": {
|
|
841
889
|
if (currentBlockType === "tool_use") {
|
|
890
|
+
// The non-stream path repairs an unparseable payload in toolUseArguments(); the
|
|
891
|
+
// stream cannot, because the fragments are already downstream. Fail the turn
|
|
892
|
+
// instead of ending a tool call whose arguments will not parse — the bridge's
|
|
893
|
+
// terminal-error path cancels the open call (status incomplete) rather than
|
|
894
|
+
// completing it before response.failed (#765).
|
|
895
|
+
if (!streamedToolArgumentsParse(currentToolCallJson)) {
|
|
896
|
+
yield {
|
|
897
|
+
type: "error",
|
|
898
|
+
message: "Anthropic stream sent malformed tool_use arguments (invalid JSON)",
|
|
899
|
+
};
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
842
902
|
yield { type: "tool_call_end" };
|
|
903
|
+
budget.closeCall(currentToolCallId);
|
|
843
904
|
currentToolCallId = "";
|
|
905
|
+
currentToolCallJson = "";
|
|
844
906
|
}
|
|
845
907
|
currentBlockType = "";
|
|
846
908
|
break;
|
|
@@ -863,6 +925,18 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
863
925
|
}
|
|
864
926
|
}
|
|
865
927
|
}
|
|
928
|
+
} catch (error) {
|
|
929
|
+
if (!isTranslatorBudgetExceededError(error)) throw error;
|
|
930
|
+
yield {
|
|
931
|
+
type: "error",
|
|
932
|
+
status: 502,
|
|
933
|
+
errorType: "upstream_error",
|
|
934
|
+
code: "translation_buffer_limit",
|
|
935
|
+
message: "upstream translation buffer exceeded the safe limit",
|
|
936
|
+
};
|
|
937
|
+
} finally {
|
|
938
|
+
if (currentToolCallId) budget.closeCall(currentToolCallId);
|
|
939
|
+
}
|
|
866
940
|
if (!emittedDone) {
|
|
867
941
|
// Fail closed on transport EOF. Compatible providers may omit message_stop after message_delta.stop_reason.
|
|
868
942
|
if (pendingStopReason !== undefined) {
|
|
@@ -883,8 +957,11 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
883
957
|
}
|
|
884
958
|
},
|
|
885
959
|
|
|
886
|
-
async parseResponse(response: Response): Promise<AdapterEvent[]> {
|
|
960
|
+
async parseResponse(response: Response, budget: TranslatorBudget): Promise<AdapterEvent[]> {
|
|
887
961
|
const json = await response.json() as Record<string, unknown>;
|
|
962
|
+
const responseBytes = new TextEncoder().encode(JSON.stringify(json)).byteLength;
|
|
963
|
+
budget.chargeRetained(responseBytes, { kind: "retained_collectors" });
|
|
964
|
+
try {
|
|
888
965
|
const events: AdapterEvent[] = [];
|
|
889
966
|
const content = json.content as { type: string; text?: string; id?: string; name?: string; input?: unknown; thinking?: string; reasoning?: string; signature?: string; data?: string }[] | undefined;
|
|
890
967
|
if (content) {
|
|
@@ -901,7 +978,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
901
978
|
} else if (block.type === "redacted_thinking" && typeof block.data === "string") {
|
|
902
979
|
events.push({ type: "redacted_thinking", data: block.data });
|
|
903
980
|
} else if (block.type === "tool_use") {
|
|
904
|
-
const id = block.id
|
|
981
|
+
const id = usableToolUseId(block.id);
|
|
905
982
|
events.push({ type: "tool_call_start", id, name: toolNames.fromWire(block.name ?? "") });
|
|
906
983
|
events.push({ type: "tool_call_delta", arguments: toolUseArguments(block.input) });
|
|
907
984
|
events.push({ type: "tool_call_end" });
|
|
@@ -915,7 +992,11 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
915
992
|
usage: usageFromAnthropic(usage),
|
|
916
993
|
...(stopReason ? { stopReason } : {}),
|
|
917
994
|
});
|
|
995
|
+
retainTranslatedEventBatch(events, budget);
|
|
918
996
|
return events;
|
|
997
|
+
} finally {
|
|
998
|
+
budget.releaseRetained(responseBytes, { kind: "retained_collectors" });
|
|
999
|
+
}
|
|
919
1000
|
},
|
|
920
1001
|
|
|
921
1002
|
};
|
package/src/adapters/azure.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ProviderAdapter } from "./base";
|
|
1
|
+
import type { IncomingMeta, ProviderAdapter } from "./base";
|
|
2
2
|
import type { OcxParsedRequest, OcxProviderConfig } from "../types";
|
|
3
3
|
import { createResponsesPassthroughAdapter } from "./openai-responses";
|
|
4
4
|
|
|
@@ -12,7 +12,7 @@ export function createAzureAdapter(provider: OcxProviderConfig): ProviderAdapter
|
|
|
12
12
|
...inner,
|
|
13
13
|
name: "azure-openai",
|
|
14
14
|
|
|
15
|
-
async buildRequest(parsed: OcxParsedRequest) {
|
|
15
|
+
async buildRequest(parsed: OcxParsedRequest, incoming: IncomingMeta) {
|
|
16
16
|
if (provider.authMode === "forward") {
|
|
17
17
|
throw new Error("azure-openai does not support forward auth mode");
|
|
18
18
|
}
|
|
@@ -20,7 +20,7 @@ export function createAzureAdapter(provider: OcxProviderConfig): ProviderAdapter
|
|
|
20
20
|
throw new Error("azure-openai requires a non-empty apiKey");
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
const request = await inner.buildRequest(parsed);
|
|
23
|
+
const request = await inner.buildRequest(parsed, incoming);
|
|
24
24
|
const unresolvedPlaceholder = request.url.match(/\{[^}]*\}/)?.[0] ?? request.url.match(/[{}]/)?.[0];
|
|
25
25
|
if (unresolvedPlaceholder) {
|
|
26
26
|
throw new Error(`azure-openai baseUrl contains unresolved ${unresolvedPlaceholder} — set your real resource URL`);
|
package/src/adapters/base.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { AdapterEvent, OcxParsedRequest } from "../types";
|
|
2
|
+
import type { TranslatorBudget } from "../lib/translator-budget";
|
|
2
3
|
|
|
3
4
|
/** Metadata about the caller's incoming request, for auth-forwarding adapters. */
|
|
4
5
|
export interface IncomingMeta {
|
|
5
6
|
headers: Headers;
|
|
7
|
+
translatorBudget: TranslatorBudget;
|
|
6
8
|
abortSignal?: AbortSignal;
|
|
7
9
|
/**
|
|
8
10
|
* Image-normalization ladder bias for upstream-413 tightened retries: every image
|
|
@@ -26,12 +28,12 @@ export interface ProviderAdapter {
|
|
|
26
28
|
* (e.g. Vertex AI ADC token) return a Promise. Sync adapters return the object directly; callers
|
|
27
29
|
* must `await` the result (awaiting a non-Promise is a no-op).
|
|
28
30
|
*/
|
|
29
|
-
buildRequest(parsed: OcxParsedRequest, incoming
|
|
31
|
+
buildRequest(parsed: OcxParsedRequest, incoming: IncomingMeta): AdapterRequest | Promise<AdapterRequest>;
|
|
30
32
|
|
|
31
33
|
fetchResponse?(request: AdapterRequest, ctx?: AdapterFetchContext): Promise<Response>;
|
|
32
34
|
|
|
33
|
-
parseStream(response: Response): AsyncGenerator<AdapterEvent>;
|
|
34
|
-
parseResponse?(response: Response): Promise<AdapterEvent[]>;
|
|
35
|
+
parseStream(response: Response, budget: TranslatorBudget): AsyncGenerator<AdapterEvent>;
|
|
36
|
+
parseResponse?(response: Response, budget: TranslatorBudget): Promise<AdapterEvent[]>;
|
|
35
37
|
runTurn?(
|
|
36
38
|
parsed: OcxParsedRequest,
|
|
37
39
|
incoming: IncomingMeta,
|
|
@@ -44,6 +46,8 @@ export interface AdapterRequest {
|
|
|
44
46
|
method: string;
|
|
45
47
|
headers: Record<string, string>;
|
|
46
48
|
body: string;
|
|
49
|
+
/** Releases observation of a serialized request body after its final fetch attempt settles. */
|
|
50
|
+
releaseBodyObservation?: () => void;
|
|
47
51
|
/** Exact reasoning parameter emitted by the adapter, for request-log diagnostics only. */
|
|
48
52
|
reasoningLog?: {
|
|
49
53
|
effectiveEffort: string;
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
CANONICAL_EFFORT_SUFFIXES,
|
|
3
|
+
cursorModelEffortLadder,
|
|
4
|
+
cursorWireModelIdWithEffort,
|
|
5
|
+
} from "./effort-map";
|
|
2
6
|
|
|
3
7
|
export interface CursorModelInfo {
|
|
4
8
|
id: string;
|
|
@@ -66,14 +70,20 @@ function stripCursorWirePrefix(id: string): string {
|
|
|
66
70
|
|
|
67
71
|
/**
|
|
68
72
|
* True when a configured Cursor base model should remain exposed after live GetUsableModels filtering.
|
|
69
|
-
* Live ids are full effort-suffixed variants (`claude-4.6-opus-high`); base ids match exactly
|
|
73
|
+
* Live ids are full effort-suffixed variants (`claude-4.6-opus-high`); base ids match exactly, the
|
|
74
|
+
* ordinary `{base}-{effort}` form, or Cursor's current `{base-without-fast}-{effort}-fast` form.
|
|
70
75
|
*/
|
|
71
76
|
export function isCursorModelAvailableForAccount(modelId: string, liveIds: readonly string[]): boolean {
|
|
72
77
|
return liveIds.some(raw => {
|
|
73
78
|
const id = stripCursorWirePrefix(raw);
|
|
74
79
|
if (id === modelId) return true;
|
|
75
|
-
const
|
|
76
|
-
|
|
80
|
+
for (const effort of CANONICAL_EFFORT_SUFFIXES) {
|
|
81
|
+
if (
|
|
82
|
+
id === `${modelId}-${effort}` ||
|
|
83
|
+
id === cursorWireModelIdWithEffort(modelId, effort)
|
|
84
|
+
) return true;
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
77
87
|
});
|
|
78
88
|
}
|
|
79
89
|
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Cursor model ids encode the reasoning effort as a suffix (`claude-4.6-opus-high`), and the available
|
|
5
5
|
* tiers differ per model — `claude-4.6-opus` tops out at `-max`, `claude-opus-4-8` at `-xhigh`,
|
|
6
|
-
* `claude-4.6-sonnet` only has `-medium`, and `composer`/`
|
|
7
|
-
*
|
|
6
|
+
* `claude-4.6-sonnet` only has `-medium`, and most `composer`/`gemini` models take no suffix at all.
|
|
7
|
+
* Grok Fast puts its mode marker after the effort (`grok-4.5-high-fast`). A bare id for a model that
|
|
8
|
+
* requires a suffix is rejected `ERROR_BAD_MODEL_NAME` (devlog 350.105).
|
|
8
9
|
*
|
|
9
10
|
* Canonical effort order is always low < medium < high < xhigh < max (max is the top tier, confirmed
|
|
10
11
|
* against Anthropic docs and Cursor's live lineup). Tiers are stored in ascending canonical order.
|
|
@@ -30,10 +31,10 @@ const CURSOR_MODEL_EFFORT_TIERS: Record<string, readonly string[]> = {
|
|
|
30
31
|
// GetUsableModels (2026-07-28) lists kimi-k3 only as effort-suffixed kimi-k3-{low,high,max};
|
|
31
32
|
// the bare id returns not_found. Tiers mirror the native Kimi provider's K3 ladder.
|
|
32
33
|
"kimi-k3": ["low", "high", "max"],
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
"grok-4.5": ["
|
|
36
|
-
"grok-4.5-fast": ["
|
|
34
|
+
// Cursor renamed the Grok 4.5 slugs to cursor-grok-4.5-{low,medium,high} and
|
|
35
|
+
// cursor-grok-4.5-{low,medium,high}-fast. The bare Fast id returns not_found.
|
|
36
|
+
"grok-4.5": ["low", "medium", "high"],
|
|
37
|
+
"grok-4.5-fast": ["low", "medium", "high"],
|
|
37
38
|
"gpt-5.1": ["low", "high"],
|
|
38
39
|
"gpt-5.1-codex-max": ["low", "medium", "high", "xhigh"],
|
|
39
40
|
"gpt-5.1-codex-mini": ["low", "high"],
|
|
@@ -113,3 +114,14 @@ export function cursorModelEffortLadder(baseModelId: string): string[] | undefin
|
|
|
113
114
|
export function cursorModelHasEffortTiers(baseModelId: string): boolean {
|
|
114
115
|
return (CURSOR_MODEL_EFFORT_TIERS[baseModelId]?.length ?? 0) > 0;
|
|
115
116
|
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Compose a Cursor wire id from a Codex-facing base id and effort tier.
|
|
120
|
+
* Fast variants put the mode after the effort; other models use the ordinary `{base}-{effort}` form.
|
|
121
|
+
*/
|
|
122
|
+
export function cursorWireModelIdWithEffort(baseModelId: string, effortSuffix: string): string {
|
|
123
|
+
if (baseModelId.endsWith("-fast")) {
|
|
124
|
+
return `${baseModelId.slice(0, -"-fast".length)}-${effortSuffix}-fast`;
|
|
125
|
+
}
|
|
126
|
+
return `${baseModelId}-${effortSuffix}`;
|
|
127
|
+
}
|