@bitkyc08/opencodex 2.88.0 → 2.90.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 +11 -1
- package/bin/ocx.mjs +15 -4
- package/gui/dist/assets/App-DgxkE6Md.js +58 -0
- package/gui/dist/assets/App-dnkJ85te.css +1 -0
- package/gui/dist/assets/{Tray-B5bC7AWL.js → Tray-DbTNc_88.js} +1 -1
- package/gui/dist/assets/{index-B5bsvxor.js → index-TmaHa54u.js} +15 -15
- package/gui/dist/assets/quota-summary-B16x5DZZ.js +1 -0
- package/gui/dist/index.html +1 -1
- package/package.json +2 -2
- package/src/adapters/anthropic/cache-retention.ts +117 -0
- package/src/adapters/anthropic/passthrough.ts +7 -4
- package/src/adapters/anthropic.ts +10 -3
- package/src/adapters/base.ts +4 -0
- package/src/adapters/claude-cli/adapter.ts +13 -7
- package/src/adapters/claude-cli/usage-admission.ts +14 -2
- package/src/adapters/coding-agent/protocol.ts +6 -0
- package/src/adapters/coding-agent/turn.ts +14 -4
- package/src/adapters/cursor/protobuf-request.ts +3 -0
- package/src/adapters/google-http.ts +3 -2
- package/src/adapters/google-progress-history.ts +19 -1
- package/src/adapters/google-vertex-endpoint.ts +67 -0
- package/src/adapters/google.ts +62 -37
- package/src/adapters/image.ts +5 -3
- package/src/adapters/input-media-guard.ts +5 -2
- package/src/adapters/kiro-refusal.ts +41 -1
- package/src/adapters/ollama-native.ts +1 -0
- package/src/adapters/openai-chat/messages.ts +34 -4
- package/src/adapters/openai-chat/passthrough.ts +37 -4
- package/src/adapters/openai-chat/reasoning-replay.ts +98 -13
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +95 -134
- package/src/adapters/openai-chat/serialized-tool-call-markup.ts +108 -0
- package/src/adapters/openai-chat/standalone-bare-tool-calls.ts +136 -0
- package/src/adapters/openai-chat.ts +7 -6
- package/src/adapters/openai-responses/compaction-search-history.ts +27 -0
- package/src/adapters/openai-responses/opengateway-input.ts +139 -0
- package/src/adapters/openai-responses/passthrough.ts +22 -13
- package/src/adapters/openai-responses/reasoning.ts +68 -1
- package/src/adapters/openai-responses/transport.ts +13 -1
- package/src/adapters/registry.ts +1 -1
- package/src/adapters/upstream-http-error.ts +3 -2
- package/src/adapters/zed.ts +7 -4
- package/src/bridge/internal.ts +11 -1
- package/src/bridge/response-json.ts +2 -2
- package/src/bridge/sse.ts +10 -4
- package/src/claude/desktop-3p-library.ts +43 -3
- package/src/claude/desktop-3p.ts +20 -10
- package/src/claude/desktop-picker-profile.ts +18 -9
- package/src/claude/desktop-remote-store-io.ts +2 -2
- package/src/claude/inbound.ts +12 -26
- package/src/claude/outbound.ts +5 -0
- package/src/cli/account-api.ts +13 -1
- package/src/cli/account-extended.ts +17 -5
- package/src/cli/account-key-quota.ts +4 -1
- package/src/cli/account.ts +1 -1
- package/src/cli/capabilities-agents-routing.ts +45 -8
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-provider-models.ts +5 -4
- package/src/cli/combo-result.ts +8 -2
- package/src/cli/combo.ts +17 -0
- package/src/cli/ensure-desktop-cold-start.ts +2 -2
- package/src/cli/help.ts +1 -1
- package/src/cli/message-command.ts +4 -1
- package/src/cli/message-remote-args.ts +69 -0
- package/src/cli/message-remote-command.ts +77 -0
- package/src/cli/provider-runtime.ts +69 -1
- package/src/cli/registry.ts +3 -3
- package/src/codex/account-lifecycle.ts +19 -6
- package/src/codex/account-pause.ts +6 -1
- package/src/codex/account-runtime-state.ts +5 -0
- package/src/codex/account-store.ts +55 -9
- package/src/codex/account-usability.ts +4 -1
- package/src/codex/auth-api/account-list.ts +13 -5
- package/src/codex/auth-api/account-pause-group.ts +2 -22
- package/src/codex/auth-api/login-collision.ts +59 -0
- package/src/codex/auth-api/login-flow.ts +12 -7
- package/src/codex/auth-api/main-account-probe.ts +3 -2
- package/src/codex/auth-api/pool-mode-gate.ts +81 -13
- package/src/codex/auth-collision.ts +31 -4
- package/src/codex/auth-context.ts +57 -10
- package/src/codex/catalog/aggregation.ts +15 -0
- package/src/codex/catalog/auto-review.ts +2 -1
- package/src/codex/catalog/build-entries.ts +17 -7
- package/src/codex/catalog/derive-entry.ts +3 -1
- package/src/codex/catalog/metadata.ts +2 -0
- package/src/codex/catalog/model-visibility.ts +0 -2
- package/src/codex/catalog/ownership.ts +71 -0
- package/src/codex/catalog/parsing.ts +12 -3
- package/src/codex/catalog/retained-sync.ts +2 -1
- package/src/codex/catalog/routed-removal.ts +4 -4
- package/src/codex/cli-update-plan.ts +18 -3
- package/src/codex/collision-identity.ts +78 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/low-quota-desktop-notifier.ts +125 -0
- package/src/codex/low-quota-protection.ts +57 -31
- package/src/codex/main-account-cache.ts +77 -3
- package/src/codex/main-account-hard-lock.ts +14 -0
- package/src/codex/main-account.ts +6 -2
- package/src/codex/native-residue.ts +2 -3
- package/src/codex/quota.ts +26 -2
- package/src/codex/routing/cooldown-math.ts +5 -0
- package/src/codex/routing/failure-window.ts +61 -9
- package/src/codex/routing.ts +4 -3
- package/src/codex/shim-diagnostics.ts +2 -1
- package/src/codex/shim-fingerprint.ts +11 -2
- package/src/codex/shim-templates.ts +16 -5
- package/src/codex/shim-windows-refresh.ts +224 -0
- package/src/codex/shim.ts +45 -21
- package/src/combos/failover.ts +10 -2
- package/src/combos/jev-model-backend.ts +30 -7
- package/src/combos/jev-model-request.ts +20 -0
- package/src/combos/jev-quota-activation.ts +153 -0
- package/src/combos/jev-quota-config.ts +62 -0
- package/src/combos/jev-quota-route.ts +26 -0
- package/src/combos/jev-quota.ts +88 -0
- package/src/combos/jev.ts +25 -12
- package/src/combos/types.ts +27 -0
- package/src/compatibility/index.ts +3 -0
- package/src/compatibility/openai-responses.ts +8 -1
- package/src/compatibility/opengateway-responses.ts +61 -0
- package/src/config/account-auto-pause-revisions.ts +136 -0
- package/src/config/diagnostics.ts +13 -0
- package/src/config/live-reconcile.ts +9 -0
- package/src/config/persist-unlocked.ts +10 -1
- package/src/config/provider-validation.ts +7 -2
- package/src/config/removed-opencode-free.ts +135 -0
- package/src/config/schema/account-auto-pause.ts +14 -0
- package/src/config/schema/config-schema.ts +15 -0
- package/src/config/schema/leaf-validators.ts +7 -0
- package/src/config.ts +10 -3
- package/src/generated/compatibility-version.json +476 -216
- package/src/generated/model-metadata.ts +1 -1
- package/src/images/loop.ts +34 -11
- package/src/lib/account-auto-pause-response.ts +16 -0
- package/src/lib/bun-runtime.ts +18 -1
- package/src/lib/bun-transpiler-cache-scope.d.mts +8 -0
- package/src/lib/bun-transpiler-cache-scope.mjs +82 -0
- package/src/lib/decision-quota-sink.ts +76 -0
- package/src/lib/provider-admission-wait.ts +14 -0
- package/src/lib/service-secrets.ts +37 -2
- package/src/lib/state-store-registrations.ts +25 -0
- package/src/lib/translator-budget.ts +1 -1
- package/src/lib/upstream-retry.ts +15 -0
- package/src/link/ssh-argv.ts +3 -0
- package/src/messaging/envelope.ts +6 -3
- package/src/messaging/remote-auth.ts +83 -0
- package/src/messaging/remote-bridge.ts +270 -0
- package/src/messaging/remote-contract.ts +108 -0
- package/src/messaging/remote-enrollment-recovery.ts +59 -0
- package/src/messaging/remote-enrollment.ts +244 -0
- package/src/messaging/remote-envelope.ts +40 -0
- package/src/messaging/remote-files.ts +117 -0
- package/src/messaging/remote-owner.ts +169 -0
- package/src/messaging/remote-ports.ts +56 -0
- package/src/messaging/remote-process.ts +131 -0
- package/src/messaging/remote-replies.ts +98 -0
- package/src/messaging/remote-rpc-admission.ts +32 -0
- package/src/messaging/remote-send.ts +72 -0
- package/src/messaging/remote-store.ts +111 -0
- package/src/messaging/remote-tunnels.ts +65 -0
- package/src/messaging/rpc.ts +13 -1
- package/src/oauth/account-auto-pause-policy.ts +66 -0
- package/src/oauth/account-auto-pause.ts +171 -0
- package/src/oauth/account-import/google-antigravity-adapter.ts +38 -2
- package/src/oauth/account-import/parser.ts +15 -2
- package/src/oauth/account-import/types.ts +7 -0
- package/src/oauth/account-quota-rank.ts +2 -1
- package/src/oauth/anthropic-account-refusal.ts +6 -0
- package/src/oauth/anthropic-model-family.ts +8 -0
- package/src/oauth/anthropic-model-quota.ts +2 -6
- package/src/oauth/api-key-pool-preserve.ts +37 -0
- package/src/oauth/chatgpt-shared.ts +10 -1
- package/src/oauth/dsh.ts +45 -2
- package/src/oauth/index.ts +5 -35
- package/src/oauth/kiro.ts +44 -1
- package/src/oauth/login-flow-state.ts +11 -2
- package/src/oauth/store.ts +35 -8
- package/src/oauth/types.ts +2 -0
- package/src/plugins/upstream-hooks.ts +14 -1
- package/src/providers/api-key-pause.ts +32 -0
- package/src/providers/api-key-resolve.ts +10 -4
- package/src/providers/api-key-selection.ts +6 -1
- package/src/providers/api-keys.ts +8 -4
- package/src/providers/claude-cli-usage.ts +25 -5
- package/src/providers/initial-model-selection.ts +2 -2
- package/src/providers/key-failover.ts +21 -13
- package/src/providers/key-store.ts +8 -3
- package/src/providers/mistral-transport.ts +207 -0
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/openai-sidecar.ts +5 -0
- package/src/providers/opencode-zen-rate-limit.ts +6 -72
- package/src/providers/quota/account-cache.ts +15 -1
- package/src/providers/quota/anthropic-account-quota.ts +20 -7
- package/src/providers/quota/anthropic-cooldown-recovery.ts +22 -4
- package/src/providers/quota/anthropic-decision-windows.ts +47 -0
- package/src/providers/quota/antigravity.ts +45 -8
- package/src/providers/quota/failure-codes.ts +18 -0
- package/src/providers/quota/report-cache.ts +14 -0
- package/src/providers/quota/vendor-probes-oauth.ts +15 -6
- package/src/providers/quota-decision-publication.ts +77 -0
- package/src/providers/quota-decision-snapshot.ts +145 -0
- package/src/providers/quota-routing-cache.ts +4 -1
- package/src/providers/quota-types.ts +6 -1
- package/src/providers/quota.ts +8 -3
- package/src/providers/registry/entries-extended.ts +14 -53
- package/src/providers/registry/model-seeds.ts +1 -1
- package/src/providers/registry.ts +1 -2
- package/src/providers/routed-config.ts +1 -1
- package/src/providers/stale-vision-classification-migration.ts +1 -1
- package/src/providers/xai-transport.ts +2 -1
- package/src/responses/audio.ts +46 -0
- package/src/responses/input-media.ts +37 -15
- package/src/responses/parser-content.ts +22 -27
- package/src/responses/parser.ts +18 -10
- package/src/responses/reasoning-retention.ts +218 -11
- package/src/responses/schema.ts +3 -4
- package/src/responses/web-search-replay.ts +147 -0
- package/src/routing/compatibility/behavior.ts +4 -1
- package/src/server/auth-cors.ts +7 -0
- package/src/server/background-lifecycle.ts +2 -1
- package/src/server/chat-native.ts +36 -20
- package/src/server/claude-messages.ts +2 -1
- package/src/server/index/optional-listeners.ts +6 -0
- package/src/server/index/serve-options.ts +8 -2
- package/src/server/index/websocket-handler.ts +10 -0
- package/src/server/lifecycle.ts +17 -1
- package/src/server/management/combo-routes.ts +17 -2
- package/src/server/management/oauth-account-routes.ts +12 -2
- package/src/server/management/provider-completion-test.ts +289 -0
- package/src/server/management/provider-routes.ts +9 -1
- package/src/server/management/route-registry.ts +2 -0
- package/src/server/messages-native.ts +29 -4
- package/src/server/messages-shared.ts +4 -1
- package/src/server/request-log.ts +23 -9
- package/src/server/responses/account-auto-pause-recovery.ts +26 -0
- package/src/server/responses/adapter-continuation.ts +2 -2
- package/src/server/responses/adapter-dispatch.ts +79 -30
- package/src/server/responses/codex-ws-exchange.ts +3 -0
- package/src/server/responses/codex-ws-metadata.ts +44 -0
- package/src/server/responses/codex-ws-session.ts +21 -1
- package/src/server/responses/combo-session-recall.ts +25 -0
- package/src/server/responses/combo-stream-preflight.ts +10 -3
- package/src/server/responses/combo-tool-loop-pin.ts +70 -0
- package/src/server/responses/compact.ts +10 -5
- package/src/server/responses/context-overflow.ts +141 -2
- package/src/server/responses/core-auth.ts +20 -0
- package/src/server/responses/core-codex-account.ts +28 -15
- package/src/server/responses/core-combo.ts +34 -5
- package/src/server/responses/core-opaque-recovery.ts +20 -11
- package/src/server/responses/core.ts +12 -9
- package/src/server/responses/deepseek-image-files.ts +121 -0
- package/src/server/responses/empty-completion-guard.ts +10 -0
- package/src/server/responses/fetch-helpers.ts +65 -14
- package/src/server/responses/input-admission.ts +6 -2
- package/src/server/responses/jev-model-invoke.ts +13 -14
- package/src/server/responses/main-hard-lock-routing.ts +33 -8
- package/src/server/responses/native-steering-log.ts +2 -2
- package/src/server/responses/opengateway-completion-guard.ts +232 -0
- package/src/server/responses/passthrough-completion.ts +41 -0
- package/src/server/responses/passthrough-delivery.ts +5 -2
- package/src/server/responses/passthrough-dispatch.ts +42 -45
- package/src/server/responses/passthrough-key-rotation.ts +40 -0
- package/src/server/responses/passthrough-observations.ts +38 -0
- package/src/server/responses/reasoning-archive-error.ts +21 -0
- package/src/server/responses/request-compression.ts +98 -0
- package/src/server/responses/request-prepare.ts +11 -1
- package/src/server/responses/request-transport.ts +92 -5
- package/src/server/responses/run-turn-execution.ts +2 -0
- package/src/server/responses/sidecar-execution.ts +16 -3
- package/src/server/responses/terminal-guard.ts +2 -0
- package/src/server/startup-action-control.ts +2 -2
- package/src/server/startup-health-cache.ts +2 -2
- package/src/server/windows-tray-control.ts +2 -2
- package/src/service/definition.ts +9 -1
- package/src/service/orchestration.ts +17 -0
- package/src/types/account-auto-pause.ts +15 -0
- package/src/types/config.ts +23 -1
- package/src/types/provider.ts +11 -5
- package/src/types/request.ts +36 -3
- package/src/types.ts +2 -0
- package/src/usage/cost.ts +17 -1
- package/src/usage/expected-prices.ts +3 -1
- package/src/usage/jev-stats.ts +4 -0
- package/src/usage/log.ts +2 -0
- package/src/usage/summary.ts +7 -0
- package/src/web-search/loop.ts +39 -13
- package/src/web-search/progress-stream.ts +4 -1
- package/src/web-search/run-turn-loop.ts +9 -3
- package/src/web-search/sidecar-search-replay.ts +96 -0
- package/gui/dist/assets/App-CHC89EXQ.js +0 -58
- package/gui/dist/assets/App-wT3mXqFz.css +0 -1
- package/gui/dist/assets/quota-summary-D8y2wTsB.js +0 -1
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import type { TranslatorBudget } from "../../lib/translator-budget";
|
|
2
2
|
import type { AdapterEvent } from "../../types";
|
|
3
3
|
import { repairFreeformToolInput } from "../../responses/apply-patch-envelope";
|
|
4
|
+
import { contextAfter, deferredContinuationBreak, splitAtPossibleSerializedToolCall, MAX_TAG_LENGTH, TAG_SUFFIX, type TextContext } from "./serialized-tool-call-markup";
|
|
5
|
+
export { splitAtPossibleSerializedToolCall } from "./serialized-tool-call-markup";
|
|
4
6
|
|
|
5
|
-
const OPEN_TAG = "<tool_call>";
|
|
6
|
-
const CLOSE_TAG = "</tool_call>";
|
|
7
|
-
const FUNCTION_TAG = "<function=";
|
|
8
7
|
/**
|
|
9
8
|
* Streaming bounds (ingestStreaming only; buffered reconciliation already knows its structured calls).
|
|
10
9
|
* MAX_HELD_BYTES is a runaway guard on held text plus queued events, far below the translator budget,
|
|
@@ -14,12 +13,7 @@ const FUNCTION_TAG = "<function=";
|
|
|
14
13
|
*/
|
|
15
14
|
const MAX_HELD_BYTES = 4 * 1024 * 1024;
|
|
16
15
|
const MAX_TRAILING_CHARS = 8 * 1024;
|
|
17
|
-
|
|
18
|
-
interface TextContext {
|
|
19
|
-
fence: string | null;
|
|
20
|
-
lineStart: boolean;
|
|
21
|
-
inlineTicks?: number;
|
|
22
|
-
}
|
|
16
|
+
export const MAX_STANDALONE_CANDIDATES = 128;
|
|
23
17
|
|
|
24
18
|
export interface SerializedToolCall {
|
|
25
19
|
name: string;
|
|
@@ -49,9 +43,9 @@ export interface StructuredToolCallReference {
|
|
|
49
43
|
recoveredParameterWrapper?: boolean;
|
|
50
44
|
}
|
|
51
45
|
|
|
52
|
-
const BLOCK_HEADER =
|
|
46
|
+
const BLOCK_HEADER = new RegExp(`<tool_call(${TAG_SUFFIX})>\\s*<function=([^>\\r\\n]+)>`, "y");
|
|
53
47
|
/** A separate bare block starts a line (see `splitAtPossibleSerializedToolCall`); a header mid-line is body text. */
|
|
54
|
-
const NEXT_BLOCK_HEADER =
|
|
48
|
+
const NEXT_BLOCK_HEADER = new RegExp(`\\n<tool_call${TAG_SUFFIX}>\\s*<function=[^>\\r\\n]+>`, "g");
|
|
55
49
|
const FUNCTION_CLOSE = "</function>";
|
|
56
50
|
const PARAMETER_CLOSE = "</parameter>";
|
|
57
51
|
|
|
@@ -73,26 +67,25 @@ function endsWithAt(text: string, from: number, to: number, suffix: string): boo
|
|
|
73
67
|
* bounds an unclosed candidate: with no close at all before it, it is body text, and a closed
|
|
74
68
|
* `</function></tool_call>` after it still ends the block.
|
|
75
69
|
*/
|
|
76
|
-
function blockAt(text: string, offset: number): SerializedToolCall | undefined {
|
|
70
|
+
function blockAt(text: string, offset: number, nextHeaderAt: (from: number) => number): SerializedToolCall | undefined {
|
|
77
71
|
BLOCK_HEADER.lastIndex = offset;
|
|
78
72
|
const header = BLOCK_HEADER.exec(text);
|
|
79
73
|
if (!header) return undefined;
|
|
74
|
+
const closeTag = `</tool_call${header[1]}>`;
|
|
80
75
|
const bodyStart = offset + header[0].length;
|
|
81
|
-
|
|
82
|
-
const next = NEXT_BLOCK_HEADER.exec(text);
|
|
83
|
-
const limit = next ? next.index + 1 : text.length;
|
|
76
|
+
const limit = nextHeaderAt(bodyStart);
|
|
84
77
|
let unclosed: SerializedToolCall | undefined;
|
|
85
|
-
for (let close = text.indexOf(
|
|
86
|
-
close = text.indexOf(
|
|
78
|
+
for (let close = text.indexOf(closeTag, bodyStart); close >= 0 && (close < limit || !unclosed);
|
|
79
|
+
close = text.indexOf(closeTag, close + closeTag.length)) {
|
|
87
80
|
let bodyEnd = trimmedEnd(text, bodyStart, close);
|
|
88
81
|
const closed = endsWithAt(text, bodyStart, bodyEnd, FUNCTION_CLOSE);
|
|
89
82
|
if (closed) bodyEnd = trimmedEnd(text, bodyStart, bodyEnd - FUNCTION_CLOSE.length);
|
|
90
83
|
if (endsWithAt(text, bodyStart, bodyEnd, PARAMETER_CLOSE)) bodyEnd -= PARAMETER_CLOSE.length;
|
|
91
84
|
const call = {
|
|
92
|
-
name: header[
|
|
85
|
+
name: header[2]!.trim(),
|
|
93
86
|
body: text.slice(bodyStart, bodyEnd),
|
|
94
87
|
start: offset,
|
|
95
|
-
end: close +
|
|
88
|
+
end: close + closeTag.length,
|
|
96
89
|
};
|
|
97
90
|
if (closed) return call;
|
|
98
91
|
if (close < limit) unclosed ??= call;
|
|
@@ -100,21 +93,64 @@ function blockAt(text: string, offset: number): SerializedToolCall | undefined {
|
|
|
100
93
|
return unclosed;
|
|
101
94
|
}
|
|
102
95
|
|
|
103
|
-
/** Finds
|
|
104
|
-
function
|
|
96
|
+
/** Finds outer blocks outside literal Markdown, reporting an unresolved enclosing header. */
|
|
97
|
+
export function scanSerializedToolCalls(
|
|
98
|
+
text: string, context: TextContext = { fence: null, lineStart: true }, allowMidLineBare = false,
|
|
99
|
+
maxCandidates = allowMidLineBare ? MAX_STANDALONE_CANDIDATES : Infinity,
|
|
100
|
+
): { calls: SerializedToolCall[]; incomplete: boolean; overflow: boolean } {
|
|
105
101
|
const calls: SerializedToolCall[] = [];
|
|
102
|
+
// Adjacent same-line blocks share one forward-only newline-header search, including EOF.
|
|
103
|
+
const headers = new RegExp(NEXT_BLOCK_HEADER.source, "g");
|
|
104
|
+
let next = headers.exec(text);
|
|
105
|
+
const nextHeaderAt = (from: number): number => {
|
|
106
|
+
while (next && next.index + 1 < from) next = headers.exec(text);
|
|
107
|
+
return next ? next.index + 1 : text.length;
|
|
108
|
+
};
|
|
106
109
|
let offset = 0;
|
|
107
110
|
while (offset < text.length) {
|
|
108
|
-
const split = splitAtPossibleSerializedToolCall(text.slice(offset), context, true);
|
|
111
|
+
const split = splitAtPossibleSerializedToolCall(text.slice(offset), context, true, allowMidLineBare);
|
|
109
112
|
offset += split.emit.length;
|
|
110
113
|
if (!split.hasOpenTag) break;
|
|
111
|
-
|
|
112
|
-
|
|
114
|
+
if (calls.length >= maxCandidates) return { calls, incomplete: false, overflow: true };
|
|
115
|
+
const match = blockAt(text, offset, nextHeaderAt);
|
|
116
|
+
if (!match) return { calls, incomplete: true, overflow: false }; // An unresolved outer block cannot authorize an inner call.
|
|
113
117
|
calls.push(match);
|
|
114
118
|
offset = match.end;
|
|
115
119
|
context = { fence: null, lineStart: false };
|
|
116
120
|
}
|
|
117
|
-
return calls;
|
|
121
|
+
return { calls, incomplete: false, overflow: false };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function callsIn(text: string, context?: TextContext, allowMidLineBare = false): SerializedToolCall[] {
|
|
125
|
+
return scanSerializedToolCalls(text, context, allowMidLineBare).calls;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Standalone recovery is separate from identified-wrapper/empty-structured-input reconciliation. */
|
|
129
|
+
export function hasNestedSerializedToolCallHeader(body: string): boolean {
|
|
130
|
+
return /<(?:tool_call(?:_[a-fA-F0-9]{1,64})?|function=[^>\r\n]+)>/.test(body);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function recoverStandaloneBareBlocks(
|
|
134
|
+
text: string,
|
|
135
|
+
freeformTools: ReadonlyMap<string, FreeformToolIdentity>,
|
|
136
|
+
context?: TextContext,
|
|
137
|
+
): { name: string; input: string; start: number; end: number }[] {
|
|
138
|
+
if (freeformTools.size === 0) return [];
|
|
139
|
+
const scan = scanSerializedToolCalls(text, context, true);
|
|
140
|
+
if (scan.overflow) return [];
|
|
141
|
+
const blocks = scan.calls;
|
|
142
|
+
// A nested header makes delimiter ownership ambiguous; no later apparent sibling is safe.
|
|
143
|
+
if (blocks.some(call => hasNestedSerializedToolCallHeader(call.body))) return [];
|
|
144
|
+
return blocks.flatMap(call => {
|
|
145
|
+
if (!text.startsWith("<tool_call>", call.start) || !freeformTools.has(call.name)) return [];
|
|
146
|
+
const wrapped = freeformBody(call.body);
|
|
147
|
+
const recoveredParameterWrapper = wrapped.startsWith("<parameter=");
|
|
148
|
+
const input = recoveredParameterWrapper ? freeformBody(wrapped.slice("<parameter=".length)) : wrapped;
|
|
149
|
+
if (!input.trim()
|
|
150
|
+
|| (recoveredParameterWrapper && /^<parameter=[A-Za-z_$][\w$.-]*>/.test(wrapped))
|
|
151
|
+
|| input.includes("<parameter=") || input.includes("</parameter>")) return [];
|
|
152
|
+
return [{ name: call.name, input, start: call.start, end: call.end }];
|
|
153
|
+
});
|
|
118
154
|
}
|
|
119
155
|
|
|
120
156
|
/**
|
|
@@ -128,94 +164,6 @@ function repeatedCallIn(text: string, context?: TextContext): SerializedToolCall
|
|
|
128
164
|
return { ...first, end: text.length };
|
|
129
165
|
}
|
|
130
166
|
|
|
131
|
-
/** Splits safe visible text from a possible control block while carrying Markdown context across chunks. */
|
|
132
|
-
export function splitAtPossibleSerializedToolCall(
|
|
133
|
-
text: string,
|
|
134
|
-
initialContext: TextContext = { fence: null, lineStart: true },
|
|
135
|
-
final = false,
|
|
136
|
-
): {
|
|
137
|
-
emit: string;
|
|
138
|
-
defer: string;
|
|
139
|
-
hasOpenTag: boolean;
|
|
140
|
-
context: TextContext;
|
|
141
|
-
} {
|
|
142
|
-
const context = { ...initialContext };
|
|
143
|
-
const split = (at: number, hasOpenTag = false) => ({
|
|
144
|
-
emit: text.slice(0, at), defer: text.slice(at), hasOpenTag, context,
|
|
145
|
-
});
|
|
146
|
-
for (let index = 0; index < text.length; index++) {
|
|
147
|
-
if (context.lineStart) {
|
|
148
|
-
const rest = text.slice(index);
|
|
149
|
-
const fence = /^ {0,3}(`{3,}|~{3,})([^\n]*)/.exec(rest);
|
|
150
|
-
if (!context.inlineTicks && fence && (!context.fence || (fence[1]![0] === context.fence[0]
|
|
151
|
-
&& fence[1]!.length >= context.fence.length && /^[ \t\r]*$/.test(fence[2]!)))) {
|
|
152
|
-
if (!final && !rest.includes("\n")) return split(index);
|
|
153
|
-
context.fence = context.fence ? null : fence[1]!;
|
|
154
|
-
index += fence[0].length - 1;
|
|
155
|
-
context.lineStart = false;
|
|
156
|
-
continue;
|
|
157
|
-
}
|
|
158
|
-
if (!final && /^ {0,3}(`*|~*)$/.test(rest)) return split(index);
|
|
159
|
-
// Only bare control markup qualifies. Prose, quotes, indented examples and
|
|
160
|
-
// fenced code stay user-visible even when their body matches a real call.
|
|
161
|
-
if (!context.fence && !context.inlineTicks) {
|
|
162
|
-
if (rest.startsWith(OPEN_TAG)) {
|
|
163
|
-
const header = rest.slice(OPEN_TAG.length).trimStart();
|
|
164
|
-
if (/^<function=[^>\r\n]+>/.test(header)) return split(index, true);
|
|
165
|
-
if (!final && (FUNCTION_TAG.startsWith(header)
|
|
166
|
-
|| (header.startsWith(FUNCTION_TAG) && !/[>\r\n]/.test(header.slice(FUNCTION_TAG.length))))) {
|
|
167
|
-
return split(index);
|
|
168
|
-
}
|
|
169
|
-
} else if (!final && OPEN_TAG.startsWith(rest)) return split(index);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
if (!context.fence && text[index] === "`") {
|
|
173
|
-
let end = index + 1;
|
|
174
|
-
while (text[end] === "`") end++;
|
|
175
|
-
if (!final && end === text.length) return split(index);
|
|
176
|
-
const ticks = end - index;
|
|
177
|
-
if (!context.inlineTicks) context.inlineTicks = ticks;
|
|
178
|
-
else if (context.inlineTicks === ticks) context.inlineTicks = undefined;
|
|
179
|
-
index = end - 1;
|
|
180
|
-
}
|
|
181
|
-
context.lineStart = text[index] === "\n";
|
|
182
|
-
}
|
|
183
|
-
return split(text.length);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* The line, fence and inline-code state after `text`. Serialized blocks are neutralised first so
|
|
188
|
-
* the scan runs through the whole text instead of stopping at the first opening tag; a block ends
|
|
189
|
-
* mid-line, which is exactly what the neutral spelling reports too.
|
|
190
|
-
*/
|
|
191
|
-
function contextAfter(text: string, context: TextContext): TextContext {
|
|
192
|
-
if (text.length === 0) return context;
|
|
193
|
-
return splitAtPossibleSerializedToolCall(text.replaceAll(OPEN_TAG, "<tool-call>"), context, true).context;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* A deferred header/fence can itself be long. While its unresolved portion only grows, a
|
|
198
|
-
* chunk-local check suffices; the first resolving character goes back through the full splitter.
|
|
199
|
-
* Each unbounded phase is scanned once on entry and once on exit, preserving its Markdown rules.
|
|
200
|
-
*/
|
|
201
|
-
function deferredContinuationBreak(text: string, context: TextContext): RegExp | undefined {
|
|
202
|
-
if (text.startsWith(OPEN_TAG)) {
|
|
203
|
-
if (/^<tool_call>\s*$/.test(text)) return /\S/;
|
|
204
|
-
if (/^<tool_call>\s*<function=[^>\r\n]*$/.test(text)) return /[>\r\n]/;
|
|
205
|
-
}
|
|
206
|
-
if (!context.inlineTicks && context.lineStart) {
|
|
207
|
-
const fence = /^ {0,3}(`{3,}|~{3,})([^\n]*)$/.exec(text);
|
|
208
|
-
if (fence) {
|
|
209
|
-
if (!context.fence) return /\n/;
|
|
210
|
-
// Closing-fence ticks may grow only before its optional whitespace suffix.
|
|
211
|
-
if (!fence[2]) return fence[1]![0] === "`" ? /[^`]/ : /[^~]/;
|
|
212
|
-
return /[^ \t\r]/;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
if (/^`+$/.test(text)) return /[^`]/;
|
|
216
|
-
return undefined;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
167
|
/**
|
|
220
168
|
* Holds possible duplicate text within the shared translator budget until the dispatch outcome is
|
|
221
169
|
* known. While a block candidate is open, any other event (reasoning) is queued at its position in
|
|
@@ -232,10 +180,13 @@ export class SerializedToolCallContentBuffer {
|
|
|
232
180
|
private sawCloser = false;
|
|
233
181
|
private openAfterCloser = false;
|
|
234
182
|
private trailingProseChars = 0;
|
|
235
|
-
|
|
183
|
+
protected context: TextContext = { fence: null, lineStart: true };
|
|
236
184
|
private queued: { offset: number; event: AdapterEvent }[] = [];
|
|
237
185
|
|
|
238
|
-
constructor(private readonly budget: TranslatorBudget) {}
|
|
186
|
+
constructor(private readonly budget: TranslatorBudget, private readonly allowMidLineBare = false) {}
|
|
187
|
+
|
|
188
|
+
/** Optional recovery retains bounded candidates before prose is released, never the prose itself. */
|
|
189
|
+
protected releaseBounded(): AdapterEvent[] { return this.drain([]); }
|
|
239
190
|
|
|
240
191
|
/** Remembers visible text released before a later structured call can be reconciled. */
|
|
241
192
|
private observeText(events: AdapterEvent[]): AdapterEvent[] {
|
|
@@ -291,19 +242,22 @@ export class SerializedToolCallContentBuffer {
|
|
|
291
242
|
/** Scan new text plus a fixed overlap, never the retained body (which may be a large rope). */
|
|
292
243
|
private observeDelimiters(delta: string): void {
|
|
293
244
|
const scan = this.delimiterSuffix + delta;
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
this.
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
245
|
+
const tags = scan.matchAll(new RegExp(`<(/?)tool_call${TAG_SUFFIX}>`, "g"));
|
|
246
|
+
let afterCloser = -1;
|
|
247
|
+
for (const tag of tags) {
|
|
248
|
+
if (tag.index! + tag[0].length <= this.delimiterSuffix.length) continue;
|
|
249
|
+
if (tag[1]) {
|
|
250
|
+
this.sawCloser = true;
|
|
251
|
+
this.openAfterCloser = false;
|
|
252
|
+
this.trailingProseChars = 0;
|
|
253
|
+
afterCloser = tag.index! + tag[0].length;
|
|
254
|
+
} else {
|
|
255
|
+
this.openAfterCloser = true;
|
|
256
|
+
}
|
|
303
257
|
}
|
|
304
|
-
const tail =
|
|
258
|
+
const tail = afterCloser >= 0 ? scan.slice(afterCloser) : delta;
|
|
305
259
|
if (this.sawCloser) this.trailingProseChars += tail.replace(/\s+/g, "").length;
|
|
306
|
-
this.delimiterSuffix = scan.slice(-(
|
|
260
|
+
this.delimiterSuffix = scan.slice(-(MAX_TAG_LENGTH - 1));
|
|
307
261
|
}
|
|
308
262
|
|
|
309
263
|
/** Returns immediately safe text and retains only the suffix that still needs reconciliation. */
|
|
@@ -312,7 +266,7 @@ export class SerializedToolCallContentBuffer {
|
|
|
312
266
|
this.append(delta);
|
|
313
267
|
return "";
|
|
314
268
|
}
|
|
315
|
-
const split = splitAtPossibleSerializedToolCall(this.text + delta, this.context);
|
|
269
|
+
const split = splitAtPossibleSerializedToolCall(this.text + delta, this.context, false, this.allowMidLineBare);
|
|
316
270
|
this.replace(split.defer, split.hasOpenTag);
|
|
317
271
|
this.context = split.context;
|
|
318
272
|
if (!split.hasOpenTag) this.continuationBreak = deferredContinuationBreak(split.defer, split.context);
|
|
@@ -328,7 +282,7 @@ export class SerializedToolCallContentBuffer {
|
|
|
328
282
|
ingestStreaming(delta: string): AdapterEvent[] {
|
|
329
283
|
const deltaBytes = this.appendedByteLength(delta);
|
|
330
284
|
if (this.hasOpenTag && this.bytes + deltaBytes > MAX_HELD_BYTES) {
|
|
331
|
-
const released = this.
|
|
285
|
+
const released = this.releaseBounded();
|
|
332
286
|
// A delta that alone passes the bound is delivered as text rather than retained.
|
|
333
287
|
if (Buffer.byteLength(delta) > MAX_HELD_BYTES) {
|
|
334
288
|
this.context = contextAfter(delta, this.context);
|
|
@@ -339,7 +293,7 @@ export class SerializedToolCallContentBuffer {
|
|
|
339
293
|
const text = this.ingest(delta);
|
|
340
294
|
// Checked after ingest too: one delta can open a block and already carry more than a bound.
|
|
341
295
|
if (this.hasOpenTag && (this.bytes > MAX_HELD_BYTES || (this.sawCloser && !this.openAfterCloser && this.trailingProseChars > MAX_TRAILING_CHARS))) {
|
|
342
|
-
return this.observeText([...textEvents(text), ...this.
|
|
296
|
+
return this.observeText([...textEvents(text), ...this.releaseBounded()]);
|
|
343
297
|
}
|
|
344
298
|
return this.observeText(textEvents(text));
|
|
345
299
|
}
|
|
@@ -358,7 +312,7 @@ export class SerializedToolCallContentBuffer {
|
|
|
358
312
|
if (!this.hasOpenTag) return [...this.drain([]), event];
|
|
359
313
|
const eventBytes = Buffer.byteLength(JSON.stringify(event));
|
|
360
314
|
// Queued events count toward the same runaway bound as held text.
|
|
361
|
-
if (this.bytes + eventBytes > MAX_HELD_BYTES) return [...this.
|
|
315
|
+
if (this.bytes + eventBytes > MAX_HELD_BYTES) return [...this.releaseBounded(), event];
|
|
362
316
|
this.budget.reserveTransient(eventBytes, { kind: "live_transient" }).commitRetained();
|
|
363
317
|
this.bytes += eventBytes;
|
|
364
318
|
this.queued.push({ offset: this.text.length, event });
|
|
@@ -371,7 +325,11 @@ export class SerializedToolCallContentBuffer {
|
|
|
371
325
|
* duplicate a dispatched call; pass an empty list on failure to preserve everything.
|
|
372
326
|
*/
|
|
373
327
|
drain(structuredCalls: readonly StructuredToolCallReference[]): AdapterEvent[] {
|
|
374
|
-
|
|
328
|
+
return this.drainRanges(duplicatedSerializedToolCallRanges(this.text, structuredCalls, this.context));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Standalone recovery supplies only its own accepted bare ranges, never identified wrappers. */
|
|
332
|
+
protected drainRanges(removed: readonly { start: number; end: number }[]): AdapterEvent[] {
|
|
375
333
|
const kept = (from: number, to: number): string => {
|
|
376
334
|
let piece = "";
|
|
377
335
|
let cursor = from;
|
|
@@ -390,8 +348,11 @@ export class SerializedToolCallContentBuffer {
|
|
|
390
348
|
if (boundary.event) out.push(boundary.event);
|
|
391
349
|
cursor = boundary.offset;
|
|
392
350
|
}
|
|
393
|
-
//
|
|
394
|
-
|
|
351
|
+
// Control bodies are opaque on every provider, regardless of standalone recovery eligibility.
|
|
352
|
+
// Context still traverses the whole released text when recovery hit its candidate limit.
|
|
353
|
+
const last = scanSerializedToolCalls(this.text, this.context, this.allowMidLineBare, Infinity).calls.at(-1);
|
|
354
|
+
this.context = last ? contextAfter(this.text.slice(last.end), { fence: null, lineStart: false })
|
|
355
|
+
: contextAfter(this.text, this.context);
|
|
395
356
|
this.queued = [];
|
|
396
357
|
this.replace("", false);
|
|
397
358
|
return this.observeText(out);
|
|
@@ -609,7 +570,7 @@ export function reconcileStructuredToolCalls(
|
|
|
609
570
|
const body = hasParameterOpener
|
|
610
571
|
? freeformBody(wrappedBody.slice("<parameter=".length)) : wrappedBody;
|
|
611
572
|
if (block.name === calls[0]!.wireName && body.trim() !== ""
|
|
612
|
-
&&
|
|
573
|
+
&& !new RegExp(`(?:^|\\n)<tool_call${TAG_SUFFIX}>\\s*<function=[^>\\r\\n]+>`).test(block.body)
|
|
613
574
|
&& !(hasParameterOpener && /^<parameter=[A-Za-z_$][\w$.-]*>/.test(wrappedBody))
|
|
614
575
|
&& !body.includes("<parameter=") && !body.includes("</parameter>")) {
|
|
615
576
|
references[0] = { ...references[0]!, argumentsText: JSON.stringify({ input: body }),
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
const OPEN_TAG = "<tool_call>";
|
|
2
|
+
const FUNCTION_TAG = "<function=";
|
|
3
|
+
// MiMo gateways append an opaque hexadecimal identity to both delimiters.
|
|
4
|
+
export const TAG_SUFFIX = "(?:_[a-fA-F0-9]{1,64})?";
|
|
5
|
+
export const MAX_TAG_LENGTH = "</tool_call_".length + 64 + 1;
|
|
6
|
+
const OPENING = new RegExp(`^<tool_call${TAG_SUFFIX}>`);
|
|
7
|
+
const OPEN_PREFIX = /^<tool_call(?:_[a-fA-F0-9]{0,64})?$/;
|
|
8
|
+
|
|
9
|
+
export interface TextContext {
|
|
10
|
+
fence: string | null;
|
|
11
|
+
lineStart: boolean;
|
|
12
|
+
inlineTicks?: number;
|
|
13
|
+
literalLine?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Splits safe visible text from a possible control block while carrying Markdown context across chunks. */
|
|
17
|
+
export function splitAtPossibleSerializedToolCall(
|
|
18
|
+
text: string,
|
|
19
|
+
initialContext: TextContext = { fence: null, lineStart: true },
|
|
20
|
+
final = false,
|
|
21
|
+
allowMidLineBare = false,
|
|
22
|
+
): {
|
|
23
|
+
emit: string;
|
|
24
|
+
defer: string;
|
|
25
|
+
hasOpenTag: boolean;
|
|
26
|
+
context: TextContext;
|
|
27
|
+
} {
|
|
28
|
+
const context = { ...initialContext };
|
|
29
|
+
const split = (at: number, hasOpenTag = false) => ({
|
|
30
|
+
emit: text.slice(0, at), defer: text.slice(at), hasOpenTag, context,
|
|
31
|
+
});
|
|
32
|
+
for (let index = 0; index < text.length; index++) {
|
|
33
|
+
if (context.lineStart) {
|
|
34
|
+
const rest = text.slice(index);
|
|
35
|
+
const fence = /^ {0,3}(`{3,}|~{3,})([^\n]*)/.exec(rest);
|
|
36
|
+
if (!context.inlineTicks && fence && (!context.fence || (fence[1]![0] === context.fence[0]
|
|
37
|
+
&& fence[1]!.length >= context.fence.length && /^[ \t\r]*$/.test(fence[2]!)))) {
|
|
38
|
+
if (!final && !rest.includes("\n")) return split(index);
|
|
39
|
+
context.fence = context.fence ? null : fence[1]!;
|
|
40
|
+
index += fence[0].length - 1;
|
|
41
|
+
context.lineStart = false;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (!final && /^ {0,3}(`*|~*)$/.test(rest)) return split(index);
|
|
45
|
+
context.literalLine = /[ \t>]/.test(text[index]!);
|
|
46
|
+
}
|
|
47
|
+
// Plain wrappers retain their bare-line rule. Nonced wrappers also occur after prose;
|
|
48
|
+
// quotes, indentation and Markdown code remain literal, even beside a matching call.
|
|
49
|
+
if (!context.fence && !context.inlineTicks && !context.literalLine && text[index] === "<") {
|
|
50
|
+
const rest = text.slice(index);
|
|
51
|
+
const opening = OPENING.exec(rest)?.[0];
|
|
52
|
+
if (opening && (context.lineStart || opening !== OPEN_TAG || allowMidLineBare)) {
|
|
53
|
+
const header = rest.slice(opening.length).trimStart();
|
|
54
|
+
if (/^<function=[^>\r\n]+>/.test(header)) return split(index, true);
|
|
55
|
+
if (!final && (FUNCTION_TAG.startsWith(header)
|
|
56
|
+
|| (header.startsWith(FUNCTION_TAG) && !/[>\r\n]/.test(header.slice(FUNCTION_TAG.length))))) {
|
|
57
|
+
return split(index);
|
|
58
|
+
}
|
|
59
|
+
} else if (!final && (OPEN_TAG.startsWith(rest) || OPEN_PREFIX.test(rest))) return split(index);
|
|
60
|
+
}
|
|
61
|
+
if (!context.fence && text[index] === "`") {
|
|
62
|
+
let end = index + 1;
|
|
63
|
+
while (text[end] === "`") end++;
|
|
64
|
+
if (!final && end === text.length) return split(index);
|
|
65
|
+
const ticks = end - index;
|
|
66
|
+
if (!context.inlineTicks) context.inlineTicks = ticks;
|
|
67
|
+
else if (context.inlineTicks === ticks) context.inlineTicks = undefined;
|
|
68
|
+
index = end - 1;
|
|
69
|
+
}
|
|
70
|
+
context.lineStart = text[index] === "\n";
|
|
71
|
+
}
|
|
72
|
+
return split(text.length);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The line, fence and inline-code state after `text`. Serialized blocks are neutralised first so
|
|
77
|
+
* the scan runs through the whole text instead of stopping at the first opening tag; a block ends
|
|
78
|
+
* mid-line, which is exactly what the neutral spelling reports too.
|
|
79
|
+
*/
|
|
80
|
+
export function contextAfter(text: string, context: TextContext): TextContext {
|
|
81
|
+
if (text.length === 0) return context;
|
|
82
|
+
return splitAtPossibleSerializedToolCall(text.replace(new RegExp(`<tool_call${TAG_SUFFIX}>`, "g"), "<tool-call>"), context, true).context;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* A deferred header/fence can itself be long. While its unresolved portion only grows, a
|
|
87
|
+
* chunk-local check suffices; the first resolving character goes back through the full splitter.
|
|
88
|
+
* Each unbounded phase is scanned once on entry and once on exit, preserving its Markdown rules.
|
|
89
|
+
*/
|
|
90
|
+
export function deferredContinuationBreak(text: string, context: TextContext): RegExp | undefined {
|
|
91
|
+
const opening = OPENING.exec(text)?.[0];
|
|
92
|
+
if (opening) {
|
|
93
|
+
const header = text.slice(opening.length);
|
|
94
|
+
if (/^\s*$/.test(header)) return /\S/;
|
|
95
|
+
if (/^\s*<function=[^>\r\n]*$/.test(header)) return /[>\r\n]/;
|
|
96
|
+
}
|
|
97
|
+
if (!context.inlineTicks && context.lineStart) {
|
|
98
|
+
const fence = /^ {0,3}(`{3,}|~{3,})([^\n]*)$/.exec(text);
|
|
99
|
+
if (fence) {
|
|
100
|
+
if (!context.fence) return /\n/;
|
|
101
|
+
// Closing-fence ticks may grow only before its optional whitespace suffix.
|
|
102
|
+
if (!fence[2]) return fence[1]![0] === "`" ? /[^`]/ : /[^~]/;
|
|
103
|
+
return /[^ \t\r]/;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (/^`+$/.test(text)) return /[^`]/;
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { TRANSLATOR_MAX_CALL_ARGUMENT_BYTES, type TranslatorBudget } from "../../lib/translator-budget";
|
|
2
|
+
import { jsonUtf8Bytes } from "../../lib/json-byte-size";
|
|
3
|
+
import type { AdapterEvent } from "../../types";
|
|
4
|
+
import {
|
|
5
|
+
hasNestedSerializedToolCallHeader, MAX_STANDALONE_CANDIDATES, recoverStandaloneBareBlocks, reconcileSerializedToolCallEvents, scanSerializedToolCalls, SerializedToolCallContentBuffer,
|
|
6
|
+
type FreeformToolIdentity, type StructuredToolCallReference,
|
|
7
|
+
} from "./serialized-tool-call-content";
|
|
8
|
+
|
|
9
|
+
const MAX_CANDIDATE_BYTES = 4 * 1024 * 1024;
|
|
10
|
+
type BareCall = Pick<ReturnType<typeof recoverStandaloneBareBlocks>[number], "name" | "input">;
|
|
11
|
+
const isMiMo = (model: string | undefined) => /(?:^|[/-])mimo-v2(?:\.|$)/i.test(model ?? "");
|
|
12
|
+
const argumentsFor = (call: BareCall) => JSON.stringify({ input: call.input });
|
|
13
|
+
function* callEvents(call: BareCall, id: string, restore: (name: string) => string): Generator<AdapterEvent> {
|
|
14
|
+
yield { type: "tool_call_start", id, name: restore(call.name) };
|
|
15
|
+
yield { type: "tool_call_delta", arguments: argumentsFor(call) };
|
|
16
|
+
yield { type: "tool_call_end" };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** An explicitly separate policy for bare blocks with NO structured calls, never identified wrappers. */
|
|
20
|
+
export class StandaloneBareToolCallBuffer extends SerializedToolCallContentBuffer {
|
|
21
|
+
private candidates: BareCall[] = [];
|
|
22
|
+
private candidateBytes = 0;
|
|
23
|
+
private disabled = false;
|
|
24
|
+
private readonly eligible: boolean;
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
private readonly recoveryBudget: TranslatorBudget,
|
|
28
|
+
model: string | undefined,
|
|
29
|
+
private readonly freeformTools: ReadonlyMap<string, FreeformToolIdentity>,
|
|
30
|
+
) {
|
|
31
|
+
super(recoveryBudget, isMiMo(model) && freeformTools.size > 0);
|
|
32
|
+
this.eligible = isMiMo(model) && freeformTools.size > 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Retain candidate inputs only, with aggregate and per-call limits; overflow disables the batch. */
|
|
36
|
+
private remember(calls: BareCall[]): void {
|
|
37
|
+
if (this.disabled || calls.length === 0) return;
|
|
38
|
+
const sizes = calls.map(call => jsonUtf8Bytes({ name: call.name, input: call.input }));
|
|
39
|
+
const bytes = sizes.reduce((sum, size) => sum + size, 0);
|
|
40
|
+
if (sizes.some(size => size > TRANSLATOR_MAX_CALL_ARGUMENT_BYTES)
|
|
41
|
+
|| this.candidateBytes + bytes > MAX_CANDIDATE_BYTES || this.candidates.length + calls.length > MAX_STANDALONE_CANDIDATES) {
|
|
42
|
+
this.clearCandidates();
|
|
43
|
+
this.disabled = true;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const retained = this.recoveryBudget.reserveTransient(bytes, { kind: "live_transient" });
|
|
47
|
+
try {
|
|
48
|
+
const temporary = this.recoveryBudget.reserveTransient(bytes, { kind: "live_transient" });
|
|
49
|
+
try {
|
|
50
|
+
// Detach small inputs from a large released answer's backing string.
|
|
51
|
+
const copies = calls.map(({ name, input }) => JSON.parse(JSON.stringify({ name, input })) as BareCall);
|
|
52
|
+
this.candidates.push(...copies);
|
|
53
|
+
this.candidateBytes += bytes;
|
|
54
|
+
retained.commitRetained();
|
|
55
|
+
} finally { temporary.release(); }
|
|
56
|
+
} catch (error) {
|
|
57
|
+
retained.release();
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
protected override releaseBounded(): AdapterEvent[] {
|
|
63
|
+
if (!this.eligible || this.disabled) return super.releaseBounded();
|
|
64
|
+
const text = this.current();
|
|
65
|
+
const scan = scanSerializedToolCalls(text, this.context, true);
|
|
66
|
+
if (scan.overflow || scan.incomplete || scan.calls.some(call => hasNestedSerializedToolCallHeader(call.body))) {
|
|
67
|
+
// We cannot safely recognize a later inner block after losing its unresolved outer body.
|
|
68
|
+
this.clearCandidates();
|
|
69
|
+
this.disabled = true;
|
|
70
|
+
return super.releaseBounded();
|
|
71
|
+
}
|
|
72
|
+
this.remember(recoverStandaloneBareBlocks(text, this.freeformTools, this.context));
|
|
73
|
+
return super.releaseBounded();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
*recoverStandalone(noStructuredCalls: boolean, nextId: () => string, restore: (name: string) => string): Generator<AdapterEvent> {
|
|
77
|
+
if (!noStructuredCalls) {
|
|
78
|
+
this.clearCandidates();
|
|
79
|
+
this.disabled = true;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (!this.eligible || this.disabled) return;
|
|
83
|
+
if (scanSerializedToolCalls(this.current(), this.context, true).overflow) {
|
|
84
|
+
this.clearCandidates();
|
|
85
|
+
this.disabled = true;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const held = recoverStandaloneBareBlocks(this.current(), this.freeformTools, this.context);
|
|
89
|
+
this.remember(held);
|
|
90
|
+
// The first terminal flush is the only recovery decision; content after it cannot authorize a call.
|
|
91
|
+
if (this.disabled || this.candidates.length === 0) { this.disabled = true; return; }
|
|
92
|
+
yield* this.drainRanges(held);
|
|
93
|
+
for (const call of this.candidates) yield* callEvents(call, nextId(), restore);
|
|
94
|
+
this.clearCandidates();
|
|
95
|
+
this.disabled = true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private clearCandidates(): void {
|
|
99
|
+
this.recoveryBudget.releaseRetained(this.candidateBytes, { kind: "live_transient" });
|
|
100
|
+
this.candidateBytes = 0;
|
|
101
|
+
this.candidates = [];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
override dispose(): void {
|
|
105
|
+
this.clearCandidates();
|
|
106
|
+
super.dispose();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Buffered counterpart; the existing structured-call/identified-wrapper reconciler owns its path. */
|
|
111
|
+
export function reconcileStandaloneBareToolCallEvents(
|
|
112
|
+
events: AdapterEvent[], start: number, end: number,
|
|
113
|
+
structuredCalls: readonly StructuredToolCallReference[], budget: TranslatorBudget,
|
|
114
|
+
model: string | undefined, freeformTools: ReadonlyMap<string, FreeformToolIdentity>, restore: (name: string) => string,
|
|
115
|
+
): void {
|
|
116
|
+
if (structuredCalls.length > 0 || !isMiMo(model) || freeformTools.size === 0) {
|
|
117
|
+
reconcileSerializedToolCallEvents(events, start, end, structuredCalls, budget);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const buffer = new StandaloneBareToolCallBuffer(budget, model, freeformTools);
|
|
121
|
+
const reconciled: AdapterEvent[] = [];
|
|
122
|
+
try {
|
|
123
|
+
for (const event of events.slice(start, end)) {
|
|
124
|
+
if (event.type !== "text_delta") reconciled.push(...buffer.hold(event).filter(held => held.type !== "heartbeat"));
|
|
125
|
+
else {
|
|
126
|
+
const text = buffer.ingest(event.text);
|
|
127
|
+
if (text) reconciled.push({ type: "text_delta", text });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
let sequence = 0;
|
|
131
|
+
reconciled.push(...buffer.recoverStandalone(true, () => `call_${++sequence}`, restore), ...buffer.drain([]));
|
|
132
|
+
} finally {
|
|
133
|
+
buffer.dispose();
|
|
134
|
+
}
|
|
135
|
+
events.splice(start, end - start, ...reconciled);
|
|
136
|
+
}
|
|
@@ -47,7 +47,8 @@ import { messagesToChatFormat } from "./openai-chat/messages";
|
|
|
47
47
|
import { withOpenAIChatToolNames } from "./openai-chat/tool-name-registry";
|
|
48
48
|
import { openAIChatTransport, stripBracketedModelSuffix } from "./openai-chat/wire";
|
|
49
49
|
import { toolChoiceToChatFormat, toolsToChatFormatForProvider } from "./openai-chat/tool-schema";
|
|
50
|
-
import { freeformToolsByWireName, type FreeformToolIdentity,
|
|
50
|
+
import { freeformToolsByWireName, type FreeformToolIdentity, reconcileStructuredToolCall, reconcileStructuredToolCalls } from "./openai-chat/serialized-tool-call-content";
|
|
51
|
+
import { StandaloneBareToolCallBuffer, reconcileStandaloneBareToolCallEvents } from "./openai-chat/standalone-bare-tool-calls";
|
|
51
52
|
|
|
52
53
|
export { stripBracketedModelSuffix } from "./openai-chat/wire";
|
|
53
54
|
export { buildOpenAIChatPassthroughRequest } from "./openai-chat/passthrough";
|
|
@@ -300,7 +301,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
300
301
|
sawArgumentsString: boolean;
|
|
301
302
|
}
|
|
302
303
|
const pendingToolCalls: PendingToolCall[] = [];
|
|
303
|
-
const toolCallContent = new
|
|
304
|
+
const toolCallContent = new StandaloneBareToolCallBuffer(budget, lastRequestedModelId, freeformTools);
|
|
304
305
|
const heldText = (): AdapterEvent[] => toolCallContent.drain([]);
|
|
305
306
|
let toolCallSeq = 0;
|
|
306
307
|
const closeToolCalls = (): PendingToolCall[] => {
|
|
@@ -322,7 +323,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
322
323
|
// Returns "terminate" when a pending call cannot be dispatched, so every flush site
|
|
323
324
|
// stops the turn instead of emitting an unusable call. `closeToolCalls()` runs first,
|
|
324
325
|
// so budget reservations are released for every pending call even on the early return.
|
|
325
|
-
const flushToolCalls = function* (): Generator<AdapterEvent, "continue" | "terminate"> {
|
|
326
|
+
const flushToolCalls = function* (terminal = true): Generator<AdapterEvent, "continue" | "terminate"> {
|
|
326
327
|
const calls = closeToolCalls();
|
|
327
328
|
for (const call of calls) {
|
|
328
329
|
// Ingest already proved `name` is a string; the typeof guard keeps this branch
|
|
@@ -335,9 +336,9 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
335
336
|
return yield* terminateWithError(unnamedToolCallEvent(pendingUsage));
|
|
336
337
|
}
|
|
337
338
|
}
|
|
338
|
-
// Held markup is released only now, as one batch per response: the doubled-input repair needs every call.
|
|
339
339
|
const references = reconcileStructuredToolCalls(calls.map(call => ({ wireName: call.name, restoredName: toolNames.restore(call.name), argumentsText: call.args, freeformTool: freeformTools.get(call.name) })), toolCallContent.current(), !toolCallContent.releasedAnswerText && /(?:^|[/-])mimo-v2(?:\.|$)/i.test(lastRequestedModelId ?? ""));
|
|
340
340
|
calls.forEach((call, index) => { call.args = references[index]!.argumentsText; });
|
|
341
|
+
yield* toolCallContent.recoverStandalone(calls.length === 0 && terminal, () => `call_${++toolCallSeq}`, name => toolNames.restore(name));
|
|
341
342
|
yield* toolCallContent.drain(references);
|
|
342
343
|
for (const call of calls) {
|
|
343
344
|
if (!call.id) call.id = `call_${++toolCallSeq}`;
|
|
@@ -657,7 +658,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
657
658
|
yield* terminateWithError({ type: "error", message: "upstream stream ended without a terminal signal ([DONE] or finish_reason) — possible truncation" });
|
|
658
659
|
return;
|
|
659
660
|
}
|
|
660
|
-
if ((yield* flushToolCalls()) === "terminate") return;
|
|
661
|
+
if ((yield* flushToolCalls(sawFinish)) === "terminate") return; // A cut stream never synthesizes a bare call.
|
|
661
662
|
const stopReason = stopReasonFor(finishReason);
|
|
662
663
|
yield { type: "done", usage: pendingUsage, ...(stopReason ? { stopReason } : {}) };
|
|
663
664
|
} catch (error) {
|
|
@@ -804,7 +805,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
804
805
|
}
|
|
805
806
|
const references = reconcileStructuredToolCalls(structuredCalls, answerText, /(?:^|[/-])mimo-v2(?:\.|$)/i.test(lastRequestedModelId ?? ""));
|
|
806
807
|
structuredCalls.forEach((call, index) => { call.delta.arguments = references[index]!.argumentsText; });
|
|
807
|
-
|
|
808
|
+
reconcileStandaloneBareToolCallEvents(events, contentStart, contentEnd, references, budget, lastRequestedModelId, freeformTools, name => toolNames.restore(name));
|
|
808
809
|
const stopReason = stopReasonFor(choice.finish_reason);
|
|
809
810
|
events.push({
|
|
810
811
|
type: "done",
|