@bitkyc08/opencodex 2.8.0 → 2.9.0
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-u5eFOv2y.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 +126 -10
- 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 +143 -17
- 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/claude-desktop.ts +2 -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/init.ts +129 -102
- 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/effort.ts +16 -6
- package/src/codex/catalog/metadata.ts +6 -0
- package/src/codex/catalog/parsing.ts +3 -1
- package/src/codex/catalog/provider-fetch.ts +29 -0
- package/src/codex/catalog/sync.ts +64 -7
- package/src/codex/catalog.ts +2 -2
- 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/destination-policy.ts +12 -1
- 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/provider-outbound.ts +3 -0
- 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 +14 -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-credentials.ts +72 -1
- package/src/oauth/kiro.ts +23 -4
- 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/free-directory.ts +4 -1
- 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 +242 -100
- package/src/server/lifecycle.ts +155 -25
- package/src/server/management/agent-settings-routes.ts +79 -36
- 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 +42 -12
- package/src/server/management/logs-usage-routes.ts +41 -21
- package/src/server/management/model-routes.ts +188 -54
- package/src/server/management/oauth-account-routes.ts +115 -26
- package/src/server/management/provider-routes.ts +56 -6
- package/src/server/management/shared.ts +16 -3
- 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 +60 -14
- 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 +179 -13
- 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 +97 -25
- package/src/types.ts +39 -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-BDjpkcRN.js +0 -67
- package/gui/dist/assets/index-BHsKRFh9.css +0 -1
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-u5eFOv2y.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
|
@@ -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 {
|
|
@@ -250,6 +251,67 @@ function usesNativeAnthropicEndpoint(provider: OcxProviderConfig): boolean {
|
|
|
250
251
|
}
|
|
251
252
|
}
|
|
252
253
|
|
|
254
|
+
/** Normalize provider baseUrl paths ending in `/`, `/v1`, or `/v1/messages` to `{origin}/v1/messages`. */
|
|
255
|
+
export function anthropicMessagesUrl(baseUrl: string): string {
|
|
256
|
+
try {
|
|
257
|
+
new URL(baseUrl);
|
|
258
|
+
} catch {
|
|
259
|
+
throw new Error(`anthropic provider has malformed baseUrl: ${baseUrl}`);
|
|
260
|
+
}
|
|
261
|
+
const trimmed = baseUrl.trim().replace(/\/+$/, "");
|
|
262
|
+
const root = trimmed.replace(/\/v1\/messages\/?$/i, "").replace(/\/v1\/?$/i, "").replace(/\/+$/, "");
|
|
263
|
+
return `${root}/v1/messages`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function synthesizeToolUseId(): string {
|
|
267
|
+
return `toolu_${crypto.randomUUID().replace(/-/g, "").slice(0, 24)}`;
|
|
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
|
+
|
|
280
|
+
function toolUseArguments(input: unknown): string {
|
|
281
|
+
if (typeof input === "string") {
|
|
282
|
+
const trimmed = input.trim();
|
|
283
|
+
if (!trimmed) return "{}";
|
|
284
|
+
try {
|
|
285
|
+
JSON.parse(trimmed);
|
|
286
|
+
return trimmed;
|
|
287
|
+
} catch {
|
|
288
|
+
// A tool call's arguments must be a JSON object. Re-encoding an unparseable string as a
|
|
289
|
+
// JSON *string* is the double-encoding #765 reports: the caller then receives
|
|
290
|
+
// `"get weather"` where an object was required and the tool call is unusable either way.
|
|
291
|
+
// An empty object at least fails in the tool's own argument validation.
|
|
292
|
+
return "{}";
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return JSON.stringify(input ?? {});
|
|
296
|
+
}
|
|
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
|
+
|
|
253
315
|
function anthropicKeyUsesBearer(provider: OcxProviderConfig): boolean {
|
|
254
316
|
return provider.apiKeyTransport === "bearer";
|
|
255
317
|
}
|
|
@@ -680,8 +742,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
680
742
|
else if (typeof tc === "object" && "name" in tc) body.tool_choice = { type: "tool", name: toolNames.toWire(resolveToolChoiceWireName(parsed.context.tools, tc.name)) };
|
|
681
743
|
}
|
|
682
744
|
|
|
683
|
-
const
|
|
684
|
-
const url = `${base}/v1/messages`;
|
|
745
|
+
const url = anthropicMessagesUrl(provider.baseUrl);
|
|
685
746
|
const unresolvedPlaceholder = url.match(/\{[^}]*\}/)?.[0];
|
|
686
747
|
if (unresolvedPlaceholder) {
|
|
687
748
|
throw new Error(`anthropic baseUrl contains unresolved ${unresolvedPlaceholder}`);
|
|
@@ -723,15 +784,17 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
723
784
|
return { url, method: "POST", headers, body: JSON.stringify(body) };
|
|
724
785
|
},
|
|
725
786
|
|
|
726
|
-
async *parseStream(response: Response): AsyncGenerator<AdapterEvent> {
|
|
787
|
+
async *parseStream(response: Response, budget: TranslatorBudget): AsyncGenerator<AdapterEvent> {
|
|
727
788
|
if (!response.body) {
|
|
728
789
|
yield { type: "error", message: "No response body" };
|
|
729
790
|
return;
|
|
730
791
|
}
|
|
731
792
|
|
|
793
|
+
const budgetEncoder = new TextEncoder();
|
|
732
794
|
let currentBlockType = "";
|
|
733
795
|
let currentToolCallId = "";
|
|
734
796
|
let currentToolCallName = "";
|
|
797
|
+
let currentToolCallJson = "";
|
|
735
798
|
let pendingUsage: Record<string, number> | undefined;
|
|
736
799
|
let pendingStopReason: string | undefined;
|
|
737
800
|
let emittedDone = false;
|
|
@@ -746,7 +809,8 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
746
809
|
};
|
|
747
810
|
};
|
|
748
811
|
|
|
749
|
-
|
|
812
|
+
try {
|
|
813
|
+
for await (const record of decodeServerSentEvents(response.body, { includeComments: true, translatorBudget: budget })) {
|
|
750
814
|
if (record.kind === "comment") {
|
|
751
815
|
yield { type: "heartbeat" };
|
|
752
816
|
continue;
|
|
@@ -773,8 +837,10 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
773
837
|
if (!block) break;
|
|
774
838
|
currentBlockType = block.type;
|
|
775
839
|
if (block.type === "tool_use") {
|
|
776
|
-
currentToolCallId = block.id
|
|
840
|
+
currentToolCallId = usableToolUseId(block.id);
|
|
777
841
|
currentToolCallName = toolNames.fromWire(block.name ?? "");
|
|
842
|
+
currentToolCallJson = "";
|
|
843
|
+
budget.openCall(currentToolCallId);
|
|
778
844
|
yield { type: "tool_call_start", id: currentToolCallId, name: currentToolCallName };
|
|
779
845
|
}
|
|
780
846
|
if (block.type === "redacted_thinking" && typeof block.data === "string") {
|
|
@@ -799,15 +865,44 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
799
865
|
// Arrives once, just before the thinking block's content_block_stop; block-scoped
|
|
800
866
|
// so a stray signature on a non-thinking block can never be captured.
|
|
801
867
|
yield { type: "thinking_signature", signature: delta.signature };
|
|
802
|
-
} else if (delta.type === "input_json_delta" && typeof delta.partial_json === "string") {
|
|
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
|
+
}
|
|
803
884
|
yield { type: "tool_call_delta", arguments: delta.partial_json };
|
|
804
885
|
}
|
|
805
886
|
break;
|
|
806
887
|
}
|
|
807
888
|
case "content_block_stop": {
|
|
808
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
|
+
}
|
|
809
902
|
yield { type: "tool_call_end" };
|
|
903
|
+
budget.closeCall(currentToolCallId);
|
|
810
904
|
currentToolCallId = "";
|
|
905
|
+
currentToolCallJson = "";
|
|
811
906
|
}
|
|
812
907
|
currentBlockType = "";
|
|
813
908
|
break;
|
|
@@ -830,13 +925,26 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
830
925
|
}
|
|
831
926
|
}
|
|
832
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
|
+
}
|
|
833
940
|
if (!emittedDone) {
|
|
941
|
+
// Fail closed on transport EOF. Compatible providers may omit message_stop after message_delta.stop_reason.
|
|
834
942
|
if (pendingStopReason !== undefined) {
|
|
835
943
|
const stopReason = pendingStopReason === "max_tokens"
|
|
836
944
|
? "max_tokens"
|
|
837
945
|
: pendingStopReason === "refusal" || pendingStopReason === "content_filter"
|
|
838
946
|
? "content_filter"
|
|
839
|
-
:
|
|
947
|
+
: pendingStopReason;
|
|
840
948
|
emittedDone = true;
|
|
841
949
|
yield {
|
|
842
950
|
type: "done",
|
|
@@ -849,8 +957,11 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
849
957
|
}
|
|
850
958
|
},
|
|
851
959
|
|
|
852
|
-
async parseResponse(response: Response): Promise<AdapterEvent[]> {
|
|
960
|
+
async parseResponse(response: Response, budget: TranslatorBudget): Promise<AdapterEvent[]> {
|
|
853
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 {
|
|
854
965
|
const events: AdapterEvent[] = [];
|
|
855
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;
|
|
856
967
|
if (content) {
|
|
@@ -867,8 +978,9 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
867
978
|
} else if (block.type === "redacted_thinking" && typeof block.data === "string") {
|
|
868
979
|
events.push({ type: "redacted_thinking", data: block.data });
|
|
869
980
|
} else if (block.type === "tool_use") {
|
|
870
|
-
|
|
871
|
-
events.push({ type: "
|
|
981
|
+
const id = usableToolUseId(block.id);
|
|
982
|
+
events.push({ type: "tool_call_start", id, name: toolNames.fromWire(block.name ?? "") });
|
|
983
|
+
events.push({ type: "tool_call_delta", arguments: toolUseArguments(block.input) });
|
|
872
984
|
events.push({ type: "tool_call_end" });
|
|
873
985
|
}
|
|
874
986
|
}
|
|
@@ -880,7 +992,11 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
880
992
|
usage: usageFromAnthropic(usage),
|
|
881
993
|
...(stopReason ? { stopReason } : {}),
|
|
882
994
|
});
|
|
995
|
+
retainTranslatedEventBatch(events, budget);
|
|
883
996
|
return events;
|
|
997
|
+
} finally {
|
|
998
|
+
budget.releaseRetained(responseBytes, { kind: "retained_collectors" });
|
|
999
|
+
}
|
|
884
1000
|
},
|
|
885
1001
|
|
|
886
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
|
+
}
|