@iislee/opencodex 2.31.0 → 2.35.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 +3 -3
- package/bin/ocx.mjs +99 -70
- package/gui/dist/assets/index-Be1sDpWw.css +1 -0
- package/gui/dist/assets/index-D4OKgUV4.js +114 -0
- package/gui/dist/index.html +2 -2
- package/package.json +3 -1
- package/src/adapters/anthropic.ts +104 -15
- package/src/adapters/base.ts +29 -1
- package/src/adapters/command-code.ts +42 -6
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/catalog.ts +541 -0
- package/src/adapters/cursor/checkpoint-store.ts +15 -10
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +69 -18
- package/src/adapters/cursor/effort-map.ts +79 -1
- package/src/adapters/cursor/envelope-echo.ts +290 -0
- package/src/adapters/cursor/live-models.ts +7 -2
- package/src/adapters/cursor/live-transport.ts +14 -0
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-desktop.ts +23 -0
- package/src/adapters/cursor/native-exec-fs.ts +2 -2
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +5 -2
- package/src/adapters/cursor/native-exec.ts +20 -0
- package/src/adapters/cursor/protobuf-request.ts +68 -14
- package/src/adapters/cursor/request-builder.ts +40 -10
- package/src/adapters/cursor/tool-definitions.ts +14 -4
- package/src/adapters/cursor/tool-result-normalize.ts +28 -5
- package/src/adapters/cursor/types.ts +20 -0
- package/src/adapters/cursor.ts +209 -38
- package/src/adapters/exec-tool-result-normalize.ts +99 -0
- package/src/adapters/google-antigravity-replay.ts +173 -25
- package/src/adapters/google-antigravity-wire.ts +5 -0
- package/src/adapters/google-errors.ts +44 -12
- package/src/adapters/google.ts +136 -24
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +27 -0
- package/src/adapters/kiro-tools.ts +55 -16
- package/src/adapters/kiro.ts +182 -23
- package/src/adapters/openai-chat.ts +300 -242
- package/src/adapters/openai-responses.ts +353 -27
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-catalog-nudge.ts +4 -3
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/adapters/xai-web-search.ts +17 -16
- package/src/bridge.ts +90 -37
- package/src/chat/inbound.ts +14 -1
- package/src/chat/outbound.ts +15 -7
- package/src/claude/agents-inject.ts +8 -1
- package/src/claude/context-windows.ts +16 -9
- package/src/claude/outbound.ts +24 -11
- package/src/cli/access.ts +46 -3
- package/src/cli/account-api.ts +116 -19
- package/src/cli/account-extended.ts +262 -29
- package/src/cli/account-main.ts +12 -12
- package/src/cli/account.ts +69 -15
- package/src/cli/agent.ts +8 -1
- package/src/cli/alias.ts +66 -0
- package/src/cli/capabilities-command.ts +94 -0
- package/src/cli/capabilities.ts +496 -0
- package/src/cli/claude-desktop.ts +31 -11
- package/src/cli/claude.ts +26 -1
- package/src/cli/dispatch.ts +256 -28
- package/src/cli/doctor.ts +126 -14
- package/src/cli/help.ts +17 -6
- package/src/cli/index.ts +39 -9
- package/src/cli/init.ts +1 -0
- package/src/cli/inspect.ts +230 -0
- package/src/cli/models-runtime.ts +95 -0
- package/src/cli/models.ts +26 -10
- package/src/cli/observe.ts +31 -8
- package/src/cli/provider-runtime.ts +16 -2
- package/src/cli/provider.ts +2 -1
- package/src/cli/registry.ts +49 -10
- package/src/cli/runtime-api.ts +51 -7
- package/src/cli/status.ts +41 -1
- package/src/cli/storage.ts +234 -0
- package/src/cli/system-command.ts +16 -0
- package/src/cli/system-restart-client.ts +1 -1
- package/src/cli/usage-report.ts +184 -0
- package/src/cli/v2.ts +34 -10
- package/src/cli/version-skew.ts +46 -0
- package/src/clients/config-export.ts +85 -3
- package/src/clients/effective-status.ts +30 -2
- package/src/clients/probes/cline.ts +81 -0
- package/src/codex/account-label.ts +21 -0
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +3 -2
- package/src/codex/app-server-processes.ts +83 -7
- package/src/codex/auth-api.ts +50 -10
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/autostart-health.ts +16 -0
- package/src/codex/catalog/aggregation.ts +18 -0
- package/src/codex/catalog/effort.ts +18 -3
- package/src/codex/catalog/metadata.ts +27 -1
- package/src/codex/catalog/native-models.ts +5 -2
- package/src/codex/catalog/parsing.ts +72 -27
- package/src/codex/catalog/provider-fetch.ts +187 -31
- package/src/codex/catalog/sync.ts +131 -5
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-write-lock.ts +3 -1
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/convergence.ts +27 -2
- package/src/codex/desired-state.ts +2 -2
- package/src/codex/desktop-app-restart.ts +355 -0
- package/src/codex/history-job.ts +32 -3
- package/src/codex/history-manifest.ts +112 -0
- package/src/codex/history-migration-guardian.ts +5 -5
- package/src/codex/history-provider.ts +825 -247
- package/src/codex/history-worker.ts +8 -5
- package/src/codex/inject-coordination.ts +83 -0
- package/src/codex/inject.ts +63 -22
- package/src/codex/injected-marker.ts +1 -1
- package/src/codex/internal/history-writer.ts +4 -3
- package/src/codex/log-guard/inspect.ts +22 -4
- package/src/codex/model-entitlements.ts +9 -2
- package/src/codex/native-profile-startup.ts +157 -27
- package/src/codex/native-residue.ts +26 -33
- package/src/codex/prompt-layers.ts +371 -25
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota.ts +91 -10
- package/src/codex/routing.ts +9 -0
- package/src/codex/shim.ts +56 -3
- package/src/codex/subagent-model-fallback.ts +104 -25
- package/src/codex/transition-state.ts +116 -8
- package/src/combos/failover.ts +27 -0
- package/src/combos/types.ts +10 -0
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +121 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +309 -0
- package/src/config/provider-validation.ts +177 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +206 -824
- package/src/generated/compatibility-version.json +373 -213
- package/src/images/loop.ts +5 -4
- package/src/images/plan.ts +5 -4
- package/src/integrations/ownership-policy.ts +160 -0
- package/src/integrations/ownership.ts +46 -2
- package/src/integrations/registry.ts +7 -0
- package/src/integrations/state.ts +79 -7
- package/src/integrations/writer.ts +26 -2
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
- package/src/lab/fabric/producer-child.ts +1 -1
- package/src/lib/admin-secrets.ts +24 -0
- package/src/lib/bounded-body.ts +14 -2
- package/src/lib/config-ownership.ts +20 -0
- package/src/lib/errors.ts +36 -3
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/lib/process-control.ts +2 -1
- package/src/lib/service-secrets.ts +15 -0
- package/src/lib/tool-argument-integers.ts +56 -5
- package/src/oauth/callback-server.ts +22 -2
- package/src/oauth/cline.ts +203 -0
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/health.ts +1 -1
- package/src/oauth/index.ts +65 -6
- package/src/oauth/key-providers.ts +10 -1
- package/src/oauth/kimi.ts +9 -1
- package/src/oauth/kiro.ts +45 -0
- package/src/oauth/local-token-detect.ts +2 -0
- package/src/oauth/open-browser-choice.ts +26 -0
- package/src/oauth/store.ts +14 -5
- package/src/oauth/types.ts +15 -0
- package/src/providers/auto-compact-budget.ts +65 -0
- package/src/providers/command-code-efforts.ts +38 -6
- package/src/providers/context-cap.ts +4 -3
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +28 -0
- package/src/providers/fastwire.ts +7 -1
- package/src/providers/label.ts +34 -1
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/new-model-policy.ts +146 -0
- package/src/providers/provider-id-rewrite.ts +2 -1
- package/src/providers/quota.ts +65 -7
- package/src/providers/registry.ts +184 -69
- package/src/providers/slug-codec.ts +52 -0
- package/src/providers/xai-transport.ts +21 -0
- package/src/reasoning-effort.ts +19 -2
- package/src/responses/apply-patch-envelope.ts +63 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/custom-tool-compat.ts +157 -39
- package/src/responses/namespace-tool-compat.ts +84 -4
- package/src/responses/parser.ts +7 -2
- package/src/responses/reasoning-replay-cache.ts +81 -3
- package/src/responses/schema.ts +5 -1
- package/src/responses/state.ts +94 -4
- package/src/responses/thought-signature-replay.ts +17 -0
- package/src/responses/turn-termination.ts +107 -0
- package/src/router.ts +40 -2
- package/src/routing/analytics.ts +1 -0
- package/src/routing/quota.ts +10 -0
- package/src/server/auth-cors.ts +27 -7
- package/src/server/chat-completions.ts +24 -16
- package/src/server/chat-native-sse.ts +3 -3
- package/src/server/chat-native.ts +30 -11
- package/src/server/claude-messages.ts +1 -1
- package/src/server/index.ts +179 -32
- package/src/server/lifecycle.ts +52 -1
- package/src/server/local-management-read-client.ts +1 -1
- package/src/server/local-provider-reload-client.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +45 -23
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +2 -1
- package/src/server/management/config-routes.ts +59 -12
- package/src/server/management/context.ts +10 -1
- package/src/server/management/logs-usage-routes.ts +30 -24
- package/src/server/management/model-routes.ts +274 -4
- package/src/server/management/model-rows.ts +4 -0
- package/src/server/management/native-integration-routes.ts +2 -1
- package/src/server/management/oauth-account-routes.ts +10 -4
- package/src/server/management/provider-capability-config.ts +1 -1
- package/src/server/management/provider-routes.ts +81 -11
- package/src/server/management/route-registry.ts +332 -0
- package/src/server/management/routing-profile-routes.ts +2 -2
- package/src/server/management/system-restart.ts +1 -1
- package/src/server/management-api.ts +18 -16
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/proxy-liveness.ts +29 -5
- package/src/server/relay-eager.ts +147 -20
- package/src/server/relay.ts +251 -19
- package/src/server/request-log-conversation.ts +51 -0
- package/src/server/request-log.ts +70 -16
- package/src/server/responses/codex-auth-error.ts +55 -0
- package/src/server/responses/collaboration.ts +37 -3
- package/src/server/responses/combo-stream-preflight.ts +178 -0
- package/src/server/responses/compact.ts +6 -21
- package/src/server/responses/core.ts +810 -208
- package/src/server/responses/empty-completion-guard.ts +63 -6
- package/src/server/responses/fetch-helpers.ts +17 -102
- package/src/server/responses/input-admission.ts +3 -1
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/responses-field-backfill.ts +105 -13
- package/src/server/responses/ws-upstream.ts +75 -2
- package/src/server/responses-custom-tool-repair.ts +87 -6
- package/src/server/responses-terminal-repair.ts +25 -4
- package/src/server/responses-undeclared-tool-guard.ts +241 -18
- package/src/server/sse-frame-buffer.ts +31 -4
- package/src/server/ws-bridge.ts +14 -2
- package/src/service.ts +42 -4
- package/src/storage/policy-job.ts +14 -4
- package/src/storage/policy.ts +88 -23
- package/src/types/config.ts +66 -0
- package/src/types/provider.ts +69 -0
- package/src/types/request.ts +17 -4
- package/src/types/tools.ts +116 -11
- package/src/types.ts +3 -1
- package/src/update/index.ts +5 -4
- package/src/update/job.ts +3 -1
- package/src/update/transactional-install.mjs +8 -1
- package/src/usage/expected-prices.ts +43 -24
- package/src/usage/log.ts +63 -12
- package/src/usage/summary.ts +211 -8
- package/src/vision/describe.ts +18 -13
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/executor.ts +10 -3
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-BF38heuV.js +0 -104
- package/gui/dist/assets/index-DMiI18Kv.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-D4OKgUV4.js"></script>
|
|
20
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Be1sDpWw.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": "@iislee/opencodex",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.35.0",
|
|
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",
|
|
@@ -42,6 +42,8 @@
|
|
|
42
42
|
"typecheck": "bun x tsc --noEmit",
|
|
43
43
|
"audit:high": "bun audit --audit-level=high && cd gui && bun audit --audit-level=high",
|
|
44
44
|
"privacy:scan": "bun scripts/privacy-scan.ts",
|
|
45
|
+
"skill:surface": "bun scripts/generate-ocx-skill-surface.ts",
|
|
46
|
+
"skill:surface:check": "bun scripts/generate-ocx-skill-surface.ts --check",
|
|
45
47
|
"generate:model-metadata": "bun scripts/generate-model-metadata.ts",
|
|
46
48
|
"build:gui": "cd gui && bun install --frozen-lockfile && bun run build && cd .. && bun run prepare:package",
|
|
47
49
|
"prepare:package": "bun scripts/prepare-package.ts",
|
|
@@ -14,7 +14,7 @@ import type {
|
|
|
14
14
|
OcxToolResultMessage,
|
|
15
15
|
OcxUsage,
|
|
16
16
|
} from "../types";
|
|
17
|
-
import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName,
|
|
17
|
+
import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName, toolChoiceToolPredicate } from "../types";
|
|
18
18
|
import { ANTHROPIC_OAUTH_BETA, CLAUDE_CODE_SYSTEM_INSTRUCTION, applyClaudeToolPrefix, stripClaudeToolPrefix } from "../oauth/anthropic";
|
|
19
19
|
import { parseDataUrl } from "./image";
|
|
20
20
|
import { enforceAnthropicImageLimits } from "./anthropic-image-guard";
|
|
@@ -27,6 +27,7 @@ import { CLAUDE_CODE_HEADERS, claudeCodeSessionId } from "./client-fingerprint";
|
|
|
27
27
|
import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge";
|
|
28
28
|
import { decodeServerSentEvents } from "../lib/sse-decoder";
|
|
29
29
|
import { isTranslatorBudgetExceededError, retainTranslatedEventBatch, type TranslatorBudget } from "../lib/translator-budget";
|
|
30
|
+
import { isReasoningEffortOmitted, modelRecordValue } from "../reasoning-effort";
|
|
30
31
|
|
|
31
32
|
/** Map a user content part to an Anthropic content block (text or image source). */
|
|
32
33
|
function toAnthropicContentPart(p: OcxContentPart): unknown {
|
|
@@ -36,6 +37,7 @@ function toAnthropicContentPart(p: OcxContentPart): unknown {
|
|
|
36
37
|
? { type: "image", source: { type: "base64", media_type: data.mediaType, data: data.base64 } }
|
|
37
38
|
: { type: "image", source: { type: "url", url: p.imageUrl } };
|
|
38
39
|
}
|
|
40
|
+
if (p.type === "video") return { type: "text", text: "[video]" };
|
|
39
41
|
return { type: "text", text: p.text };
|
|
40
42
|
}
|
|
41
43
|
|
|
@@ -264,6 +266,39 @@ export function formatAnthropicErrorBody(status: number, _headers: Headers, payl
|
|
|
264
266
|
return redactSecretString(detail).slice(0, 400);
|
|
265
267
|
}
|
|
266
268
|
|
|
269
|
+
function isAnthropicRecord(value: unknown): value is Record<string, unknown> {
|
|
270
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function anthropicStructuralValueType(value: unknown): string {
|
|
274
|
+
if (value === null) return "null";
|
|
275
|
+
return Array.isArray(value) ? "array" : typeof value;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
interface InvalidAnthropicShapeDiagnostic {
|
|
279
|
+
reason: "content_not_array" | "content_block_not_object";
|
|
280
|
+
blockIndex?: number;
|
|
281
|
+
valueType: string;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Structured refusal for a malformed buffered response body, shaped like the google adapter's
|
|
286
|
+
* `invalidGoogleShapeEvent` so an operator reading a log can tell which rung failed: the content
|
|
287
|
+
* container, or one block inside an otherwise well-formed container.
|
|
288
|
+
*/
|
|
289
|
+
function invalidAnthropicShapeEvent(
|
|
290
|
+
diagnostic: InvalidAnthropicShapeDiagnostic,
|
|
291
|
+
): Extract<AdapterEvent, { type: "error" }> {
|
|
292
|
+
const at = diagnostic.blockIndex !== undefined ? `; blockIndex=${diagnostic.blockIndex}` : "";
|
|
293
|
+
const subject = diagnostic.reason === "content_not_array" ? "content" : "content block";
|
|
294
|
+
return {
|
|
295
|
+
type: "error",
|
|
296
|
+
message: `anthropic response contained invalid ${subject} (${diagnostic.reason}${at}; valueType=${diagnostic.valueType})`,
|
|
297
|
+
status: 502,
|
|
298
|
+
errorType: "upstream_error",
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
267
302
|
function extractAnthropicErrorDetail(parsed: unknown): string | undefined {
|
|
268
303
|
if (typeof parsed === "string") return parsed.trim() || undefined;
|
|
269
304
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
|
|
@@ -469,10 +504,10 @@ function claudeFamilyVersion(modelId: string): { family: string; major: number;
|
|
|
469
504
|
// Find the segment that actually starts with `claude-`, rather than assuming it is either
|
|
470
505
|
// the first (breaks `anthropic/claude-sonnet-5`) or the last (breaks `claude-sonnet-5/variant`,
|
|
471
506
|
// where the slash carries a vendor suffix rather than a routing prefix).
|
|
472
|
-
const match = /(?:^|\/)claude-([a-z]+)-(\d+)(
|
|
507
|
+
const match = /(?:^|\/)claude-([a-z]+)-(\d+)(?:[.-](\d{1,2}))?(?!\d)/i.exec(modelId);
|
|
473
508
|
if (!match) return undefined;
|
|
474
509
|
return {
|
|
475
|
-
family: match[1]
|
|
510
|
+
family: match[1]!.toLowerCase(),
|
|
476
511
|
major: Number(match[2]),
|
|
477
512
|
minor: match[3] === undefined ? 0 : Number(match[3]),
|
|
478
513
|
};
|
|
@@ -518,6 +553,18 @@ function adaptiveEffort(effort: string): string {
|
|
|
518
553
|
return effort === "minimal" ? "low" : effort;
|
|
519
554
|
}
|
|
520
555
|
|
|
556
|
+
function defaultReasoningEffort(provider: OcxProviderConfig, modelId: string): string | undefined {
|
|
557
|
+
const value = modelRecordValue(provider.modelDefaultReasoningEfforts, modelId);
|
|
558
|
+
if (typeof value !== "string") return undefined;
|
|
559
|
+
const trimmed = value.trim();
|
|
560
|
+
// `__omit__` means "send no reasoning field", not "an effort literally named
|
|
561
|
+
// __omit__". Without this the sentinel reached the wire as
|
|
562
|
+
// `output_config.effort: "__omit__"` on adaptive models, and enabled budget
|
|
563
|
+
// thinking on the rest — the opposite of what it asks for (#2432).
|
|
564
|
+
if (!trimmed || isReasoningEffortOmitted(trimmed)) return undefined;
|
|
565
|
+
return trimmed;
|
|
566
|
+
}
|
|
567
|
+
|
|
521
568
|
function usageFromAnthropic(usage: Record<string, number> | undefined): OcxUsage | undefined {
|
|
522
569
|
if (!usage) return undefined;
|
|
523
570
|
const hasCache = usage.cache_read_input_tokens !== undefined || usage.cache_creation_input_tokens !== undefined;
|
|
@@ -796,11 +843,8 @@ function messagesToAnthropicFormat(
|
|
|
796
843
|
|
|
797
844
|
function toolsToAnthropicFormat(parsed: OcxParsedRequest, toolNames: { toWire: (name: string) => string }): unknown[] | undefined {
|
|
798
845
|
if (!parsed.context.tools || parsed.context.tools.length === 0) return undefined;
|
|
799
|
-
const
|
|
800
|
-
?
|
|
801
|
-
: undefined;
|
|
802
|
-
const tools = allowed
|
|
803
|
-
? parsed.context.tools.filter(t => toolAllowedByChoice(t, allowed, parsed.context.tools))
|
|
846
|
+
const tools = isAllowedToolChoice(parsed.options.toolChoice)
|
|
847
|
+
? parsed.context.tools.filter(toolChoiceToolPredicate(parsed.options.toolChoice, parsed.context.tools))
|
|
804
848
|
: parsed.context.tools;
|
|
805
849
|
if (tools.length === 0) return undefined;
|
|
806
850
|
const converted = tools.map(t => ({
|
|
@@ -929,18 +973,20 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
929
973
|
// anyway, and thinking shares the caller's `max_tokens` — which truncates a small-budget
|
|
930
974
|
// request before it can emit its stop sequence (#545). Say "disabled" out loud where the
|
|
931
975
|
// model both defaults to thinking and accepts being told not to.
|
|
932
|
-
|
|
976
|
+
const effectiveReasoning = parsed.options.reasoning ?? defaultReasoningEffort(provider, parsed.modelId);
|
|
977
|
+
if (effectiveReasoning === "none" && supportsExplicitThinkingDisable(parsed.modelId)) {
|
|
933
978
|
body.thinking = { type: "disabled" };
|
|
934
|
-
} else if (typeof
|
|
979
|
+
} else if (typeof effectiveReasoning === "string" && effectiveReasoning !== "none") {
|
|
935
980
|
if (usesAdaptiveThinking(parsed.modelId)) {
|
|
936
981
|
// Adaptive-thinking models replace the token budget with an effort knob and reject
|
|
937
982
|
// `thinking.type: "enabled"` outright. `max_tokens` still caps thinking plus visible
|
|
938
983
|
// output, so high effort needs the same total-token headroom as budget thinking or a
|
|
939
984
|
// default 8192-token request can spend everything on thought and return empty text.
|
|
940
985
|
body.thinking = { type: "adaptive" };
|
|
941
|
-
|
|
986
|
+
const effort = adaptiveEffort(effectiveReasoning);
|
|
987
|
+
body.output_config = { effort };
|
|
942
988
|
const explicitMaxOut = parsed.options.maxOutputTokens;
|
|
943
|
-
const wantBudget = reasoningBudget(
|
|
989
|
+
const wantBudget = reasoningBudget(effort);
|
|
944
990
|
const floor = wantBudget + OUTPUT_HEADROOM;
|
|
945
991
|
// Preserve explicit caller limits as-is; for omitted limits use the adaptive ceiling
|
|
946
992
|
// so effort=max (budget=32k) still leaves OUTPUT_HEADROOM tokens for visible output.
|
|
@@ -953,7 +999,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
953
999
|
// 400s ("max_tokens must be greater than thinking.budget_tokens"). Size them so max_tokens
|
|
954
1000
|
// always exceeds the budget within a model-safe ceiling, reserving room for visible output.
|
|
955
1001
|
const maxOut = parsed.options.maxOutputTokens ?? DEFAULT_MAX_TOKENS;
|
|
956
|
-
const wantBudget = reasoningBudget(
|
|
1002
|
+
const wantBudget = reasoningBudget(effectiveReasoning);
|
|
957
1003
|
const maxTokens = Math.min(REASONING_MAX_TOKENS_CEILING, Math.max(maxOut, wantBudget + OUTPUT_HEADROOM));
|
|
958
1004
|
const budget = Math.max(MIN_THINKING_BUDGET, Math.min(wantBudget, maxTokens - OUTPUT_FLOOR));
|
|
959
1005
|
body.max_tokens = maxTokens;
|
|
@@ -1267,12 +1313,55 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
1267
1313
|
},
|
|
1268
1314
|
|
|
1269
1315
|
async parseResponse(response: Response, budget: TranslatorBudget): Promise<AdapterEvent[]> {
|
|
1270
|
-
const
|
|
1316
|
+
const parsed: unknown = await response.json();
|
|
1317
|
+
// `response.json()` resolves a body of `null` to `null` without throwing, so the cast below
|
|
1318
|
+
// used to reach `json.content` on it — the #1219 defect at the buffered body root. The
|
|
1319
|
+
// streaming parser has skipped a non-record frame since #1240, but a buffered body has no
|
|
1320
|
+
// next frame to recover into, so this fails closed instead.
|
|
1321
|
+
if (!isAnthropicRecord(parsed)) {
|
|
1322
|
+
return [{
|
|
1323
|
+
type: "error",
|
|
1324
|
+
message: `anthropic response was not a JSON object (${anthropicStructuralValueType(parsed)})`,
|
|
1325
|
+
status: 502,
|
|
1326
|
+
errorType: "upstream_error",
|
|
1327
|
+
}];
|
|
1328
|
+
}
|
|
1329
|
+
const json = parsed;
|
|
1271
1330
|
const responseBytes = new TextEncoder().encode(JSON.stringify(json)).byteLength;
|
|
1272
1331
|
budget.chargeRetained(responseBytes, { kind: "retained_collectors" });
|
|
1273
1332
|
try {
|
|
1274
1333
|
const events: AdapterEvent[] = [];
|
|
1275
|
-
|
|
1334
|
+
// Same retain-then-return tail every other terminal branch in this function uses; naming it
|
|
1335
|
+
// keeps the two shape guards below from drifting out of step with the accounting.
|
|
1336
|
+
const finishWithEvents = (batch: AdapterEvent[]): AdapterEvent[] => {
|
|
1337
|
+
retainTranslatedEventBatch(batch, budget);
|
|
1338
|
+
return batch;
|
|
1339
|
+
};
|
|
1340
|
+
const rawContent: unknown = json.content;
|
|
1341
|
+
// `content` is claimed model output inside a well-formed response, so it is governed by the
|
|
1342
|
+
// #1332 nested-shape rule (fail closed) rather than #1240's root-frame padding rule (skip).
|
|
1343
|
+
// Absence stays legal in both encodings. A present non-array was silently accepted and is
|
|
1344
|
+
// the dangerous case: a string is iterable, so `for (const block of "text")` walked it one
|
|
1345
|
+
// CHARACTER at a time, read `undefined` from every `block.type`, and completed the turn as a
|
|
1346
|
+
// successful empty response — the #2231 failure mode on a different adapter.
|
|
1347
|
+
if (rawContent !== undefined && rawContent !== null && !Array.isArray(rawContent)) {
|
|
1348
|
+
return finishWithEvents([invalidAnthropicShapeEvent({
|
|
1349
|
+
reason: "content_not_array",
|
|
1350
|
+
valueType: anthropicStructuralValueType(rawContent),
|
|
1351
|
+
})]);
|
|
1352
|
+
}
|
|
1353
|
+
if (Array.isArray(rawContent)) {
|
|
1354
|
+
for (let blockIndex = 0; blockIndex < rawContent.length; blockIndex++) {
|
|
1355
|
+
if (!isAnthropicRecord(rawContent[blockIndex])) {
|
|
1356
|
+
return finishWithEvents([invalidAnthropicShapeEvent({
|
|
1357
|
+
reason: "content_block_not_object",
|
|
1358
|
+
blockIndex,
|
|
1359
|
+
valueType: anthropicStructuralValueType(rawContent[blockIndex]),
|
|
1360
|
+
})]);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
const content = rawContent as { type: string; text?: string; id?: string; name?: string; input?: unknown; thinking?: string; reasoning?: string; signature?: string; data?: string }[] | undefined;
|
|
1276
1365
|
if (content) {
|
|
1277
1366
|
for (const block of content) {
|
|
1278
1367
|
if (block.type === "text" && block.text) {
|
package/src/adapters/base.ts
CHANGED
|
@@ -37,6 +37,21 @@ export interface ProviderAdapter {
|
|
|
37
37
|
*/
|
|
38
38
|
buildRequest(parsed: OcxParsedRequest, incoming: IncomingMeta): AdapterRequest | Promise<AdapterRequest>;
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Decide, BEFORE any request is built or sent, that this turn has nothing to ask upstream.
|
|
42
|
+
*
|
|
43
|
+
* Returning a reason short-circuits the turn to a locally constructed completed response: no
|
|
44
|
+
* `buildRequest`, no send, no token estimate, and no empty-completion retry. That last part is
|
|
45
|
+
* why this cannot be expressed as an outputless `done` from `parseStream`: the empty-completion
|
|
46
|
+
* guard treats a terminal with no content as a failed turn and re-invokes the identical request,
|
|
47
|
+
* so an adapter that "successfully returned nothing" would be retried into the very loop it was
|
|
48
|
+
* trying to end.
|
|
49
|
+
*
|
|
50
|
+
* Only for turns whose input already contains the answer — see the Kiro adapter, where replayed
|
|
51
|
+
* history ending in a delivered final answer has nothing left to complete.
|
|
52
|
+
*/
|
|
53
|
+
localTerminal?(parsed: OcxParsedRequest): AdapterLocalTerminal | undefined;
|
|
54
|
+
|
|
40
55
|
fetchResponse?(request: AdapterRequest, ctx?: AdapterFetchContext): Promise<Response>;
|
|
41
56
|
|
|
42
57
|
/**
|
|
@@ -70,10 +85,12 @@ export interface AdapterRequest {
|
|
|
70
85
|
body: string;
|
|
71
86
|
/** Final upstream wire names of custom tools lowered to functions while building this request. */
|
|
72
87
|
convertedRoutedCustomToolNames?: ReadonlySet<string>;
|
|
88
|
+
/** Native custom-tool wire names authorized for representation-only response repair. */
|
|
89
|
+
routedCustomToolRepairNames?: ReadonlySet<string>;
|
|
73
90
|
/** Client tool-search names actually lowered to upstream function calls for this request. */
|
|
74
91
|
convertedRoutedToolSearchNames?: ReadonlySet<string>;
|
|
75
92
|
/** Upstream-only aliases for namespace tools flattened in this request. */
|
|
76
|
-
convertedRoutedNamespaceToolAliases?: ReadonlyMap<string, { namespace: string; name: string }>;
|
|
93
|
+
convertedRoutedNamespaceToolAliases?: ReadonlyMap<string, { namespace: string; name: string; kind: "function" | "custom" }>;
|
|
77
94
|
/** Releases observation of a serialized request body after its final fetch attempt settles. */
|
|
78
95
|
releaseBodyObservation?: () => void;
|
|
79
96
|
/** Exact reasoning parameter emitted by the adapter, for request-log diagnostics only. */
|
|
@@ -117,3 +134,14 @@ export interface AdapterFetchContext {
|
|
|
117
134
|
/** Custom fetch executor to use for physical upstream network requests (defaults to globalThis.fetch). */
|
|
118
135
|
executor?: typeof globalThis.fetch;
|
|
119
136
|
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* An adapter's decision that a turn needs no upstream inference at all.
|
|
140
|
+
*
|
|
141
|
+
* `reason` is diagnostic only. It is never sent to the client and never logged as request
|
|
142
|
+
* content: it names the code path for a maintainer reading a request log, so it must stay a
|
|
143
|
+
* fixed identifier rather than anything derived from the conversation.
|
|
144
|
+
*/
|
|
145
|
+
export interface AdapterLocalTerminal {
|
|
146
|
+
reason: string;
|
|
147
|
+
}
|
|
@@ -3,10 +3,11 @@ import { execFile as execFileCallback } from "node:child_process";
|
|
|
3
3
|
import { promisify } from "node:util";
|
|
4
4
|
import { opendir } from "node:fs/promises";
|
|
5
5
|
import type { AdapterEvent, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTool, OcxUsage } from "../types";
|
|
6
|
-
import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName,
|
|
6
|
+
import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName, toolChoiceToolPredicate } from "../types";
|
|
7
7
|
import type { AdapterFetchContext, AdapterRequest, ProviderAdapter } from "./base";
|
|
8
8
|
import type { TranslatorBudget } from "../lib/translator-budget";
|
|
9
9
|
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
10
|
+
import { debugDroppedFrame } from "../lib/debug";
|
|
10
11
|
import { configuredReasoningEfforts } from "../reasoning-effort";
|
|
11
12
|
import { commandCodeReasoningEfforts, refreshCommandCodeReasoningEfforts } from "../providers/command-code-efforts";
|
|
12
13
|
import { identifyRoutedModel } from "./identity";
|
|
@@ -143,7 +144,8 @@ function wireMessages(messages: OcxMessage[]): Array<Record<string, unknown>> {
|
|
|
143
144
|
if (typeof message.content === "string") content.push({ type: "text", text: message.content });
|
|
144
145
|
else for (const part of message.content) {
|
|
145
146
|
if (part.type === "text") content.push({ type: "text", text: part.text });
|
|
146
|
-
else content.push(wireImagePart(part.imageUrl));
|
|
147
|
+
else if (part.type === "image") content.push(wireImagePart(part.imageUrl));
|
|
148
|
+
else content.push({ type: "text", text: "[video]" });
|
|
147
149
|
}
|
|
148
150
|
out.push({ role: "user", content });
|
|
149
151
|
}
|
|
@@ -156,8 +158,7 @@ function visibleTools(parsed: OcxParsedRequest): OcxTool[] {
|
|
|
156
158
|
if (choice === "none") return [];
|
|
157
159
|
const tools = parsed.context.tools ?? [];
|
|
158
160
|
if (isAllowedToolChoice(choice)) {
|
|
159
|
-
|
|
160
|
-
return tools.filter(tool => toolAllowedByChoice(tool, allowed, tools));
|
|
161
|
+
return tools.filter(toolChoiceToolPredicate(choice, tools));
|
|
161
162
|
}
|
|
162
163
|
if (choice && typeof choice !== "string") {
|
|
163
164
|
const selected = resolveToolChoiceWireName(tools, choice.name);
|
|
@@ -366,7 +367,7 @@ async function*ndjson(response: Response, budget: TranslatorBudget): AsyncGenera
|
|
|
366
367
|
let newline = buffer.indexOf("\n");
|
|
367
368
|
while (newline >= 0) {
|
|
368
369
|
const line = buffer.slice(0, newline).trim(); buffer = buffer.slice(newline + 1);
|
|
369
|
-
if (line)
|
|
370
|
+
if (line) yield* decodeEventLine(line);
|
|
370
371
|
newline = buffer.indexOf("\n");
|
|
371
372
|
}
|
|
372
373
|
const residualBytes = encoder.encode(buffer).byteLength;
|
|
@@ -377,7 +378,7 @@ async function*ndjson(response: Response, budget: TranslatorBudget): AsyncGenera
|
|
|
377
378
|
if (done) break;
|
|
378
379
|
}
|
|
379
380
|
const final = buffer.trim();
|
|
380
|
-
if (final)
|
|
381
|
+
if (final) yield* decodeEventLine(final);
|
|
381
382
|
} finally {
|
|
382
383
|
budget.releaseRetained(bufferBytes, { kind: "live_transient" });
|
|
383
384
|
try { await reader.cancel(); } catch { /* already closed */ }
|
|
@@ -385,6 +386,41 @@ async function*ndjson(response: Response, budget: TranslatorBudget): AsyncGenera
|
|
|
385
386
|
}
|
|
386
387
|
}
|
|
387
388
|
|
|
389
|
+
/**
|
|
390
|
+
* Yield one NDJSON line as an event record, or nothing.
|
|
391
|
+
*
|
|
392
|
+
* `JSON.parse("null")` returns `null` instead of throwing, so the `try/catch` around the parse
|
|
393
|
+
* cannot see it and the `event.type` read in parseStream crashed the turn — the #1219 defect, on
|
|
394
|
+
* the one streaming transport the #1240 audit did not cover because it is NDJSON rather than SSE.
|
|
395
|
+
*
|
|
396
|
+
* A frame that does not parse to a record is padding, not an event: drop it and continue exactly
|
|
397
|
+
* as an unparseable line is already dropped, so a stream whose only frames are junk ends in the
|
|
398
|
+
* same single terminal `done` as an empty body. Skipping is what preserves an answer whose deltas
|
|
399
|
+
* have already arrived — the observed #1219 case is `null` padding BETWEEN content deltas, where
|
|
400
|
+
* terminating would discard a complete response (#1240).
|
|
401
|
+
*
|
|
402
|
+
* Note this deliberately makes a junk-only stream a quiet `[done]` where it previously threw. That
|
|
403
|
+
* throw was an unguarded type assumption, not a designed failure signal, and `[done]` is already
|
|
404
|
+
* what an empty body, a blank-line-only body and an unparseable-only body all produce here. The
|
|
405
|
+
* broader question — whether this adapter should report *any* no-valid-event stream as a failure
|
|
406
|
+
* rather than an empty success — is pre-existing, applies to all four of those inputs equally, and
|
|
407
|
+
* is deliberately not decided by this change.
|
|
408
|
+
*/
|
|
409
|
+
function* decodeEventLine(line: string): Generator<Record<string, unknown>> {
|
|
410
|
+
let parsed: unknown;
|
|
411
|
+
try {
|
|
412
|
+
parsed = JSON.parse(stripEventFrame(line));
|
|
413
|
+
} catch {
|
|
414
|
+
debugDroppedFrame("command-code", line);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
418
|
+
debugDroppedFrame("command-code", line);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
yield parsed as Record<string, unknown>;
|
|
422
|
+
}
|
|
423
|
+
|
|
388
424
|
/** The endpoint is newline-delimited JSON; defensively strip an SSE `data:` frame if the gateway ever switches shapes. */
|
|
389
425
|
function stripEventFrame(line: string): string {
|
|
390
426
|
return line.startsWith("data:") ? line.slice("data:".length).trim() : line;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reversible single-line codec for Cursor composite tool-call ids.
|
|
3
|
+
*
|
|
4
|
+
* Cursor's wire delivers tool-call ids that can be two identifiers glued with a
|
|
5
|
+
* literal newline ("call-<uuid>-<n>\nfc_<uuid>_<n>"). OpenCodex forwards ids
|
|
6
|
+
* verbatim, so that newline leaked into Responses-visible `call_id` values,
|
|
7
|
+
* where line-oriented clients (logging, splitting, validation) break. The codec
|
|
8
|
+
* encodes only ids containing CR/LF into a versioned single-line form and
|
|
9
|
+
* decodes both that form and legacy raw multi-line ids back to the exact
|
|
10
|
+
* upstream bytes before anything is serialized toward Cursor.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const CALL_ID_PREFIX = "ocxc1_";
|
|
14
|
+
|
|
15
|
+
/** True when the id needs encoding to survive line-oriented consumers. */
|
|
16
|
+
function needsEncoding(id: string): boolean {
|
|
17
|
+
return id.includes("\n") || id.includes("\r");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Encode a Cursor wire call id into a single-line Responses-safe id. */
|
|
21
|
+
export function encodeCursorCallId(id: string): string {
|
|
22
|
+
if (!needsEncoding(id)) return id;
|
|
23
|
+
return CALL_ID_PREFIX + Buffer.from(id, "utf8").toString("base64url");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Decode a Responses-visible call id back to the exact Cursor wire id.
|
|
28
|
+
* Non-encoded ids (including legacy raw multi-line ids replayed by older
|
|
29
|
+
* clients) pass through unchanged; a malformed encoded payload also passes
|
|
30
|
+
* through rather than corrupting pairing.
|
|
31
|
+
*/
|
|
32
|
+
export function decodeCursorCallId(id: string): string {
|
|
33
|
+
if (!id.startsWith(CALL_ID_PREFIX)) return id;
|
|
34
|
+
const payload = id.slice(CALL_ID_PREFIX.length);
|
|
35
|
+
if (payload.length === 0) return id;
|
|
36
|
+
try {
|
|
37
|
+
const decoded = Buffer.from(payload, "base64url").toString("utf8");
|
|
38
|
+
// Round-trip guard: only trust payloads our encoder could have produced.
|
|
39
|
+
if (Buffer.from(decoded, "utf8").toString("base64url") !== payload) return id;
|
|
40
|
+
return decoded;
|
|
41
|
+
} catch {
|
|
42
|
+
return id;
|
|
43
|
+
}
|
|
44
|
+
}
|