@bitkyc08/opencodex 2.63.0 → 2.64.0-preview.20260923
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 +4 -3
- package/README.md +46 -33
- package/assets/download-linux.svg +10 -0
- package/assets/download-macos.svg +10 -0
- package/assets/download-windows.svg +10 -0
- package/bin/ocx.mjs +12 -4
- package/gui/dist/assets/App-D5eN1TID.js +50 -0
- package/gui/dist/assets/App-I5AnaSLh.css +1 -0
- package/gui/dist/assets/{Tray-CncKDBTp.js → Tray-Bh_ErQDh.js} +1 -1
- package/gui/dist/assets/index-BEq4OCOz.js +86 -0
- package/gui/dist/assets/index-DiBRuK-d.css +1 -0
- package/gui/dist/assets/usage-companion-chart-DBG_37kQ.js +1 -0
- package/gui/dist/index.html +2 -2
- package/native/remote-workspace-helper/src/protocol.rs +5 -0
- package/package.json +4 -1
- package/src/adapters/anthropic.ts +66 -7
- package/src/adapters/codebuddy/adapter.ts +112 -16
- package/src/adapters/codebuddy/mcp-server.ts +180 -0
- package/src/adapters/codebuddy/scaffold-guard.ts +25 -8
- package/src/adapters/codebuddy/tool-bridge.ts +597 -0
- package/src/adapters/coding-agent/protocol.ts +123 -10
- package/src/adapters/coding-agent/turn.ts +324 -2
- package/src/adapters/command-code-restored-schema.ts +112 -0
- package/src/adapters/command-code-tool-text.ts +589 -0
- package/src/adapters/command-code.ts +80 -6
- package/src/adapters/cursor/catalog.ts +12 -0
- package/src/adapters/cursor/current-request.ts +46 -0
- package/src/adapters/cursor/discovery.ts +1 -1
- package/src/adapters/cursor/effort-map.ts +4 -0
- package/src/adapters/cursor/native-exec.ts +15 -0
- package/src/adapters/cursor/protobuf-events.ts +40 -12
- package/src/adapters/cursor/protobuf-request.ts +83 -18
- package/src/adapters/cursor/request-builder.ts +5 -4
- package/src/adapters/cursor/tool-guidance.ts +1 -1
- package/src/adapters/devin/live-models.ts +7 -0
- package/src/adapters/inline-think-tags.ts +251 -0
- package/src/adapters/kiro/adapter.ts +1 -0
- package/src/adapters/kiro/stream.ts +5 -3
- package/src/adapters/kiro/usage.ts +4 -3
- package/src/adapters/mimo-free.ts +37 -18
- package/src/adapters/openai-chat/messages.ts +5 -5
- package/src/adapters/openai-chat/tool-schema.ts +124 -2
- package/src/adapters/openai-chat.ts +13 -3
- package/src/adapters/openai-responses/passthrough.ts +15 -2
- package/src/adapters/openai-responses/reasoning.ts +14 -1
- package/src/adapters/openai-responses/request-strips.ts +29 -3
- package/src/adapters/openai-responses/tool-output-recovery.ts +9 -3
- package/src/adapters/qoder/adapter.ts +15 -12
- package/src/adapters/qoder/scaffold-guard.ts +45 -13
- package/src/bridge/internal.ts +20 -0
- package/src/bridge/sse.ts +14 -5
- package/src/claude/agents-inject.ts +2 -1
- package/src/claude/desktop-applied-marker.ts +44 -0
- package/src/claude/desktop-profile.ts +22 -12
- package/src/claude/inbound.ts +8 -3
- package/src/cli/access.ts +22 -5
- package/src/cli/account-api.ts +6 -0
- package/src/cli/account-auth.ts +5 -2
- package/src/cli/account.ts +5 -6
- package/src/cli/aside-profiles.ts +61 -1
- package/src/cli/claude-desktop.ts +3 -2
- package/src/cli/claude.ts +3 -1
- package/src/cli/codex-cli-update.ts +2 -2
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/dispatch.ts +12 -4
- package/src/cli/index.ts +98 -10
- package/src/cli/registry.ts +1 -1
- package/src/cli/resolve.ts +113 -1
- package/src/cli/root.ts +5 -0
- package/src/cli/stop-approval.ts +186 -0
- package/src/cli/stop-report.ts +1 -1
- package/src/cli/system-restart-client.ts +19 -4
- package/src/client/hub-client.ts +25 -18
- package/src/clients/config-export.ts +12 -4
- package/src/codex/account-auto-switch.ts +59 -0
- package/src/codex/account-lifecycle.ts +7 -0
- package/src/codex/account-priority.ts +10 -0
- package/src/codex/account-usability.ts +9 -0
- package/src/codex/auth-api/account-list.ts +5 -0
- package/src/codex/auth-api/login-flow.ts +15 -5
- package/src/codex/auth-api/login-state.ts +5 -16
- package/src/codex/auth-api/pool-mode-gate.ts +30 -7
- package/src/codex/auth-api/routes.ts +39 -3
- package/src/codex/auth-context.ts +145 -17
- package/src/codex/catalog/build-entries.ts +2 -1
- package/src/codex/catalog/effort.ts +8 -2
- package/src/codex/catalog/metadata.ts +33 -7
- package/src/codex/catalog/native-models.ts +102 -5
- package/src/codex/catalog/parsing.ts +2 -0
- package/src/codex/catalog/provider-models.ts +19 -6
- package/src/codex/cli-installation-targets.ts +44 -40
- package/src/codex/codex-write-lock.ts +32 -2
- package/src/codex/inject/multi-agent-v2.ts +90 -0
- package/src/codex/inject/plan.ts +365 -0
- package/src/codex/inject.ts +237 -362
- package/src/codex/log-guard/maintenance.ts +30 -13
- package/src/codex/project-config-warnings.ts +47 -7
- package/src/codex/prompt-layers/encoding.ts +41 -0
- package/src/codex/prompt-layers/toml-read.ts +1 -1
- package/src/codex/prompt-layers.ts +17 -13
- package/src/codex/prompt-text-probe.ts +20 -10
- package/src/codex/quota-observation-freshness.ts +34 -0
- package/src/codex/quota-rejection.ts +8 -4
- package/src/codex/quota.ts +4 -2
- package/src/codex/routing/selection.ts +32 -9
- package/src/codex/routing.ts +53 -11
- package/src/codex/subagent-model-fallback.ts +4 -3
- package/src/codex/sync.ts +11 -0
- package/src/codex/windows-installation-files.ts +33 -11
- package/src/codex/write-coordination.ts +4 -0
- package/src/combos/request.ts +6 -2
- package/src/companion/settings.ts +6 -1
- package/src/config/atomic-write.ts +12 -1
- package/src/config/derived-registries.ts +29 -0
- package/src/config/diagnostics.ts +17 -0
- package/src/config/live-reconcile.ts +11 -2
- package/src/config/load-degrade.ts +9 -2
- package/src/config/persist-unlocked.ts +4 -3
- package/src/config/persisted-mutation.ts +94 -0
- package/src/config/provider-validation.ts +1 -1
- package/src/config/proxy-env.ts +62 -12
- package/src/config/rebase-provenance.ts +80 -1
- package/src/config/schema/config-schema.ts +5 -0
- package/src/config/schema/leaf-validators.ts +50 -1
- package/src/config/subagent-models.ts +41 -15
- package/src/config.ts +18 -95
- package/src/generated/compatibility-version.json +330 -218
- package/src/generated/model-metadata.ts +8 -5
- package/src/github/star-state.ts +46 -6
- package/src/grok/inject.ts +74 -40
- package/src/grok/status.ts +11 -4
- package/src/integrations/cursor-effort-table.ts +50 -13
- package/src/integrations/raycast-detect.ts +19 -4
- package/src/integrations/serialize.ts +11 -1
- package/src/lib/bounded-body.ts +30 -0
- package/src/lib/crash-guard.ts +52 -4
- package/src/lib/local-aside-sync-contract.ts +41 -0
- package/src/lib/package-tree-integrity.ts +181 -9
- package/src/lib/proxy-env.ts +18 -1
- package/src/lib/request-failure-attribution.ts +1 -0
- package/src/lib/request-failure-model.ts +3 -0
- package/src/lib/service-secrets.ts +99 -2
- package/src/lib/socks5-fetch.ts +43 -14
- package/src/lib/token-estimate.ts +17 -2
- package/src/oauth/command-code.ts +3 -2
- package/src/oauth/generic-account-failover.ts +29 -1
- package/src/oauth/index.ts +24 -16
- package/src/oauth/login-flow-state.ts +5 -5
- package/src/oauth/meta-muse-device.ts +49 -7
- package/src/oauth/store.ts +74 -9
- package/src/providers/alibaba-region-backup.ts +16 -1
- package/src/providers/anthropic-fast.ts +89 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +288 -0
- package/src/providers/anthropic-reset-grants.ts +329 -0
- package/src/providers/claude-cli-identity.ts +12 -0
- package/src/providers/command-code-efforts.ts +184 -102
- package/src/providers/derive.ts +11 -3
- package/src/providers/fastwire.ts +16 -3
- package/src/providers/label.ts +8 -3
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/quota/vendor-probes-oauth.ts +2 -1
- package/src/providers/reasoning-metadata.ts +38 -15
- package/src/providers/registry/entries-core.ts +95 -7
- package/src/providers/registry/entries-extended.ts +21 -10
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/model-seeds.ts +31 -3
- package/src/providers/registry/types.ts +3 -1
- package/src/providers/resolved-model-policy-merge.ts +38 -8
- package/src/providers/resolved-model-policy.ts +4 -2
- package/src/providers/service-tier.ts +3 -1
- package/src/reasoning-effort.ts +6 -5
- package/src/responses/code-mode-helper-compat.ts +14 -4
- package/src/responses/code-mode-shell-input.ts +54 -0
- package/src/responses/custom-tool-compat.ts +173 -5
- package/src/responses/parser.ts +38 -2
- package/src/responses/reasoning-replay-cache.ts +26 -0
- package/src/router.ts +11 -1
- package/src/routing/history/indexer.ts +7 -2
- package/src/routing/history/schema.ts +3 -1
- package/src/server/adapter-resolve.ts +2 -2
- package/src/server/auth-cors.ts +3 -0
- package/src/server/chat-completions.ts +14 -1
- package/src/server/chat-native.ts +17 -0
- package/src/server/claude-messages.ts +4 -3
- package/src/server/direct-local-http.ts +45 -19
- package/src/server/gui-session.ts +6 -15
- package/src/server/index/package-tree-guard.ts +53 -0
- package/src/server/index/serve-options.ts +17 -3
- package/src/server/index/startup-warnings.ts +17 -0
- package/src/server/index.ts +15 -17
- package/src/server/lifecycle.ts +8 -0
- package/src/server/live.ts +54 -4
- package/src/server/management/agent-settings-routes.ts +44 -8
- package/src/server/management/anthropic-reset-grant-routes.ts +252 -0
- package/src/server/management/codex-prompt-routes.ts +5 -1
- package/src/server/management/config-routes.ts +14 -4
- package/src/server/management/oauth-account-routes.ts +17 -1
- package/src/server/management/provider-overwrite-carry.ts +164 -0
- package/src/server/management/provider-routes.ts +36 -6
- package/src/server/management/remote-workspace-routes.ts +2 -2
- package/src/server/management/route-registry.ts +2 -0
- package/src/server/management/shadow-call-validation.ts +39 -0
- package/src/server/management/system-restart.ts +72 -6
- package/src/server/management-api.ts +16 -2
- package/src/server/management-auth.ts +52 -8
- package/src/server/proxy-liveness.ts +110 -4
- package/src/server/request-log.ts +26 -9
- package/src/server/request-metrics.ts +5 -0
- package/src/server/responses/adapter-continuation.ts +6 -3
- package/src/server/responses/adapter-dispatch.ts +65 -1
- package/src/server/responses/compact.ts +31 -1
- package/src/server/responses/compaction-routing.ts +28 -3
- package/src/server/responses/core-codex-account.ts +85 -26
- package/src/server/responses/core-combo-failure.ts +16 -15
- package/src/server/responses/core-normalize.ts +5 -1
- package/src/server/responses/core-opaque-recovery.ts +50 -2
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/core-replay.ts +7 -0
- package/src/server/responses/core.ts +1 -1
- package/src/server/responses/encrypted-payload.ts +23 -9
- package/src/server/responses/passthrough-delivery.ts +55 -35
- package/src/server/responses/passthrough-dispatch.ts +12 -13
- package/src/server/responses/policy-fallback.ts +12 -1
- package/src/server/responses/request-prepare.ts +46 -7
- package/src/server/responses/request-spend.ts +31 -15
- package/src/server/responses/run-turn-execution.ts +6 -5
- package/src/server/responses/shadow-target-availability.ts +61 -0
- package/src/server/responses-custom-tool-repair.ts +2 -0
- package/src/service/claim.ts +185 -0
- package/src/service/cli.ts +10 -1
- package/src/service/guarded-manager-target.ts +151 -0
- package/src/service/guards.ts +47 -48
- package/src/service/managing-cli.ts +170 -0
- package/src/service/orchestration.ts +3 -1
- package/src/service/state.ts +4 -0
- package/src/service/systemd.ts +16 -1
- package/src/service.ts +1 -1
- package/src/types/config.ts +8 -0
- package/src/types/provider.ts +16 -0
- package/src/types/request.ts +10 -0
- package/src/types/tools.ts +9 -1
- package/src/types/wire.ts +68 -4
- package/src/types.ts +1 -0
- package/src/update/job.ts +10 -16
- package/src/update/npm-invocation.mjs +17 -16
- package/src/update/transactional-install.d.mts +22 -1
- package/src/update/transactional-install.mjs +176 -19
- package/src/update/update-failure-guidance.d.mts +8 -0
- package/src/update/update-failure-guidance.mjs +47 -0
- package/src/usage/expected-prices.ts +54 -12
- package/src/usage/log.ts +117 -3
- package/src/usage/telemetry-contract.ts +1 -0
- package/src/usage/timeline.ts +34 -6
- package/src/usage/user-cost-overlay-reconciler.ts +3 -3
- package/src/vision/reasoning.ts +2 -4
- package/src/web-search/xai-executor.ts +14 -4
- package/gui/dist/assets/App-BqrsSrIR.js +0 -50
- package/gui/dist/assets/index-C6SJrh0N.js +0 -86
- package/gui/dist/assets/index-DdDunwDb.css +0 -1
- package/gui/dist/assets/usage-companion-chart-CzAAAB1o.js +0 -1
- package/src/adapters/kiro-thinking.ts +0 -112
|
@@ -209,8 +209,9 @@ export function cursorRequestEmitsFastVariant(parsed: OcxParsedRequest): boolean
|
|
|
209
209
|
|
|
210
210
|
/**
|
|
211
211
|
* Resolve a `cursor/<model>` selection + Codex reasoning effort to Cursor's requested model shape.
|
|
212
|
-
* Most models encode effort in a flat id (`claude-4.6-opus-high`). Grok Fast is
|
|
213
|
-
*
|
|
212
|
+
* Most models encode effort in a flat id (`claude-4.6-opus-high`). Grok 4.5/4.6 Fast is
|
|
213
|
+
* parameterized: current Cursor clients send the matching base id plus `effort` and `fast` parameters;
|
|
214
|
+
* Grok 4.7 (no wirePrefix) instead uses the flattened effort-fast id.
|
|
214
215
|
* A fully-qualified id (one that is not a known effort base) passes through unchanged.
|
|
215
216
|
*/
|
|
216
217
|
function normalizeCursorModelId(modelId: string, reasoning?: string, fast?: boolean, liveRosterScope?: string): {
|
|
@@ -226,8 +227,8 @@ function normalizeCursorModelId(modelId: string, reasoning?: string, fast?: bool
|
|
|
226
227
|
// resolver owns effort composition, variant dimensions, the synthetic -1m
|
|
227
228
|
// marker (ultra -> Max Mode, evidence-gated), and the cursor- wire prefix.
|
|
228
229
|
const id = selection.modelId;
|
|
229
|
-
// Grok Fast stays parameterized: current Cursor clients send the base id
|
|
230
|
-
// plus effort/fast parameters
|
|
230
|
+
// Grok 4.5/4.6 Fast stays parameterized: current Cursor clients send the base id
|
|
231
|
+
// plus effort/fast parameters; 4.7 (no wirePrefix) uses the flattened effort-fast id.
|
|
231
232
|
const grokFast = cursorGrokFastSelection(id, reasoning, fast);
|
|
232
233
|
if (grokFast) {
|
|
233
234
|
return {
|
|
@@ -185,7 +185,7 @@ export function buildCursorToolGuidanceSystemNote(
|
|
|
185
185
|
// Code mode: shell/edit/MCP live inside freeform `exec` as nested helpers. Without this the
|
|
186
186
|
// model probes for a top-level shell tool that is not there.
|
|
187
187
|
codeMode
|
|
188
|
-
? `\`${CODEX_UNIFIED_EXEC_TOOL}\` is Codex code mode: its body is JavaScript evaluated in a V8 isolate, not a shell command and not Node. Shell, file edits, and MCP are nested helpers called INSIDE that body as \`await tools.<name>(...)\`, for example \`await tools.exec_command({cmd: \"ls\"})\`. Read the tool description and the isolate global \`ALL_TOOLS\` (not \`tools.ALL_TOOLS\`) for helpers this turn provides; absence from the top-level catalog or from \`exec\`'s description is not absence. Those nested helpers are not themselves top-level tools, so do not call \`exec_command\` or \`shell_command\` at the top level here${codeModeOtherTopLevelNames.length > 0 ? `; every other tool this turn lists, including ${quotedNames(codeModeOtherTopLevelNames)}, remains callable at the top level as usual` : ""}. Nested \`tools.apply_patch(input)\` is host-executed: the string must begin exactly with \`*** Begin Patch\` and end with \`*** End Patch\`, each marker line being three asterisks, one space, the two words, then end of line with no further asterisks. OpenCodex does not rewrite JavaScript inside exec, so extra asterisks on a marker line are rejected by Codex before the file is touched.`
|
|
188
|
+
? `\`${CODEX_UNIFIED_EXEC_TOOL}\` is Codex code mode: its body is JavaScript evaluated in a V8 isolate, not a shell command and not Node. Shell, file edits, and MCP are nested helpers called INSIDE that body as \`await tools.<name>(...)\`, for example \`text(await tools.exec_command({cmd: \"ls\"}))\`. Read the tool description and the isolate global \`ALL_TOOLS\` (not \`tools.ALL_TOOLS\`) for helpers this turn provides; absence from the top-level catalog or from \`exec\`'s description is not absence. Those nested helpers are not themselves top-level tools, so do not call \`exec_command\` or \`shell_command\` at the top level here${codeModeOtherTopLevelNames.length > 0 ? `; every other tool this turn lists, including ${quotedNames(codeModeOtherTopLevelNames)}, remains callable at the top level as usual` : ""}. Nested \`tools.apply_patch(input)\` is host-executed: the string must begin exactly with \`*** Begin Patch\` and end with \`*** End Patch\`, each marker line being three asterisks, one space, the two words, then end of line with no further asterisks. OpenCodex does not rewrite JavaScript inside exec, so extra asterisks on a marker line are rejected by Codex before the file is touched.`
|
|
189
189
|
: undefined,
|
|
190
190
|
codeMode
|
|
191
191
|
? CODE_MODE_RESULT_ECHO_SENTENCE + " There is no `require`, no `module`, and no filesystem or network globals; reach the host only through the nested helpers. " + CODE_MODE_HOST_CONTRACT_SENTENCE
|
|
@@ -30,6 +30,7 @@ export const DEVIN_STATIC_MODELS = [
|
|
|
30
30
|
"glm-5-2",
|
|
31
31
|
"kimi-k2-7",
|
|
32
32
|
"grok-4-5",
|
|
33
|
+
"grok-4-7",
|
|
33
34
|
] as const;
|
|
34
35
|
|
|
35
36
|
/**
|
|
@@ -73,6 +74,9 @@ export const DEVIN_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
|
73
74
|
"gemini-3-8-flash": 1_048_576,
|
|
74
75
|
"grok-4-5": 500_000,
|
|
75
76
|
"grok-4-6": 500_000,
|
|
77
|
+
// Live Devin catalog context_length, 2026-09-23:
|
|
78
|
+
// devlog/_plan/260923_grok47_parity/010_probe-evidence.md.
|
|
79
|
+
"grok-4-7": 500_000,
|
|
76
80
|
};
|
|
77
81
|
|
|
78
82
|
/**
|
|
@@ -135,6 +139,9 @@ export function sortDevinRungs(rungs: Iterable<string>): string[] {
|
|
|
135
139
|
*/
|
|
136
140
|
export const DEVIN_MODEL_EFFORTS: Record<string, string[]> = {
|
|
137
141
|
"swe-2": ["medium", "high", "max"],
|
|
142
|
+
// Live Devin catalog, 2026-09-23:
|
|
143
|
+
// devlog/_plan/260923_grok47_parity/010_probe-evidence.md.
|
|
144
|
+
"grok-4-7": ["low", "medium", "high", "xhigh", "max"],
|
|
138
145
|
};
|
|
139
146
|
|
|
140
147
|
/**
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import type { AdapterEvent } from "../types";
|
|
2
|
+
import { modelInList } from "../types";
|
|
3
|
+
import type { TranslatorBudget } from "../lib/translator-budget";
|
|
4
|
+
|
|
5
|
+
type ThinkingTag = "<thinking>" | "<think>" | "<reasoning>";
|
|
6
|
+
type ParserState = "pre" | "thinking" | "scanning" | "streaming";
|
|
7
|
+
|
|
8
|
+
const OPEN_TAGS: ThinkingTag[] = ["<thinking>", "<think>", "<reasoning>"];
|
|
9
|
+
const MAX_OPEN_TAG = Math.max(...OPEN_TAGS.map(t => t.length));
|
|
10
|
+
const MAX_CLOSE_TAG = Math.max(...OPEN_TAGS.map(t => `</${t.slice(1)}`.length));
|
|
11
|
+
|
|
12
|
+
function closeTagFor(openTag: ThinkingTag): string {
|
|
13
|
+
return `</${openTag.slice(1)}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isPossibleOpenTagPrefix(text: string): boolean {
|
|
17
|
+
return OPEN_TAGS.some(tag => tag.startsWith(text) && text.length < tag.length);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Move a send boundary back one unit rather than splitting a surrogate pair into U+FFFD. */
|
|
21
|
+
function surrogateSafeCut(text: string, cut: number): number {
|
|
22
|
+
if (cut <= 0 || cut >= text.length) return Math.max(0, Math.min(cut, text.length));
|
|
23
|
+
const atCut = text.charCodeAt(cut - 1);
|
|
24
|
+
return atCut >= 0xd800 && atCut <= 0xdbff ? cut - 1 : cut;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface InlineThinkTagOptions {
|
|
28
|
+
/**
|
|
29
|
+
* Keep scanning for further think blocks after the first one closes. Kiro emits a single
|
|
30
|
+
* leading block, so it leaves this off and streams the rest verbatim. MiniMax M-series
|
|
31
|
+
* interleaves several blocks with answer segments, so a reusing adapter opts in.
|
|
32
|
+
*/
|
|
33
|
+
interleaved?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Recovers thinking that a gateway left inline in visible content as `<think>` blocks instead of
|
|
38
|
+
* a separate `reasoning_content` / `reasoning_details` field. Shared by the Kiro adapter and by
|
|
39
|
+
* the openai-chat adapter's opt-in `inlineThinkTagModels`.
|
|
40
|
+
*/
|
|
41
|
+
export class InlineThinkTagParser {
|
|
42
|
+
private state: ParserState = "pre";
|
|
43
|
+
private preWhitespaceChunks: string[] = [];
|
|
44
|
+
private preWhitespaceLength = 0;
|
|
45
|
+
private preWhitespaceBytes = 0;
|
|
46
|
+
private preBuffer = "";
|
|
47
|
+
private thinkingBuffer = "";
|
|
48
|
+
private closeTag = "";
|
|
49
|
+
|
|
50
|
+
private readonly interleaved: boolean;
|
|
51
|
+
|
|
52
|
+
constructor(private readonly budget?: TranslatorBudget, options?: InlineThinkTagOptions) {
|
|
53
|
+
this.interleaved = options?.interleaved === true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private replaceCarry(field: "preBuffer" | "thinkingBuffer", next: string): void {
|
|
57
|
+
const previous = this[field];
|
|
58
|
+
if (previous === next) return;
|
|
59
|
+
const previousBytes = Buffer.byteLength(previous);
|
|
60
|
+
const nextBytes = Buffer.byteLength(next);
|
|
61
|
+
const reservation = this.budget?.reserveTransient(nextBytes, { kind: "reasoning" });
|
|
62
|
+
this[field] = next;
|
|
63
|
+
reservation?.commitRetained();
|
|
64
|
+
this.budget?.releaseRetained(previousBytes, { kind: "reasoning" });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private appendPreWhitespace(text: string): void {
|
|
68
|
+
if (!text) return;
|
|
69
|
+
const bytes = Buffer.byteLength(text);
|
|
70
|
+
const reservation = this.budget?.reserveTransient(bytes, { kind: "reasoning" });
|
|
71
|
+
this.preWhitespaceChunks.push(text);
|
|
72
|
+
this.preWhitespaceLength += text.length;
|
|
73
|
+
this.preWhitespaceBytes += bytes;
|
|
74
|
+
reservation?.commitRetained();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private finishPreWhitespace(emit: boolean): string {
|
|
78
|
+
if (this.preWhitespaceLength === 0) return "";
|
|
79
|
+
const reservation = emit ? this.budget?.reserveTransient(this.preWhitespaceBytes, { kind: "reasoning" }) : undefined;
|
|
80
|
+
try {
|
|
81
|
+
return emit ? this.preWhitespaceChunks.join("") : "";
|
|
82
|
+
} finally {
|
|
83
|
+
this.preWhitespaceChunks.length = 0;
|
|
84
|
+
this.preWhitespaceLength = 0;
|
|
85
|
+
this.budget?.releaseRetained(this.preWhitespaceBytes, { kind: "reasoning" });
|
|
86
|
+
this.preWhitespaceBytes = 0;
|
|
87
|
+
reservation?.release();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
feed(text: string): AdapterEvent[] {
|
|
92
|
+
if (!text) return [];
|
|
93
|
+
if (this.state === "streaming") return [{ type: "text_delta", text }];
|
|
94
|
+
let input = text;
|
|
95
|
+
if (this.state === "pre") {
|
|
96
|
+
if (this.preBuffer) {
|
|
97
|
+
input = this.preBuffer + text;
|
|
98
|
+
this.replaceCarry("preBuffer", "");
|
|
99
|
+
} else {
|
|
100
|
+
const stripped = text.trimStart();
|
|
101
|
+
const leadingLength = text.length - stripped.length;
|
|
102
|
+
if (leadingLength > 0) this.appendPreWhitespace(text.slice(0, leadingLength));
|
|
103
|
+
if (!stripped) return [];
|
|
104
|
+
input = stripped;
|
|
105
|
+
}
|
|
106
|
+
const openTag = OPEN_TAGS.find(tag => input.startsWith(tag));
|
|
107
|
+
if (openTag) {
|
|
108
|
+
const leading = this.finishPreWhitespace(this.interleaved);
|
|
109
|
+
this.state = "thinking";
|
|
110
|
+
this.closeTag = closeTagFor(openTag);
|
|
111
|
+
const events: AdapterEvent[] = leading ? [{ type: "text_delta", text: leading }] : [];
|
|
112
|
+
return this.drainChunk(input, openTag.length, events);
|
|
113
|
+
}
|
|
114
|
+
if (input.length <= MAX_OPEN_TAG && isPossibleOpenTagPrefix(input)) {
|
|
115
|
+
this.replaceCarry("preBuffer", input);
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
this.state = "streaming";
|
|
119
|
+
return [{ type: "text_delta", text: this.finishPreWhitespace(true) + input }];
|
|
120
|
+
}
|
|
121
|
+
if (this.state === "thinking") {
|
|
122
|
+
input = this.thinkingBuffer + text;
|
|
123
|
+
this.replaceCarry("thinkingBuffer", "");
|
|
124
|
+
} else {
|
|
125
|
+
input = this.preBuffer + text;
|
|
126
|
+
this.replaceCarry("preBuffer", "");
|
|
127
|
+
}
|
|
128
|
+
return this.drainChunk(input, 0, []);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
flush(): AdapterEvent[] {
|
|
132
|
+
if (this.state === "thinking") {
|
|
133
|
+
const out = this.thinkingBuffer;
|
|
134
|
+
this.replaceCarry("thinkingBuffer", "");
|
|
135
|
+
this.state = "streaming";
|
|
136
|
+
return out ? [{ type: "reasoning_raw_delta", text: out }] : [];
|
|
137
|
+
}
|
|
138
|
+
if (this.preWhitespaceLength > 0 || this.preBuffer) {
|
|
139
|
+
const out = this.finishPreWhitespace(true) + this.preBuffer;
|
|
140
|
+
this.replaceCarry("preBuffer", "");
|
|
141
|
+
this.state = "streaming";
|
|
142
|
+
return [{ type: "text_delta", text: out }];
|
|
143
|
+
}
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Release any partial tag/content carry when the owning stream stops early. */
|
|
148
|
+
dispose(): void {
|
|
149
|
+
this.finishPreWhitespace(false);
|
|
150
|
+
this.replaceCarry("preBuffer", "");
|
|
151
|
+
this.replaceCarry("thinkingBuffer", "");
|
|
152
|
+
this.closeTag = "";
|
|
153
|
+
this.state = "streaming";
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private drainChunk(input: string, start: number, events: AdapterEvent[]): AdapterEvent[] {
|
|
157
|
+
let offset = start;
|
|
158
|
+
for (;;) {
|
|
159
|
+
if (this.state === "thinking") {
|
|
160
|
+
const idx = input.indexOf(this.closeTag, offset);
|
|
161
|
+
if (idx >= 0) {
|
|
162
|
+
if (idx > offset) events.push({ type: "reasoning_raw_delta", text: input.slice(offset, idx) });
|
|
163
|
+
offset = idx + this.closeTag.length;
|
|
164
|
+
if (!this.interleaved) {
|
|
165
|
+
// Opt-in Chat answers are byte-preserving; keep Kiro's legacy normalization.
|
|
166
|
+
const after = input.slice(offset).trimStart();
|
|
167
|
+
this.state = "streaming";
|
|
168
|
+
if (after) events.push({ type: "text_delta", text: after });
|
|
169
|
+
return events;
|
|
170
|
+
}
|
|
171
|
+
this.state = "scanning";
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
// Keep only a possible close tag, and do not split a surrogate pair.
|
|
175
|
+
const cut = Math.max(offset, surrogateSafeCut(input, input.length - MAX_CLOSE_TAG));
|
|
176
|
+
if (cut > offset) events.push({ type: "reasoning_raw_delta", text: input.slice(offset, cut) });
|
|
177
|
+
this.replaceCarry("thinkingBuffer", input.slice(cut));
|
|
178
|
+
return events;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Interleaved mode has already seen a leading tag; later tags delimit anywhere.
|
|
182
|
+
let openIndex = input.indexOf("<", offset);
|
|
183
|
+
let openTag: ThinkingTag | undefined;
|
|
184
|
+
while (openIndex >= 0) {
|
|
185
|
+
openTag = OPEN_TAGS.find(tag => input.startsWith(tag, openIndex));
|
|
186
|
+
if (openTag) break;
|
|
187
|
+
openIndex = input.indexOf("<", openIndex + 1);
|
|
188
|
+
}
|
|
189
|
+
if (openIndex >= 0 && openTag) {
|
|
190
|
+
if (openIndex > offset) events.push({ type: "text_delta", text: input.slice(offset, openIndex) });
|
|
191
|
+
offset = openIndex + openTag.length;
|
|
192
|
+
this.state = "thinking";
|
|
193
|
+
this.closeTag = closeTagFor(openTag);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
// Hold back only a possible open-tag prefix, with a surrogate-safe boundary.
|
|
197
|
+
const cut = Math.max(offset, surrogateSafeCut(input, input.length - (MAX_OPEN_TAG - 1)));
|
|
198
|
+
if (cut > offset) events.push({ type: "text_delta", text: input.slice(offset, cut) });
|
|
199
|
+
this.replaceCarry("preBuffer", input.slice(cut));
|
|
200
|
+
return events;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Visible-content splitter the openai-chat adapter holds for the life of one response. */
|
|
206
|
+
export interface InlineThinkContentSplitter {
|
|
207
|
+
feed(text: string): AdapterEvent[];
|
|
208
|
+
flush(): AdapterEvent[];
|
|
209
|
+
dispose(): void;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const PASSTHROUGH: InlineThinkContentSplitter = {
|
|
213
|
+
feed: text => [{ type: "text_delta", text }],
|
|
214
|
+
flush: () => [],
|
|
215
|
+
dispose: () => { /* nothing carried */ },
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Opt-in recovery for `inlineThinkTagModels`. A model that is not listed gets a passthrough that
|
|
220
|
+
* never inspects or rewrites visible content, so the 66 registry providers sharing the openai-chat
|
|
221
|
+
* adapter keep byte-exact behavior.
|
|
222
|
+
*/
|
|
223
|
+
export function createInlineThinkContentSplitter(
|
|
224
|
+
models: string[] | undefined,
|
|
225
|
+
modelId: string | undefined,
|
|
226
|
+
budget?: TranslatorBudget,
|
|
227
|
+
): InlineThinkContentSplitter {
|
|
228
|
+
if (!modelInList(models, modelId ?? "")) return PASSTHROUGH;
|
|
229
|
+
const parser = new InlineThinkTagParser(budget, { interleaved: true });
|
|
230
|
+
return {
|
|
231
|
+
// An empty content delta stays an empty delta: it is a wire signal, not thinking.
|
|
232
|
+
feed: text => (text.length === 0 ? [{ type: "text_delta", text }] : parser.feed(text)),
|
|
233
|
+
flush: () => parser.flush(),
|
|
234
|
+
dispose: () => parser.dispose(),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** One-shot form for a non-streaming response body. */
|
|
239
|
+
export function splitInlineThinkContent(
|
|
240
|
+
models: string[] | undefined,
|
|
241
|
+
modelId: string | undefined,
|
|
242
|
+
budget: TranslatorBudget | undefined,
|
|
243
|
+
content: string,
|
|
244
|
+
): AdapterEvent[] {
|
|
245
|
+
const splitter = createInlineThinkContentSplitter(models, modelId, budget);
|
|
246
|
+
try {
|
|
247
|
+
return [...splitter.feed(content), ...splitter.flush()];
|
|
248
|
+
} finally {
|
|
249
|
+
splitter.dispose();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -250,6 +250,7 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
|
|
|
250
250
|
});
|
|
251
251
|
return {
|
|
252
252
|
response,
|
|
253
|
+
abortSignal: requestAbortSignal,
|
|
253
254
|
inputTokens: retry.inputTokens,
|
|
254
255
|
contextInputEstimate: retry.contextInputEstimate,
|
|
255
256
|
nameMap: retry.nameMap,
|
|
@@ -16,9 +16,10 @@ import {
|
|
|
16
16
|
} from "../kiro-errors";
|
|
17
17
|
import { parseKiroEvent } from "../kiro-events";
|
|
18
18
|
import { noteKiroTransientThrottle } from "../kiro-retry";
|
|
19
|
-
import {
|
|
19
|
+
import { InlineThinkTagParser } from "../inline-think-tags";
|
|
20
20
|
import { isCompleteKiroToolInput, kiroTruncationErrorMessage } from "../kiro-truncation";
|
|
21
21
|
import { isValidKiroConversationId } from "../kiro-wire";
|
|
22
|
+
import { readDisplaySafeErrorPayloadText } from "../upstream-http-error";
|
|
22
23
|
import { tagKiroReasoningBlob } from "./reasoning";
|
|
23
24
|
import { estimateKiroTokens, kiroUpstreamContextWindow } from "./usage";
|
|
24
25
|
|
|
@@ -74,6 +75,7 @@ function createKiroAttemptRetention(budget: TranslatorBudget): KiroAttemptRetent
|
|
|
74
75
|
|
|
75
76
|
interface KiroFallbackAttempt {
|
|
76
77
|
response: Response;
|
|
78
|
+
abortSignal?: AbortSignal;
|
|
77
79
|
inputTokens: number;
|
|
78
80
|
contextInputEstimate: number;
|
|
79
81
|
nameMap: Map<string, string>;
|
|
@@ -319,7 +321,7 @@ async function* parseKiroAttemptEvents(
|
|
|
319
321
|
let authoritativeUsage: OcxUsage | undefined;
|
|
320
322
|
let stopReason: string | undefined;
|
|
321
323
|
const fallbackEvents: AdapterEvent[] = [];
|
|
322
|
-
const thinking = new
|
|
324
|
+
const thinking = new InlineThinkTagParser(budget);
|
|
323
325
|
|
|
324
326
|
const retainedEventBytes = (event: AdapterEvent): number => Buffer.byteLength(JSON.stringify(event));
|
|
325
327
|
const retainEvent = (event: AdapterEvent): void => {
|
|
@@ -1092,7 +1094,7 @@ export async function* parseKiroStream(
|
|
|
1092
1094
|
firstResult.releaseRetained();
|
|
1093
1095
|
fallback.releaseRequestBody?.();
|
|
1094
1096
|
if (!fallback.response.ok) {
|
|
1095
|
-
const payload = await fallback.response.
|
|
1097
|
+
const payload = await readDisplaySafeErrorPayloadText(fallback.response, fallback.abortSignal);
|
|
1096
1098
|
const failure = classifyKiroHttpError(fallback.response.status, fallback.response.headers, payload);
|
|
1097
1099
|
yield {
|
|
1098
1100
|
type: "error",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { estimateTokens } from "../../lib/token-estimate";
|
|
1
|
+
import { estimateTokens, estimateTokensFromCharacterCounts } from "../../lib/token-estimate";
|
|
2
2
|
import { KIRO_MODEL_CONTEXT_WINDOWS, normalizeKiroModelId } from "../../providers/kiro-models";
|
|
3
3
|
import { modelRecordValue } from "../../reasoning-effort";
|
|
4
4
|
import { sniffImageDimensions } from "../anthropic-image-guard";
|
|
@@ -98,8 +98,9 @@ export function estimateKiroWireTokens(text: string, modelId: string): number {
|
|
|
98
98
|
if (!text) return 0;
|
|
99
99
|
const cjk = kiroCjkCount(text);
|
|
100
100
|
if (cjk === 0) return Math.ceil(estimateKiroTokens(text, modelId) * KIRO_LATIN_WIRE_EXPANSION);
|
|
101
|
-
const
|
|
102
|
-
const
|
|
101
|
+
const prefixedModelId = `kiro/${modelId}`;
|
|
102
|
+
const latinTokens = estimateTokensFromCharacterCounts(text.length - cjk, 0, prefixedModelId);
|
|
103
|
+
const cjkTokens = estimateTokensFromCharacterCounts(0, cjk, prefixedModelId);
|
|
103
104
|
return Math.ceil(latinTokens * KIRO_LATIN_WIRE_EXPANSION + cjkTokens);
|
|
104
105
|
}
|
|
105
106
|
|
|
@@ -105,11 +105,10 @@ export function resetMimoJwtCache(): void {
|
|
|
105
105
|
inFlightJwt = null;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
async function fetchJwt(
|
|
109
|
-
// Bounded bootstrap:
|
|
110
|
-
//
|
|
108
|
+
async function fetchJwt(): Promise<string> {
|
|
109
|
+
// Bounded bootstrap: a stalled bootstrap can never hang past BOOTSTRAP_TIMEOUT_MS. It carries
|
|
110
|
+
// no caller signal because concurrent requests share it; each caller aborts its own wait.
|
|
111
111
|
const timeout = AbortSignal.timeout(BOOTSTRAP_TIMEOUT_MS);
|
|
112
|
-
const combined = signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
113
112
|
const response = await fetch(BOOTSTRAP_URL, {
|
|
114
113
|
method: "POST",
|
|
115
114
|
redirect: "manual",
|
|
@@ -118,7 +117,7 @@ async function fetchJwt(signal?: AbortSignal): Promise<string> {
|
|
|
118
117
|
"User-Agent": randomUserAgent(),
|
|
119
118
|
},
|
|
120
119
|
body: JSON.stringify({ client: getMimoClientId() }),
|
|
121
|
-
signal:
|
|
120
|
+
signal: timeout,
|
|
122
121
|
});
|
|
123
122
|
if (!response.ok) {
|
|
124
123
|
try { await response.body?.cancel(); } catch { /* already consumed */ }
|
|
@@ -159,25 +158,45 @@ async function fetchJwt(signal?: AbortSignal): Promise<string> {
|
|
|
159
158
|
return data.jwt;
|
|
160
159
|
}
|
|
161
160
|
|
|
161
|
+
function abortReason(signal: AbortSignal): unknown {
|
|
162
|
+
return signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Wait for the shared bootstrap, or stop waiting when this caller aborts; the bootstrap keeps running. */
|
|
166
|
+
function awaitForCaller(shared: Promise<string>, signal?: AbortSignal): Promise<string> {
|
|
167
|
+
if (!signal) return shared;
|
|
168
|
+
if (signal.aborted) return Promise.reject(abortReason(signal));
|
|
169
|
+
return new Promise<string>((resolve, reject) => {
|
|
170
|
+
const onAbort = () => reject(abortReason(signal));
|
|
171
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
172
|
+
shared.then(
|
|
173
|
+
jwt => { signal.removeEventListener("abort", onAbort); resolve(jwt); },
|
|
174
|
+
error => { signal.removeEventListener("abort", onAbort); reject(error); },
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
162
179
|
export async function getMimoJwt(signal?: AbortSignal): Promise<string> {
|
|
163
180
|
if (cachedJwt && Date.now() < jwtExpiresAt - JWT_EXPIRY_BUFFER_MS) {
|
|
164
181
|
return cachedJwt;
|
|
165
182
|
}
|
|
183
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
166
184
|
// Single-flight: concurrent callers await the same bootstrap instead of issuing
|
|
167
|
-
// parallel bootstraps.
|
|
185
|
+
// parallel bootstraps. One caller aborting must not fail the others, so the shared
|
|
186
|
+
// bootstrap is bound only to its timeout and each caller races it against its own signal.
|
|
168
187
|
if (!inFlightJwt) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
188
|
+
const shared = fetchJwt().then(jwt => {
|
|
189
|
+
cachedJwt = jwt;
|
|
190
|
+
jwtExpiresAt = parseJwtExp(jwt);
|
|
191
|
+
return jwt;
|
|
192
|
+
});
|
|
193
|
+
inFlightJwt = shared;
|
|
194
|
+
// Registered before any waiter, so the slot is cleared first; it also handles a failure that
|
|
195
|
+
// arrives after every waiter has left. A reset during the flight owns the slot and is kept.
|
|
196
|
+
const release = () => { if (inFlightJwt === shared) inFlightJwt = null; };
|
|
197
|
+
shared.then(release, release);
|
|
179
198
|
}
|
|
180
|
-
return inFlightJwt;
|
|
199
|
+
return awaitForCaller(inFlightJwt, signal);
|
|
181
200
|
}
|
|
182
201
|
|
|
183
202
|
/**
|
|
@@ -223,7 +242,7 @@ export function createMimoFreeAdapter(provider: OcxProviderConfig): ProviderAdap
|
|
|
223
242
|
name: "mimo-free",
|
|
224
243
|
|
|
225
244
|
async buildRequest(parsed: OcxParsedRequest, incoming: IncomingMeta): Promise<AdapterRequest> {
|
|
226
|
-
const jwt = await getMimoJwt();
|
|
245
|
+
const jwt = await getMimoJwt(incoming?.abortSignal);
|
|
227
246
|
|
|
228
247
|
// Let the base adapter build the wire body (handles reasoning, tools, etc.)
|
|
229
248
|
// but override the URL and headers after.
|
|
@@ -224,7 +224,7 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
|
|
|
224
224
|
let reasoningContent = thinkingParts.map(p => p.thinking).join("");
|
|
225
225
|
if (
|
|
226
226
|
reasoningContent.length === 0
|
|
227
|
-
&& toolCalls.length > 0
|
|
227
|
+
&& (toolCalls.length > 0 || thinkingParts.length > 0)
|
|
228
228
|
&& modelInList(provider.preserveReasoningContentModels, parsed.modelId)
|
|
229
229
|
) {
|
|
230
230
|
const cached = toolCalls
|
|
@@ -235,11 +235,11 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
|
|
|
235
235
|
if (cached.length > 0) {
|
|
236
236
|
reasoningContent = [...new Set(cached)].join("\n");
|
|
237
237
|
} else if (modelInList(provider.requiresReasoningPlaceholderModels ?? provider.preserveReasoningContentModels, parsed.modelId)) {
|
|
238
|
-
// Fallback (extends #950
|
|
238
|
+
// Fallback (extends #950 and #1193; fixes #5421): the replay cache is
|
|
239
239
|
// bounded (64 entries / 256 KiB / 1 h TTL) and always misses on
|
|
240
|
-
// long sessions, and some tool rounds carry no recorded
|
|
241
|
-
// at all. DeepSeek thinking mode rejects
|
|
242
|
-
//
|
|
240
|
+
// long sessions, and some thinking/tool rounds carry no recorded
|
|
241
|
+
// reasoning at all. DeepSeek thinking mode rejects replay without
|
|
242
|
+
// reasoning_content with HTTP 400, so inject a
|
|
243
243
|
// minimal placeholder rather than emit a bare continuation the
|
|
244
244
|
// upstream will reject. Scoped to requiresReasoningPlaceholderModels
|
|
245
245
|
// (defaulting to the preserve list): preserve-listed providers with
|