@bitkyc08/opencodex 2.10.0 → 2.10.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/AGENTS_INSTALL.md +77 -0
- package/README.md +4 -10
- package/bin/ocx.mjs +71 -18
- package/gui/dist/assets/index-Cd6_PBKn.css +1 -0
- package/gui/dist/assets/index-ChZQsmBY.js +70 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/alibaba-color.svg +1 -1
- package/gui/dist/provider-icons/antigravity-color.svg +1 -1
- package/gui/dist/provider-icons/claude-color.svg +1 -1
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/copilot-color.svg +1 -1
- package/gui/dist/provider-icons/cursor-color.svg +1 -1
- package/gui/dist/provider-icons/deepseek-color.svg +1 -1
- package/gui/dist/provider-icons/firepass-color.svg +1 -1
- package/gui/dist/provider-icons/fireworks-color.svg +1 -1
- package/gui/dist/provider-icons/gemini-color.svg +1 -1
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
- package/gui/dist/provider-icons/grok.svg +1 -1
- package/gui/dist/provider-icons/groq-color.svg +1 -1
- package/gui/dist/provider-icons/huggingface-color.svg +1 -1
- package/gui/dist/provider-icons/kimi-color.svg +1 -1
- package/gui/dist/provider-icons/kiro-color.svg +2 -2
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
- package/gui/dist/provider-icons/mistral-color.svg +1 -1
- package/gui/dist/provider-icons/moonshot-color.svg +1 -1
- package/gui/dist/provider-icons/nvidia-color.svg +1 -1
- package/gui/dist/provider-icons/ollama-color.svg +1 -1
- package/gui/dist/provider-icons/openai.svg +1 -1
- package/gui/dist/provider-icons/opencode.svg +2 -1
- package/gui/dist/provider-icons/openrouter-color.svg +1 -1
- package/gui/dist/provider-icons/pi.svg +2 -2
- package/gui/dist/provider-icons/qianfan-color.svg +1 -1
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/vllm-color.svg +1 -1
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
- package/package.json +8 -4
- package/src/adapters/anthropic.ts +208 -14
- package/src/adapters/base.ts +16 -5
- package/src/adapters/cursor/effort-map.ts +3 -2
- package/src/adapters/cursor/framing.ts +39 -0
- package/src/adapters/cursor/live-transport.ts +105 -95
- package/src/adapters/cursor/native-exec.ts +32 -6
- package/src/adapters/cursor/protobuf-request.ts +20 -15
- package/src/adapters/cursor/request-builder.ts +21 -7
- package/src/adapters/cursor/types.ts +7 -0
- package/src/adapters/google-antigravity-replay.ts +237 -21
- package/src/adapters/google-truncation.ts +11 -0
- package/src/adapters/google.ts +50 -9
- package/src/adapters/identity.ts +39 -6
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +10 -2
- package/src/adapters/kiro-tools.ts +10 -1
- package/src/adapters/kiro.ts +37 -11
- package/src/adapters/openai-chat.ts +284 -83
- package/src/adapters/openai-responses.ts +182 -24
- package/src/bridge.ts +177 -7
- package/src/chat/outbound.ts +78 -23
- package/src/claude/agents-inject.ts +27 -5
- package/src/claude/inbound.ts +11 -1
- package/src/claude/model-info.ts +13 -10
- package/src/claude/outbound.ts +17 -0
- package/src/cli/account-api.ts +24 -0
- package/src/cli/account-auth.ts +31 -6
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +5 -0
- package/src/cli/claude.ts +2 -1
- package/src/cli/doctor.ts +93 -22
- package/src/cli/export-command.ts +26 -12
- package/src/cli/help.ts +8 -6
- package/src/cli/index.ts +56 -22
- package/src/cli/integrations.ts +84 -1
- package/src/cli/observe.ts +54 -1
- package/src/cli/opencode.ts +2 -1
- package/src/cli/provider-runtime.ts +18 -1
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +6 -3
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +1 -1
- package/src/cli/v2.ts +36 -9
- package/src/clients/config-export.ts +687 -10
- package/src/codex/account-lifecycle.ts +30 -5
- package/src/codex/account-usability.ts +22 -2
- package/src/codex/admission.ts +255 -0
- package/src/codex/auth-api.ts +427 -140
- package/src/codex/auth-context.ts +155 -30
- package/src/codex/autostart-health.ts +8 -1
- package/src/codex/catalog/account-models.ts +62 -0
- package/src/codex/catalog/aggregation.ts +14 -1
- package/src/codex/catalog/bundled.ts +282 -32
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/metadata.ts +51 -6
- package/src/codex/catalog/parsing.ts +6 -3
- package/src/codex/catalog/provider-fetch.ts +576 -41
- package/src/codex/catalog/sync.ts +505 -66
- package/src/codex/catalog-admission.ts +197 -0
- package/src/codex/catalog-write-serialization.ts +241 -0
- package/src/codex/catalog.ts +2 -1
- package/src/codex/codex-write-lock.ts +372 -0
- package/src/codex/convergence-types.ts +593 -0
- package/src/codex/convergence.ts +441 -0
- package/src/codex/desired-state.ts +177 -0
- package/src/codex/features.ts +52 -8
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +257 -0
- package/src/codex/history-lock.ts +241 -0
- package/src/codex/history-migration-guardian.ts +18 -5
- package/src/codex/history-provider.ts +9 -2
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +176 -0
- package/src/codex/inject-coordination.ts +245 -0
- package/src/codex/inject.ts +605 -124
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +80 -0
- package/src/codex/journal.ts +10 -1
- package/src/codex/main-account-cache.ts +24 -0
- package/src/codex/management-convergence.ts +114 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +167 -0
- package/src/codex/native-main-lock-file.ts +158 -0
- package/src/codex/native-main-owner.ts +315 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1512 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +340 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +557 -0
- package/src/codex/project-config-warnings.ts +18 -4
- package/src/codex/prompt-journal.ts +311 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/quota-rejection.ts +224 -0
- package/src/codex/quota.ts +86 -3
- package/src/codex/routing.ts +299 -62
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +39 -13
- package/src/codex/subagent-model-fallback.ts +73 -12
- package/src/codex/transition-state.ts +604 -0
- package/src/codex/upstream-host-health.ts +70 -0
- package/src/codex/user-identity.ts +266 -0
- package/src/codex/write-coordination.ts +114 -0
- package/src/config.ts +562 -26
- package/src/generated/jawcode-model-metadata.ts +2 -2
- package/src/grok/inject.ts +15 -4
- package/src/grok/inspect.ts +45 -0
- package/src/images/loop.ts +113 -20
- package/src/integrations/config-io.ts +151 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/native/ownership-preflight.ts +165 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +101 -0
- package/src/integrations/serialize.ts +235 -0
- package/src/integrations/state.ts +290 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer.ts +492 -0
- package/src/lib/bounded-body.ts +46 -8
- package/src/lib/bun-runtime.ts +110 -1
- package/src/lib/bun-stream-caps.ts +2 -1
- package/src/lib/redact.ts +407 -2
- package/src/lib/shadow-call.ts +24 -0
- package/src/lib/translator-budget.ts +10 -0
- package/src/lib/upstream-reachability.ts +91 -0
- package/src/lib/upstream-retry.ts +154 -2
- package/src/lib/windows-secret-acl.ts +212 -11
- package/src/lib/winsw.ts +9 -3
- package/src/oauth/index.ts +61 -3
- package/src/oauth/key-providers.ts +4 -0
- package/src/oauth/kiro.ts +50 -6
- package/src/oauth/store.ts +31 -6
- package/src/oauth/token-guardian.ts +9 -3
- package/src/providers/codex-capacity.ts +288 -0
- package/src/providers/derive.ts +33 -1
- package/src/providers/free-directory.ts +3 -1
- package/src/providers/key-failover.ts +71 -3
- package/src/providers/openai-sidecar.ts +64 -4
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/quota.ts +334 -26
- package/src/providers/registry.ts +284 -16
- package/src/providers/xai-transport.ts +11 -4
- package/src/responses/compaction.ts +8 -1
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/parser.ts +44 -2
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/reasoning-replay-cache.ts +105 -0
- package/src/responses/spill-store.ts +45 -8
- package/src/responses/state.ts +161 -17
- package/src/router.ts +302 -16
- package/src/routing/analytics.ts +377 -0
- package/src/routing/capability.ts +204 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +444 -0
- package/src/routing/health.ts +401 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +590 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile.ts +423 -0
- package/src/routing/quota.ts +124 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +686 -0
- package/src/server/auth-cors.ts +46 -6
- package/src/server/chat-completions.ts +28 -13
- package/src/server/claude-messages.ts +23 -15
- package/src/server/gui-static.ts +39 -10
- package/src/server/images.ts +10 -1
- package/src/server/index.ts +238 -52
- package/src/server/lifecycle.ts +228 -9
- package/src/server/live.ts +14 -3
- package/src/server/management/agent-settings-routes.ts +64 -14
- package/src/server/management/combo-routes.ts +5 -5
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/context.ts +42 -2
- package/src/server/management/integration-routes.ts +538 -0
- package/src/server/management/logs-usage-routes.ts +1 -1
- package/src/server/management/model-routes.ts +32 -113
- package/src/server/management/model-rows.ts +117 -0
- package/src/server/management/native-integration-routes.ts +587 -0
- package/src/server/management/oauth-account-routes.ts +1 -1
- package/src/server/management/provider-routes.ts +218 -117
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +177 -0
- package/src/server/management/shared.ts +2 -2
- package/src/server/management/sidebar-routes.ts +47 -31
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +276 -24
- package/src/server/management/system-routes.ts +4 -0
- package/src/server/management-api.ts +84 -9
- package/src/server/management-auth.ts +43 -5
- package/src/server/relay-eager.ts +82 -42
- package/src/server/relay.ts +120 -6
- package/src/server/request-log.ts +26 -6
- package/src/server/responses/collaboration.ts +63 -8
- package/src/server/responses/compact.ts +272 -41
- package/src/server/responses/core.ts +730 -132
- package/src/server/responses/fetch-helpers.ts +15 -1
- package/src/server/responses-item-id-repair.ts +32 -3
- package/src/server/responses-json-events.ts +52 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/search.ts +51 -6
- package/src/server/sse-payload-rewrite.ts +89 -12
- package/src/server/startup-health-cache.ts +7 -1
- package/src/server/ws-bridge.ts +11 -17
- package/src/service-manager-probe.ts +297 -0
- package/src/service.ts +222 -32
- package/src/tray/windows-tray.ps1 +9 -0
- package/src/tray/windows.ts +15 -7
- package/src/types.ts +194 -14
- package/src/update/index.ts +13 -13
- package/src/update/job.ts +24 -21
- package/src/update/notify.ts +7 -3
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +129 -10
- package/src/usage/log.ts +50 -15
- package/src/usage/summary.ts +4 -4
- package/src/vision/index.ts +6 -1
- package/src/web-search/loop.ts +161 -34
- package/gui/dist/assets/index-OY43ubAq.css +0 -1
- package/gui/dist/assets/index-YwNnKZcL.js +0 -67
- package/gui/dist/provider-icons/antigravity.svg +0 -1
- package/gui/dist/provider-icons/claude.svg +0 -1
- package/gui/dist/provider-icons/copilot.svg +0 -1
- package/gui/dist/provider-icons/cursor.svg +0 -2
- package/gui/dist/provider-icons/gemini.svg +0 -1
- package/gui/dist/provider-icons/grok-color.svg +0 -1
- package/gui/dist/provider-icons/kiro.svg +0 -14
|
@@ -18,7 +18,8 @@ import { ANTHROPIC_OAUTH_BETA, CLAUDE_CODE_SYSTEM_INSTRUCTION, applyClaudeToolPr
|
|
|
18
18
|
import { parseDataUrl } from "./image";
|
|
19
19
|
import { enforceAnthropicImageLimits } from "./anthropic-image-guard";
|
|
20
20
|
import { normalizeAnthropicImages } from "./anthropic-image-normalize";
|
|
21
|
-
import {
|
|
21
|
+
import { identifyRoutedModel } from "./identity";
|
|
22
|
+
import { redactSecretString } from "../lib/redact";
|
|
22
23
|
import { CLAUDE_CODE_HEADERS, claudeCodeSessionId } from "./client-fingerprint";
|
|
23
24
|
import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge";
|
|
24
25
|
import { decodeServerSentEvents } from "../lib/sse-decoder";
|
|
@@ -243,6 +244,43 @@ function isLikelyRealAnthropicThinkingSignature(signature: string | undefined):
|
|
|
243
244
|
return /^[A-Za-z0-9+/_=-]+$/.test(signature);
|
|
244
245
|
}
|
|
245
246
|
|
|
247
|
+
/**
|
|
248
|
+
* Bridge error fidelity (web-search/images loops): extract a display-safe summary from an
|
|
249
|
+
* Anthropic JSON error envelope so `Provider error <status>` carries the upstream reason.
|
|
250
|
+
* JSON-only extraction — HTML/non-JSON bodies yield "" so raw markup is never echoed.
|
|
251
|
+
*/
|
|
252
|
+
export function formatAnthropicErrorBody(status: number, _headers: Headers, payloadText: string): string {
|
|
253
|
+
let parsed: unknown;
|
|
254
|
+
try {
|
|
255
|
+
parsed = JSON.parse(payloadText);
|
|
256
|
+
} catch {
|
|
257
|
+
return "";
|
|
258
|
+
}
|
|
259
|
+
const detail = extractAnthropicErrorDetail(parsed);
|
|
260
|
+
if (!detail) return "";
|
|
261
|
+
return redactSecretString(detail).slice(0, 400);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function extractAnthropicErrorDetail(parsed: unknown): string | undefined {
|
|
265
|
+
if (typeof parsed === "string") return parsed.trim() || undefined;
|
|
266
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
|
|
267
|
+
const obj = parsed as Record<string, unknown>;
|
|
268
|
+
// Anthropic envelope: { type: "error", error: { type, message } }; tolerate a bare
|
|
269
|
+
// { error: { message } } and a string error field.
|
|
270
|
+
const err = obj.error;
|
|
271
|
+
if (typeof err === "string" && err.trim()) return err.trim();
|
|
272
|
+
if (err !== null && typeof err === "object" && !Array.isArray(err)) {
|
|
273
|
+
const e = err as Record<string, unknown>;
|
|
274
|
+
const msg = e.message;
|
|
275
|
+
if (typeof msg !== "string" || !msg.trim()) return undefined;
|
|
276
|
+
const type = e.type;
|
|
277
|
+
return typeof type === "string" && type.trim()
|
|
278
|
+
? `${type.trim()}: ${msg.trim()}`
|
|
279
|
+
: msg.trim();
|
|
280
|
+
}
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
|
|
246
284
|
function usesNativeAnthropicEndpoint(provider: OcxProviderConfig): boolean {
|
|
247
285
|
try {
|
|
248
286
|
return new URL(provider.baseUrl).hostname === "api.anthropic.com";
|
|
@@ -277,7 +315,69 @@ function usableToolUseId(id: unknown): string {
|
|
|
277
315
|
return typeof id === "string" && id.trim() ? id : synthesizeToolUseId();
|
|
278
316
|
}
|
|
279
317
|
|
|
280
|
-
|
|
318
|
+
/**
|
|
319
|
+
* Bound repair for a malformed tool-arguments string under the compatibility profile (#658):
|
|
320
|
+
* a gateway such as AgentRouter can concatenate JSON objects (`{}{"value":42}`). Find the
|
|
321
|
+
* last parseable JSON object by scanning suffixes from each object-open brace and prefixes
|
|
322
|
+
* ending at each object-close brace. Both scans walk backwards from the end trying at most
|
|
323
|
+
* `maxCandidates` positions, so no offset index is ever materialized: a brace-dense hostile
|
|
324
|
+
* input costs at most 2 × maxCandidates bounded JSON.parse attempts and no extra storage.
|
|
325
|
+
* Inputs above MAX_REPAIRABLE_TOOL_ARGUMENT_BYTES are not repaired at all.
|
|
326
|
+
*/
|
|
327
|
+
const MAX_REPAIRABLE_TOOL_ARGUMENT_BYTES = 1024 * 1024;
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Whether `input` encodes to more than `max` UTF-8 bytes, with an early exit so the check
|
|
331
|
+
* itself never allocates a copy of a hostile string. `string.length` counts UTF-16 code
|
|
332
|
+
* units, which undercounts astral text by 2x against a byte budget.
|
|
333
|
+
*/
|
|
334
|
+
function utf8BytesExceed(input: string, max: number): boolean {
|
|
335
|
+
let bytes = 0;
|
|
336
|
+
for (let i = 0; i < input.length; i++) {
|
|
337
|
+
const code = input.charCodeAt(i);
|
|
338
|
+
if (code < 0x80) bytes += 1;
|
|
339
|
+
else if (code < 0x800) bytes += 2;
|
|
340
|
+
else if (code >= 0xd800 && code <= 0xdbff && i + 1 < input.length
|
|
341
|
+
&& input.charCodeAt(i + 1) >= 0xdc00 && input.charCodeAt(i + 1) <= 0xdfff) {
|
|
342
|
+
// A complete surrogate pair is one 4-byte scalar. Anything else — a high surrogate
|
|
343
|
+
// followed by another high surrogate or a non-surrogate — encodes as two separate
|
|
344
|
+
// U+FFFD replacements, so the next unit must NOT be skipped.
|
|
345
|
+
bytes += 4;
|
|
346
|
+
i++;
|
|
347
|
+
} else bytes += 3; // lone surrogates encode as U+FFFD (3 bytes)
|
|
348
|
+
if (bytes > max) return true;
|
|
349
|
+
}
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function lastValidJsonObject(input: string, maxCandidates: number): string | undefined {
|
|
354
|
+
const tryParseObject = (candidate: string): string | undefined => {
|
|
355
|
+
try {
|
|
356
|
+
const parsed = JSON.parse(candidate) as unknown;
|
|
357
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return candidate;
|
|
358
|
+
} catch { /* keep scanning */ }
|
|
359
|
+
return undefined;
|
|
360
|
+
};
|
|
361
|
+
let scanFrom = input.length - 1;
|
|
362
|
+
for (let tried = 0; tried < maxCandidates && scanFrom >= 0; tried++) {
|
|
363
|
+
const open = input.lastIndexOf("{", scanFrom);
|
|
364
|
+
if (open === -1) break;
|
|
365
|
+
const repaired = tryParseObject(input.slice(open));
|
|
366
|
+
if (repaired !== undefined) return repaired;
|
|
367
|
+
scanFrom = open - 1;
|
|
368
|
+
}
|
|
369
|
+
scanFrom = input.length - 1;
|
|
370
|
+
for (let tried = 0; tried < maxCandidates && scanFrom >= 0; tried++) {
|
|
371
|
+
const close = input.lastIndexOf("}", scanFrom);
|
|
372
|
+
if (close === -1) break;
|
|
373
|
+
const repaired = tryParseObject(input.slice(0, close + 1));
|
|
374
|
+
if (repaired !== undefined) return repaired;
|
|
375
|
+
scanFrom = close - 1;
|
|
376
|
+
}
|
|
377
|
+
return undefined;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function toolUseArguments(input: unknown, lenient = false): string {
|
|
281
381
|
if (typeof input === "string") {
|
|
282
382
|
const trimmed = input.trim();
|
|
283
383
|
if (!trimmed) return "{}";
|
|
@@ -285,6 +385,10 @@ function toolUseArguments(input: unknown): string {
|
|
|
285
385
|
JSON.parse(trimmed);
|
|
286
386
|
return trimmed;
|
|
287
387
|
} catch {
|
|
388
|
+
if (lenient && !utf8BytesExceed(trimmed, MAX_REPAIRABLE_TOOL_ARGUMENT_BYTES)) {
|
|
389
|
+
const repaired = lastValidJsonObject(trimmed, 32);
|
|
390
|
+
if (repaired !== undefined) return repaired;
|
|
391
|
+
}
|
|
288
392
|
// A tool call's arguments must be a JSON object. Re-encoding an unparseable string as a
|
|
289
393
|
// JSON *string* is the double-encoding #765 reports: the caller then receives
|
|
290
394
|
// `"get weather"` where an object was required and the tool call is unusable either way.
|
|
@@ -342,16 +446,68 @@ const ADAPTIVE_THINKING_FAMILY_MINIMUMS: Record<string, readonly [major: number,
|
|
|
342
446
|
fable: [0, 0],
|
|
343
447
|
};
|
|
344
448
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
449
|
+
/**
|
|
450
|
+
* Family/version parse for a Claude model id, tolerant of a routing prefix.
|
|
451
|
+
*
|
|
452
|
+
* `parsed.modelId` is not always bare, and the slash can fall on either side.
|
|
453
|
+
* A `modelMap` entry may point at a routed destination such as
|
|
454
|
+
* `anthropic/claude-sonnet-5` (prefix), while a custom provider may expose a
|
|
455
|
+
* native id such as `claude-sonnet-5/variant` (suffix); both survive routing's
|
|
456
|
+
* known-id decoding. So this matches the segment that actually begins with
|
|
457
|
+
* `claude-` rather than assuming it is the first or the last one. A capability
|
|
458
|
+
* predicate that quietly returns false is worse than one that throws — the
|
|
459
|
+
* request just goes out wrong.
|
|
460
|
+
*
|
|
461
|
+
* Minor is 1-2 digits with a non-digit lookahead so date-pinned ids
|
|
462
|
+
* ("claude-opus-4-20250514") parse as minor 0 instead of minor 20250514;
|
|
463
|
+
* suffixed ids ("claude-opus-4-8[1m]") still match.
|
|
464
|
+
*/
|
|
465
|
+
function claudeFamilyVersion(modelId: string): { family: string; major: number; minor: number } | undefined {
|
|
466
|
+
// Find the segment that actually starts with `claude-`, rather than assuming it is either
|
|
467
|
+
// the first (breaks `anthropic/claude-sonnet-5`) or the last (breaks `claude-sonnet-5/variant`,
|
|
468
|
+
// where the slash carries a vendor suffix rather than a routing prefix).
|
|
469
|
+
const match = /(?:^|\/)claude-([a-z]+)-(\d+)(?:-(\d{1,2}))?(?!\d)/.exec(modelId);
|
|
470
|
+
if (!match) return undefined;
|
|
471
|
+
return {
|
|
472
|
+
family: match[1]!,
|
|
473
|
+
major: Number(match[2]),
|
|
474
|
+
minor: match[3] === undefined ? 0 : Number(match[3]),
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function meetsFamilyMinimum(
|
|
479
|
+
modelId: string,
|
|
480
|
+
minimums: Record<string, readonly [major: number, minor: number]>,
|
|
481
|
+
): boolean {
|
|
482
|
+
const parsed = claudeFamilyVersion(modelId);
|
|
483
|
+
if (!parsed) return false;
|
|
484
|
+
const minimum = minimums[parsed.family];
|
|
351
485
|
if (!minimum) return false;
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
486
|
+
return parsed.major > minimum[0] || (parsed.major === minimum[0] && parsed.minor >= minimum[1]);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function usesAdaptiveThinking(modelId: string): boolean {
|
|
490
|
+
return meetsFamilyMinimum(modelId, ADAPTIVE_THINKING_FAMILY_MINIMUMS);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Claude families that (a) think by DEFAULT when the request omits `thinking`,
|
|
495
|
+
* and (b) accept an explicit `thinking: {type: "disabled"}` to turn it off.
|
|
496
|
+
*
|
|
497
|
+
* Deliberately NOT `usesAdaptiveThinking()`, which answers a different question
|
|
498
|
+
* (which wire shape a family accepts). The two sets differ in both directions:
|
|
499
|
+
* Fable always thinks and REJECTS an explicit disable, while Opus 4.7/4.8 use
|
|
500
|
+
* the adaptive wire but leave thinking off when the field is omitted, so they
|
|
501
|
+
* need no disable at all. Seeded with the family where the defect reproduces
|
|
502
|
+
* (#545); widen only with vendor evidence, since a wrong entry here turns a
|
|
503
|
+
* silent truncation into a 400.
|
|
504
|
+
*/
|
|
505
|
+
const EXPLICIT_THINKING_DISABLE_FAMILY_MINIMUMS: Record<string, readonly [major: number, minor: number]> = {
|
|
506
|
+
sonnet: [5, 0],
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
function supportsExplicitThinkingDisable(modelId: string): boolean {
|
|
510
|
+
return meetsFamilyMinimum(modelId, EXPLICIT_THINKING_DISABLE_FAMILY_MINIMUMS);
|
|
355
511
|
}
|
|
356
512
|
|
|
357
513
|
/** `output_config.effort` accepts low|medium|high|xhigh|max — "minimal" is rejected with a 400. */
|
|
@@ -441,7 +597,7 @@ function messagesToAnthropicFormat(
|
|
|
441
597
|
);
|
|
442
598
|
const systemParts = [...(parsed.context.systemPrompt ?? []), ...(toolCatalogNudge ? [toolCatalogNudge] : [])];
|
|
443
599
|
const system = systemParts.length
|
|
444
|
-
?
|
|
600
|
+
? identifyRoutedModel(systemParts.join("\n\n"), parsed.modelId) || undefined
|
|
445
601
|
: undefined;
|
|
446
602
|
const messages: unknown[] = [];
|
|
447
603
|
|
|
@@ -659,6 +815,8 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
659
815
|
return {
|
|
660
816
|
name: "anthropic",
|
|
661
817
|
|
|
818
|
+
formatErrorBody: formatAnthropicErrorBody,
|
|
819
|
+
|
|
662
820
|
async buildRequest(parsed: OcxParsedRequest, incoming?: IncomingMeta) {
|
|
663
821
|
if (typeof provider.apiKey !== "string" || provider.apiKey.trim() === "") {
|
|
664
822
|
if (isOAuth) {
|
|
@@ -700,7 +858,14 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
700
858
|
// `reasoning` is a Codex effort string; "none" is the disable sentinel (see parser.ts
|
|
701
859
|
// REASONING_EFFORTS). A bare truthy check would treat "none" as truthy and wrongly enable
|
|
702
860
|
// extended thinking (and strip temperature/top_p), so gate on a real, non-disable effort.
|
|
703
|
-
|
|
861
|
+
//
|
|
862
|
+
// "none" is not the same as absent. Omitting `thinking` lets a default-on model think
|
|
863
|
+
// anyway, and thinking shares the caller's `max_tokens` — which truncates a small-budget
|
|
864
|
+
// request before it can emit its stop sequence (#545). Say "disabled" out loud where the
|
|
865
|
+
// model both defaults to thinking and accepts being told not to.
|
|
866
|
+
if (parsed.options.reasoning === "none" && supportsExplicitThinkingDisable(parsed.modelId)) {
|
|
867
|
+
body.thinking = { type: "disabled" };
|
|
868
|
+
} else if (typeof parsed.options.reasoning === "string" && parsed.options.reasoning !== "none") {
|
|
704
869
|
if (usesAdaptiveThinking(parsed.modelId)) {
|
|
705
870
|
// Adaptive-thinking models replace the token budget with an effort knob and reject
|
|
706
871
|
// `thinking.type: "enabled"` outright. `max_tokens` still caps thinking plus visible
|
|
@@ -798,6 +963,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
798
963
|
let pendingUsage: Record<string, number> | undefined;
|
|
799
964
|
let pendingStopReason: string | undefined;
|
|
800
965
|
let emittedDone = false;
|
|
966
|
+
let sawVisibleText = false;
|
|
801
967
|
|
|
802
968
|
const emitDone = function* (): Generator<AdapterEvent> {
|
|
803
969
|
if (emittedDone) return;
|
|
@@ -853,6 +1019,10 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
853
1019
|
const delta = data.delta as Record<string, unknown> | undefined;
|
|
854
1020
|
if (!delta) break;
|
|
855
1021
|
if (delta.type === "text_delta" && typeof delta.text === "string") {
|
|
1022
|
+
// Only non-empty text proves the upstream produced usable output; an empty
|
|
1023
|
+
// delta followed by EOF must stay a truncation error even on the tolerant
|
|
1024
|
+
// profile, or a cut-off turn would surface as a successful empty answer.
|
|
1025
|
+
if (delta.text.length > 0) sawVisibleText = true;
|
|
856
1026
|
yield { type: "text_delta", text: delta.text };
|
|
857
1027
|
} else if (delta.type === "thinking_delta" && typeof delta.thinking === "string") {
|
|
858
1028
|
yield { type: "thinking_delta", thinking: delta.thinking };
|
|
@@ -934,6 +1104,10 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
934
1104
|
code: "translation_buffer_limit",
|
|
935
1105
|
message: "upstream translation buffer exceeded the safe limit",
|
|
936
1106
|
};
|
|
1107
|
+
// The budget error IS the terminal event for this stream. Falling through to the
|
|
1108
|
+
// EOF handling below could append tool_call_end/done after it, violating the
|
|
1109
|
+
// one-terminal-event contract for consumers that keep draining the generator.
|
|
1110
|
+
return;
|
|
937
1111
|
} finally {
|
|
938
1112
|
if (currentToolCallId) budget.closeCall(currentToolCallId);
|
|
939
1113
|
}
|
|
@@ -951,6 +1125,26 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
951
1125
|
usage: usageFromAnthropic(pendingUsage),
|
|
952
1126
|
...(stopReason ? { stopReason } : {}),
|
|
953
1127
|
};
|
|
1128
|
+
} else if (provider.anthropicEofTolerance === true) {
|
|
1129
|
+
// AgentRouter-style compatibility profile (#658): the upstream can close the stream
|
|
1130
|
+
// after valid content without terminal frames. Complete only when visible text was
|
|
1131
|
+
// received or an open tool call has complete JSON-object arguments; everything else
|
|
1132
|
+
// (incomplete tool JSON, no usable content, transport failure) stays a truncation
|
|
1133
|
+
// error, matching the strict default.
|
|
1134
|
+
if (currentToolCallId) {
|
|
1135
|
+
if (streamedToolArgumentsParse(currentToolCallJson)) {
|
|
1136
|
+
budget.closeCall(currentToolCallId);
|
|
1137
|
+
currentToolCallId = "";
|
|
1138
|
+
yield { type: "tool_call_end" };
|
|
1139
|
+
yield* emitDone();
|
|
1140
|
+
} else {
|
|
1141
|
+
yield { type: "error", message: "upstream stream ended before message_stop — possible truncation" };
|
|
1142
|
+
}
|
|
1143
|
+
} else if (sawVisibleText) {
|
|
1144
|
+
yield* emitDone();
|
|
1145
|
+
} else {
|
|
1146
|
+
yield { type: "error", message: "upstream stream ended before message_stop — possible truncation" };
|
|
1147
|
+
}
|
|
954
1148
|
} else {
|
|
955
1149
|
yield { type: "error", message: "upstream stream ended before message_stop — possible truncation" };
|
|
956
1150
|
}
|
|
@@ -980,7 +1174,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
980
1174
|
} else if (block.type === "tool_use") {
|
|
981
1175
|
const id = usableToolUseId(block.id);
|
|
982
1176
|
events.push({ type: "tool_call_start", id, name: toolNames.fromWire(block.name ?? "") });
|
|
983
|
-
events.push({ type: "tool_call_delta", arguments: toolUseArguments(block.input) });
|
|
1177
|
+
events.push({ type: "tool_call_delta", arguments: toolUseArguments(block.input, provider.anthropicEofTolerance === true) });
|
|
984
1178
|
events.push({ type: "tool_call_end" });
|
|
985
1179
|
}
|
|
986
1180
|
}
|
package/src/adapters/base.ts
CHANGED
|
@@ -49,11 +49,22 @@ export interface AdapterRequest {
|
|
|
49
49
|
/** Releases observation of a serialized request body after its final fetch attempt settles. */
|
|
50
50
|
releaseBodyObservation?: () => void;
|
|
51
51
|
/** Exact reasoning parameter emitted by the adapter, for request-log diagnostics only. */
|
|
52
|
-
reasoningLog?:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
reasoningLog?:
|
|
53
|
+
| {
|
|
54
|
+
effectiveEffort: string;
|
|
55
|
+
wireField: "reasoning.enabled";
|
|
56
|
+
wireValue: boolean;
|
|
57
|
+
}
|
|
58
|
+
| {
|
|
59
|
+
effectiveEffort: string;
|
|
60
|
+
wireField: "thinking_budget";
|
|
61
|
+
wireValue: number;
|
|
62
|
+
}
|
|
63
|
+
| {
|
|
64
|
+
effectiveEffort: string;
|
|
65
|
+
wireField: "reasoning_effort" | "reasoning.effort" | "thinking.type";
|
|
66
|
+
wireValue: string;
|
|
67
|
+
};
|
|
57
68
|
usageLog?: {
|
|
58
69
|
inputTokens?: number;
|
|
59
70
|
estimated?: boolean;
|
|
@@ -116,8 +116,9 @@ export function cursorModelHasEffortTiers(baseModelId: string): boolean {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
/**
|
|
119
|
-
* Compose
|
|
120
|
-
*
|
|
119
|
+
* Compose Cursor's flattened model id from a Codex-facing base id and effort tier. Discovery uses
|
|
120
|
+
* this for the ids returned by GetUsableModels. Parameterized Grok Fast requests bypass the flat id
|
|
121
|
+
* and send the base model plus requested_model parameters instead.
|
|
121
122
|
*/
|
|
122
123
|
export function cursorWireModelIdWithEffort(baseModelId: string, effortSuffix: string): string {
|
|
123
124
|
if (baseModelId.endsWith("-fast")) {
|
|
@@ -168,6 +168,45 @@ export function decodeAvailableConnectFrames(
|
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Cursor-based sibling of decodeAvailableConnectFrames for callers that keep
|
|
173
|
+
* their own raw backlog: consumes complete frames from the FRONT of `input`
|
|
174
|
+
* and reports how many bytes were consumed (headers included) instead of
|
|
175
|
+
* materializing a remainder copy, and hands payload VIEWS (not copies) back so
|
|
176
|
+
* the caller can transfer the already-charged bytes to the frame lifecycle
|
|
177
|
+
* instead of reserving a second copy. The caller advances its own cursor by
|
|
178
|
+
* `consumedBytes` and never pays an O(backlog) copy per drain.
|
|
179
|
+
*/
|
|
180
|
+
export function consumeConnectFrames(
|
|
181
|
+
input: Uint8Array,
|
|
182
|
+
maxPayloadBytes = MAX_CONNECT_FRAME_PAYLOAD_BYTES,
|
|
183
|
+
availableFrameSlots = Number.POSITIVE_INFINITY,
|
|
184
|
+
): { frames: ConnectFrame[]; consumedBytes: number } {
|
|
185
|
+
const planned: InspectedConnectFrame[] = [];
|
|
186
|
+
let offset = 0;
|
|
187
|
+
while (offset < input.length && planned.length < availableFrameSlots) {
|
|
188
|
+
const inspected = inspectConnectFrame(input, offset, maxPayloadBytes);
|
|
189
|
+
if (!inspected) break;
|
|
190
|
+
planned.push(inspected);
|
|
191
|
+
offset += inspected.readBytes;
|
|
192
|
+
}
|
|
193
|
+
// Zero-copy handoff for large payloads: VIEWS into the caller's backlog
|
|
194
|
+
// buffer (safe: append-only at its end, compaction/growth replace the
|
|
195
|
+
// buffer, a consumed region is never mutated in place). Small payloads are
|
|
196
|
+
// COPIED instead — a small view would pin the whole backlog buffer (up to
|
|
197
|
+
// 32 MiB) alive while the frame waits in the work queue.
|
|
198
|
+
const COPY_PIN_THRESHOLD_BYTES = 64 * 1024;
|
|
199
|
+
const frames = planned.map(({ flags, length, payloadStart }) => ({
|
|
200
|
+
flags,
|
|
201
|
+
payload: length <= COPY_PIN_THRESHOLD_BYTES
|
|
202
|
+
? input.slice(payloadStart, payloadStart + length)
|
|
203
|
+
: input.subarray(payloadStart, payloadStart + length),
|
|
204
|
+
compressed: isConnectFrameCompressed(flags),
|
|
205
|
+
endStream: isConnectFrameEndStream(flags),
|
|
206
|
+
}));
|
|
207
|
+
return { frames, consumedBytes: offset };
|
|
208
|
+
}
|
|
209
|
+
|
|
171
210
|
function inspectConnectFrame(
|
|
172
211
|
input: Uint8Array,
|
|
173
212
|
offset: number,
|