@bitkyc08/opencodex 2.57.0 → 2.59.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 +28 -10
- package/gui/dist/assets/index-C5IebErG.js +136 -0
- package/gui/dist/assets/{index-C5-RdDmD.css → index-OESInAjC.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/crusoe.svg +1 -0
- package/gui/dist/provider-icons/opper.svg +3 -0
- package/package.json +2 -2
- package/src/adapters/base.ts +11 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +5 -4
- package/src/adapters/command-code.ts +13 -4
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +65 -1
- package/src/adapters/cursor/effort-map.ts +16 -2
- package/src/adapters/cursor/envelope-echo.ts +55 -2
- package/src/adapters/cursor/live-transport.ts +5 -1
- package/src/adapters/cursor/message-mapper.ts +3 -2
- package/src/adapters/cursor/protobuf-events.ts +110 -11
- package/src/adapters/cursor/protobuf-request.ts +27 -6
- package/src/adapters/cursor/request-builder.ts +14 -3
- package/src/adapters/cursor/text-toolcall.ts +230 -0
- package/src/adapters/cursor/thread-continuity.ts +141 -0
- package/src/adapters/cursor/tool-guidance.ts +5 -4
- package/src/adapters/cursor/types.ts +5 -0
- package/src/adapters/cursor.ts +97 -6
- package/src/adapters/devin/cloud-direct/chat.ts +11 -2
- package/src/adapters/devin/cloud-direct/index.ts +7 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +103 -0
- package/src/adapters/devin.ts +75 -13
- package/src/adapters/google-antigravity-wire.ts +29 -2
- package/src/adapters/google-http.ts +45 -13
- package/src/adapters/google.ts +23 -4
- package/src/adapters/mimo-free.ts +32 -17
- package/src/adapters/ollama-native.ts +42 -8
- package/src/adapters/openai-chat/response-events.ts +61 -0
- package/src/adapters/openai-chat.ts +5 -10
- package/src/adapters/openai-responses/passthrough.ts +40 -5
- package/src/adapters/openai-responses/request-strips.ts +43 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
- package/src/adapters/openai-responses/tool-schema.ts +19 -7
- package/src/adapters/physical-send.ts +50 -0
- package/src/adapters/responses-tool-schema.ts +76 -46
- package/src/adapters/run-turn-queue.ts +17 -4
- package/src/bridge/response-json.ts +2 -2
- package/src/bridge/sse.ts +166 -25
- package/src/claude/context-windows.ts +22 -0
- package/src/claude/outbound.ts +46 -5
- package/src/cli/account-api.ts +4 -3
- package/src/cli/account-extended.ts +22 -2
- package/src/cli/account-orca-import.ts +63 -0
- package/src/cli/account.ts +32 -4
- package/src/cli/capabilities.ts +40 -0
- package/src/cli/claude.ts +29 -1
- package/src/cli/codex-cli-update.ts +97 -2
- package/src/cli/config-command.ts +35 -18
- package/src/cli/dispatch.ts +71 -4
- package/src/cli/doctor.ts +197 -2
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +132 -22
- package/src/cli/models-runtime.ts +33 -4
- package/src/cli/registry.ts +11 -1
- package/src/cli/runtime-api.ts +44 -0
- package/src/cli/start-args.ts +94 -0
- package/src/cli/system-command.ts +72 -1
- package/src/cli/uninstall-client-state.ts +12 -0
- package/src/client/machine-api.ts +4 -3
- package/src/client/machine-listener.ts +14 -1
- package/src/clients/config-export/constants.ts +2 -3
- package/src/clients/config-export.ts +5 -5
- package/src/codex/account-store.ts +81 -5
- package/src/codex/auth-api/pool-quota-probe.ts +14 -3
- package/src/codex/auth-api/routes.ts +17 -2
- package/src/codex/auth-context.ts +58 -20
- package/src/codex/catalog/build-entries.ts +25 -4
- package/src/codex/catalog/derive-entry.ts +8 -1
- package/src/codex/catalog/effort.ts +10 -6
- package/src/codex/catalog/gather-capture.ts +1 -0
- package/src/codex/catalog/model-hints.ts +37 -5
- package/src/codex/catalog/parsing.ts +83 -5
- package/src/codex/catalog/reserve-warn.ts +96 -0
- package/src/codex/catalog/retained-sync.ts +19 -0
- package/src/codex/catalog/routed-gather.ts +42 -3
- package/src/codex/cli-installation-identity.ts +210 -0
- package/src/codex/cli-installation-targets.ts +158 -0
- package/src/codex/convergence.ts +5 -0
- package/src/codex/desktop-switches.ts +145 -0
- package/src/codex/history-job.ts +5 -1
- package/src/codex/history-provider.ts +37 -5
- package/src/codex/history-state-open.ts +105 -0
- package/src/codex/history-worker.ts +14 -1
- package/src/codex/inject/config-toml.ts +44 -2
- package/src/codex/inject/remove.ts +145 -7
- package/src/codex/inject/restore.ts +204 -32
- package/src/codex/inject.ts +6 -9
- package/src/codex/lineage.ts +83 -32
- package/src/codex/loopback-target.ts +40 -0
- package/src/codex/main-account-hard-lock.ts +2 -1
- package/src/codex/main-account.ts +10 -3
- package/src/codex/main-device-reauth.ts +17 -9
- package/src/codex/model-entitlements.ts +60 -1
- package/src/codex/native-profile-startup.ts +64 -20
- package/src/codex/observed-model-denials.ts +137 -0
- package/src/codex/orca-auth-source.ts +94 -0
- package/src/codex/orca-import.ts +219 -0
- package/src/codex/prompt-text-probe.ts +282 -12
- package/src/codex/quota-401-recovery.ts +12 -0
- package/src/codex/quota-types.ts +65 -0
- package/src/codex/quota.ts +24 -19
- package/src/codex/routing/cooldown-math.ts +8 -47
- package/src/codex/routing/pin-drain.ts +57 -0
- package/src/codex/routing.ts +13 -15
- package/src/codex/subagent-model-fallback.ts +94 -0
- package/src/codex/windows-installation-files.ts +224 -0
- package/src/combos/failover.ts +122 -5
- package/src/config/atomic-write.ts +83 -8
- package/src/config/diagnostics.ts +21 -0
- package/src/config/load-degrade.ts +15 -0
- package/src/config/pending-teardown.ts +8 -0
- package/src/config/process-state.ts +36 -3
- package/src/config/provider-relative-send-path.ts +16 -0
- package/src/config/proxy-env.ts +23 -5
- package/src/config/schema/config-schema.ts +23 -0
- package/src/config/schema/leaf-validators.ts +65 -17
- package/src/generated/compatibility-version.json +337 -201
- package/src/generated/model-metadata.ts +1 -1
- package/src/lib/bounded-body.ts +4 -2
- package/src/lib/bounded-subprocess.ts +62 -10
- package/src/lib/destination-policy.ts +48 -6
- package/src/lib/errors.ts +3 -15
- package/src/lib/local-destinations.ts +32 -5
- package/src/lib/provider-outbound.ts +3 -3
- package/src/lib/proxy-env.ts +70 -3
- package/src/lib/request-execution-budget.ts +11 -3
- package/src/lib/response-body-inactivity.ts +193 -0
- package/src/lib/retry-delay.ts +69 -0
- package/src/lib/socks5-fetch.ts +631 -0
- package/src/lib/spend-reservation-ledger.ts +115 -9
- package/src/lib/windows-secret-acl.ts +151 -15
- package/src/lib/windows-user-principal.ts +5 -1
- package/src/lib/workflow-budget.ts +145 -8
- package/src/oauth/account-quota-rank.ts +72 -15
- package/src/oauth/generic-account-failover.ts +40 -27
- package/src/oauth/orcarouter.ts +15 -2
- package/src/oauth/store.ts +8 -0
- package/src/providers/codex-capacity.ts +9 -0
- package/src/providers/derive.ts +6 -0
- package/src/providers/devin-provider-merge-migration.ts +33 -12
- package/src/providers/free-directory.ts +20 -2
- package/src/providers/key-failover.ts +261 -7
- package/src/providers/model-discovery.ts +19 -7
- package/src/providers/model-rename-migration.ts +1 -0
- package/src/providers/openai-sidecar.ts +4 -0
- package/src/providers/opencode-go-transport.ts +14 -5
- package/src/providers/quota/report-cache.ts +3 -0
- package/src/providers/registry/entries-core.ts +11 -0
- package/src/providers/registry/entries-extended.ts +146 -28
- package/src/providers/registry/model-seeds.ts +136 -29
- package/src/providers/registry/types.ts +9 -0
- package/src/responses/apply-patch-envelope.ts +44 -11
- package/src/responses/bridge-search-replay-cache.ts +152 -0
- package/src/responses/code-mode-helper-compat.ts +26 -16
- package/src/responses/custom-tool-compat.ts +1 -1
- package/src/responses/hosted-tool-policy.ts +85 -2
- package/src/responses/schema.ts +9 -2
- package/src/responses/spill-store.ts +17 -0
- package/src/responses/state/body-policy.ts +25 -0
- package/src/responses/state/spill-queue.ts +8 -6
- package/src/responses/state.ts +3 -22
- package/src/router.ts +4 -0
- package/src/server/auth-cors.ts +27 -0
- package/src/server/chat-completions.ts +9 -4
- package/src/server/chat-native-sse.ts +26 -9
- package/src/server/chat-native.ts +10 -4
- package/src/server/claude-messages.ts +24 -2
- package/src/server/gui-static.ts +36 -2
- package/src/server/inbound-body-admission.ts +187 -0
- package/src/server/index/websocket-handler.ts +48 -1
- package/src/server/index.ts +15 -19
- package/src/server/management/api-access.ts +3 -4
- package/src/server/management/config-routes.ts +57 -10
- package/src/server/management/provider-capability-config.ts +35 -7
- package/src/server/management/provider-routes.ts +70 -18
- package/src/server/models-capabilities.ts +24 -3
- package/src/server/proxy-liveness.ts +97 -2
- package/src/server/relay.ts +17 -24
- package/src/server/request-log.ts +25 -1
- package/src/server/responses/adapter-continuation.ts +71 -27
- package/src/server/responses/adapter-delivery.ts +39 -8
- package/src/server/responses/adapter-dispatch.ts +52 -24
- package/src/server/responses/codex-ws-exchange.ts +65 -4
- package/src/server/responses/combo-stream-preflight.ts +68 -5
- package/src/server/responses/compact.ts +60 -11
- package/src/server/responses/core-codex-account.ts +83 -22
- package/src/server/responses/core-combo.ts +26 -0
- package/src/server/responses/core-normalize.ts +12 -5
- package/src/server/responses/core-options.ts +3 -0
- package/src/server/responses/fetch-helpers.ts +72 -3
- package/src/server/responses/native-injection-protocol.ts +42 -0
- package/src/server/responses/native-injection-replay.ts +105 -0
- package/src/server/responses/native-injection.ts +242 -0
- package/src/server/responses/native-response-control.ts +56 -0
- package/src/server/responses/native-response-json.ts +14 -0
- package/src/server/responses/native-response-output.ts +37 -0
- package/src/server/responses/native-steering-log.ts +44 -0
- package/src/server/responses/native-steering-policy.ts +49 -0
- package/src/server/responses/native-steering-replay.ts +126 -0
- package/src/server/responses/native-steering-settings.ts +76 -0
- package/src/server/responses/native-steering.ts +400 -0
- package/src/server/responses/native-tool-results.ts +130 -0
- package/src/server/responses/passthrough-delivery.ts +21 -1
- package/src/server/responses/passthrough-dispatch.ts +146 -49
- package/src/server/responses/passthrough-execution.ts +11 -1
- package/src/server/responses/request-prepare.ts +70 -0
- package/src/server/responses/request-send-budget.ts +84 -7
- package/src/server/responses/request-sidecar-auth.ts +16 -8
- package/src/server/responses/request-spend.ts +38 -9
- package/src/server/responses/request-transport.ts +13 -10
- package/src/server/responses/run-turn-execution.ts +20 -5
- package/src/server/responses/sidecar-execution.ts +2 -0
- package/src/server/responses/ws-upstream.ts +23 -2
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/sse-frame-buffer.ts +12 -10
- package/src/server/sse-payload-rewrite.ts +36 -9
- package/src/server/stop-teardown.ts +8 -1
- package/src/server/system-env-shell.ts +5 -1
- package/src/server/system-env.ts +7 -1
- package/src/server/workflow-refusal.ts +56 -2
- package/src/server/ws-bridge.ts +16 -1
- package/src/service/cli.ts +29 -7
- package/src/service/guards.ts +10 -0
- package/src/service/health.ts +43 -0
- package/src/service/state.ts +7 -2
- package/src/types/accounts.ts +4 -0
- package/src/types/config.ts +104 -3
- package/src/types/provider.ts +32 -0
- package/src/types/request.ts +7 -1
- package/src/types/wire.ts +9 -1
- package/src/usage/expected-prices.ts +28 -0
- package/src/usage/log.ts +87 -4
- package/src/web-search/passthrough-bridge.ts +39 -5
- package/gui/dist/assets/index-Cz7CLdif.js +0 -128
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { AdapterFetchContext } from "./base";
|
|
2
|
+
import type { SendClass } from "../lib/request-execution-budget";
|
|
3
|
+
import type { AttemptRecoveryKind } from "../usage/log";
|
|
4
|
+
import { abortError, SendBudgetExhaustedError } from "../lib/upstream-retry";
|
|
5
|
+
|
|
6
|
+
type PacedFetch = typeof globalThis.fetch & {
|
|
7
|
+
waitForPacing?: (signal?: AbortSignal) => Promise<void>;
|
|
8
|
+
unpacedFetch?: typeof globalThis.fetch;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** One ordinal sequence per adapter fetchResponse call, across all of its inference retries.
|
|
12
|
+
* Consumption starts at underlying executor invocation; its own later preflight may still fail. */
|
|
13
|
+
export function createAdapterPhysicalSend(ctx: AdapterFetchContext = {}, fallback = globalThis.fetch) {
|
|
14
|
+
const executor = (ctx.executor ?? fallback) as PacedFetch;
|
|
15
|
+
let ordinal = 0;
|
|
16
|
+
return async (options: {
|
|
17
|
+
url: string;
|
|
18
|
+
sendClass?: SendClass;
|
|
19
|
+
recovery?: AttemptRecoveryKind;
|
|
20
|
+
/** Runs only after admission, e.g. backoff and cancellation of a superseded response. */
|
|
21
|
+
beforeDispatch?: () => void | Promise<void>;
|
|
22
|
+
dispatch: (executor: typeof globalThis.fetch) => Promise<Response>;
|
|
23
|
+
}): Promise<Response> => {
|
|
24
|
+
if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
|
|
25
|
+
const decision = ctx.sendBudget?.reserveDispatch({
|
|
26
|
+
sendClass: options.sendClass ?? "transient", targetKey: options.url,
|
|
27
|
+
});
|
|
28
|
+
if (decision && !decision.allowed) throw new SendBudgetExhaustedError(options.url);
|
|
29
|
+
const permit = decision?.allowed ? decision.permit : undefined;
|
|
30
|
+
let dispatched = false;
|
|
31
|
+
const physicalExecutor = (async (input, init) => {
|
|
32
|
+
if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
|
|
33
|
+
if (init?.signal?.aborted) throw abortError(init.signal);
|
|
34
|
+
if (dispatched || (permit && !permit.use())) throw new SendBudgetExhaustedError(options.url);
|
|
35
|
+
dispatched = true;
|
|
36
|
+
ordinal += 1;
|
|
37
|
+
ctx.onPhysicalSend?.({ ordinal, ...(options.recovery ? { recovery: options.recovery } : {}) });
|
|
38
|
+
return (executor.unpacedFetch ?? executor)(input, init);
|
|
39
|
+
}) as typeof globalThis.fetch;
|
|
40
|
+
try {
|
|
41
|
+
await executor.waitForPacing?.(ctx.abortSignal);
|
|
42
|
+
if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
|
|
43
|
+
await options.beforeDispatch?.();
|
|
44
|
+
if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
|
|
45
|
+
return await options.dispatch(physicalExecutor);
|
|
46
|
+
} finally {
|
|
47
|
+
permit?.release();
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -107,66 +107,96 @@ function usesUnicodePropertyEscape(pattern: string): boolean {
|
|
|
107
107
|
* preserved: loosening a nested constraint can instead reject an input in those contexts.
|
|
108
108
|
* Unsupported patterns there remain the destination's validation responsibility.
|
|
109
109
|
*
|
|
110
|
-
* Returns `node` itself when nothing was dropped.
|
|
111
|
-
*
|
|
110
|
+
* Returns `node` itself when nothing was dropped. Traversal keeps only the active path and clones
|
|
111
|
+
* only ancestors of a removed constraint, so a broad no-op schema does not create an output tree
|
|
112
|
+
* or one pending closure per sibling. The explicit stack still handles caller-controlled nesting
|
|
113
|
+
* depth; the separate Responses-only encrypted-marker normalization is unchanged.
|
|
112
114
|
*/
|
|
113
115
|
export function stripUnicodePropertyPatterns(node: unknown, inNameBag = false): unknown {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
116
|
+
interface Frame {
|
|
117
|
+
node: unknown[] | Record<string, unknown>;
|
|
118
|
+
inNameBag: boolean;
|
|
119
|
+
parent?: Frame;
|
|
120
|
+
parentKey?: string | number;
|
|
121
|
+
output?: unknown[] | Record<string, unknown>;
|
|
122
|
+
index?: number;
|
|
123
|
+
entries?: IterableIterator<[string, unknown]>;
|
|
124
|
+
}
|
|
120
125
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const
|
|
126
|
+
function * ownEntries(value: Record<string, unknown>): IterableIterator<[string, unknown]> {
|
|
127
|
+
// Unlike Object.entries(), this does not materialize every key/value pair before traversal.
|
|
128
|
+
for (const key in value) {
|
|
129
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) yield [key, value[key]];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
124
132
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
stack.push({ node: current[i], inNameBag: false, assign: value => { out[i] = value; } });
|
|
131
|
-
}
|
|
132
|
-
continue;
|
|
133
|
+
function cloneContainer(frame: Frame): unknown[] | Record<string, unknown> {
|
|
134
|
+
if (frame.output) return frame.output;
|
|
135
|
+
if (Array.isArray(frame.node)) {
|
|
136
|
+
frame.output = frame.node.slice();
|
|
137
|
+
return frame.output;
|
|
133
138
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
139
|
+
const output: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
|
|
140
|
+
for (const key in frame.node) {
|
|
141
|
+
if (Object.prototype.hasOwnProperty.call(frame.node, key)) output[key] = frame.node[key];
|
|
137
142
|
}
|
|
143
|
+
frame.output = output;
|
|
144
|
+
return output;
|
|
145
|
+
}
|
|
138
146
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
frame.
|
|
147
|
+
function finish(frame: Frame): void {
|
|
148
|
+
if (!frame.output || !frame.parent) return;
|
|
149
|
+
const parent = cloneContainer(frame.parent);
|
|
150
|
+
if (Array.isArray(parent)) parent[frame.parentKey as number] = frame.output;
|
|
151
|
+
else parent[frame.parentKey as string] = frame.output;
|
|
152
|
+
}
|
|
142
153
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
dropped++;
|
|
154
|
+
if (!node || typeof node !== "object") return node;
|
|
155
|
+
const root: Frame = { node: node as unknown[] | Record<string, unknown>, inNameBag };
|
|
156
|
+
const stack: Frame[] = [root];
|
|
157
|
+
|
|
158
|
+
while (stack.length > 0) {
|
|
159
|
+
const frame = stack[stack.length - 1]!;
|
|
160
|
+
|
|
161
|
+
if (Array.isArray(frame.node)) {
|
|
162
|
+
const index = frame.index ?? 0;
|
|
163
|
+
if (index >= frame.node.length) {
|
|
164
|
+
stack.pop();
|
|
165
|
+
finish(frame);
|
|
156
166
|
continue;
|
|
157
167
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
168
|
+
frame.index = index + 1;
|
|
169
|
+
const child = frame.node[index];
|
|
170
|
+
if (child && typeof child === "object") {
|
|
171
|
+
stack.push({ node: child as unknown[] | Record<string, unknown>, inNameBag: false, parent: frame, parentKey: index });
|
|
162
172
|
}
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
frame.entries ??= ownEntries(frame.node);
|
|
177
|
+
const next = frame.entries.next();
|
|
178
|
+
if (next.done) {
|
|
179
|
+
stack.pop();
|
|
180
|
+
finish(frame);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const [key, value] = next.value;
|
|
184
|
+
if (!frame.inNameBag && key === "pattern" && typeof value === "string" && usesUnicodePropertyEscape(value)) {
|
|
185
|
+
delete (cloneContainer(frame) as Record<string, unknown>)[key];
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (!frame.inNameBag && (PRESERVED_PATTERN_SUBTREES.has(key) || SCHEMA_LITERAL_VALUE_KEYS.has(key))) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (value && typeof value === "object") {
|
|
163
192
|
stack.push({
|
|
164
|
-
node: value,
|
|
165
|
-
inNameBag: SCHEMA_NAME_BAG_KEYS.has(key),
|
|
166
|
-
|
|
193
|
+
node: value as unknown[] | Record<string, unknown>,
|
|
194
|
+
inNameBag: !frame.inNameBag && SCHEMA_NAME_BAG_KEYS.has(key),
|
|
195
|
+
parent: frame,
|
|
196
|
+
parentKey: key,
|
|
167
197
|
});
|
|
168
198
|
}
|
|
169
199
|
}
|
|
170
200
|
|
|
171
|
-
return
|
|
201
|
+
return root.output ?? node;
|
|
172
202
|
}
|
|
@@ -28,6 +28,7 @@ export interface AdapterEventPreflight {
|
|
|
28
28
|
stream: AsyncIterable<AdapterEvent>;
|
|
29
29
|
error?: Extract<AdapterEvent, { type: "error" }>;
|
|
30
30
|
empty: boolean;
|
|
31
|
+
replayUnsafe: boolean;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
async function* replay(
|
|
@@ -51,10 +52,12 @@ export async function preflightAdapterEvents(
|
|
|
51
52
|
): Promise<AdapterEventPreflight> {
|
|
52
53
|
const iterator = source[Symbol.asyncIterator]();
|
|
53
54
|
const buffered: AdapterEvent[] = [];
|
|
55
|
+
let replayUnsafe = false;
|
|
54
56
|
while (true) {
|
|
55
57
|
const next = await iterator.next();
|
|
56
|
-
if (next.done) return { stream: replay(buffered, iterator), empty: true };
|
|
58
|
+
if (next.done) return { stream: replay(buffered, iterator), empty: true, replayUnsafe };
|
|
57
59
|
if (next.value.type === "heartbeat") {
|
|
60
|
+
replayUnsafe ||= next.value.replayUnsafe === true;
|
|
58
61
|
buffered.push(next.value);
|
|
59
62
|
if (buffered.length > PREFLIGHT_HEARTBEAT_RETAIN_LIMIT) buffered.shift();
|
|
60
63
|
continue;
|
|
@@ -62,9 +65,9 @@ export async function preflightAdapterEvents(
|
|
|
62
65
|
buffered.push(next.value);
|
|
63
66
|
if (next.value.type === "error") {
|
|
64
67
|
await iterator.return?.();
|
|
65
|
-
return { stream: replay(buffered, iterator), error: next.value, empty: false };
|
|
68
|
+
return { stream: replay(buffered, iterator), error: next.value, empty: false, replayUnsafe };
|
|
66
69
|
}
|
|
67
|
-
return { stream: replay(buffered, iterator), empty: false };
|
|
70
|
+
return { stream: replay(buffered, iterator), empty: false, replayUnsafe };
|
|
68
71
|
}
|
|
69
72
|
}
|
|
70
73
|
|
|
@@ -90,7 +93,17 @@ export function createAdapterEventQueue(opts?: {
|
|
|
90
93
|
const coalesceIntoTail = (event: AdapterEvent): boolean => {
|
|
91
94
|
const tail = queued[queued.length - 1];
|
|
92
95
|
if (!tail) return false;
|
|
93
|
-
if (event.type === "heartbeat")
|
|
96
|
+
if (event.type === "heartbeat") {
|
|
97
|
+
if (tail.type !== "heartbeat") return false;
|
|
98
|
+
// Heartbeats carry no ordering between themselves, but the replay-unsafe
|
|
99
|
+
// marker is not ordering — it is a latch. Dropping the incoming event
|
|
100
|
+
// would discard the only record that Cursor already performed a local
|
|
101
|
+
// side effect, and preflight would then permit an OAuth replay of it.
|
|
102
|
+
if (event.replayUnsafe === true && tail.replayUnsafe !== true) {
|
|
103
|
+
queued[queued.length - 1] = { type: "heartbeat", replayUnsafe: true };
|
|
104
|
+
}
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
94
107
|
if (event.type === "text_delta" && tail.type === "text_delta" && tail.phase === event.phase) {
|
|
95
108
|
if (tail.text.length + event.text.length > COALESCE_MAX_CHUNK_LENGTH) return false;
|
|
96
109
|
queued[queued.length - 1] = { type: "text_delta", text: tail.text + event.text, phase: tail.phase };
|
|
@@ -203,7 +203,7 @@ function buildResponseJSONWithBudget(
|
|
|
203
203
|
): string => {
|
|
204
204
|
const helper = resolveCodeModeHelperName(codeModeHelperName, toolName, args, namespace, options?.declaredToolNames);
|
|
205
205
|
return helper
|
|
206
|
-
? compileCodeModeHelperInput(args, helper)
|
|
206
|
+
? compileCodeModeHelperInput(args, helper, codeModeHelperName ?? toolName)
|
|
207
207
|
: repairFreeformToolInput(args, toolName, namespace);
|
|
208
208
|
};
|
|
209
209
|
const parseArgsObj = (args: string): Record<string, unknown> => {
|
|
@@ -527,7 +527,7 @@ function buildResponseJSONWithBudget(
|
|
|
527
527
|
compactionEncryptedContent = e.compactionEncryptedContent;
|
|
528
528
|
sawTerminal = true;
|
|
529
529
|
endTurn = e.endTurn;
|
|
530
|
-
cleanDone = e.stopReason
|
|
530
|
+
cleanDone = !isTruncatedStopReason(e.stopReason);
|
|
531
531
|
rawStopReason = e.stopReason;
|
|
532
532
|
if (e.providerState) options?.onProviderState?.(e.providerState);
|
|
533
533
|
// Match streaming: max_tokens and content_filter both terminate as incomplete.
|
package/src/bridge/sse.ts
CHANGED
|
@@ -16,7 +16,12 @@ import {
|
|
|
16
16
|
type OcxErrorPayload,
|
|
17
17
|
} from "../lib/errors";
|
|
18
18
|
import { redactSecretString } from "../lib/redact";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
freeformFallbackKeys,
|
|
21
|
+
mayBecomePatchEnvelope,
|
|
22
|
+
repairFreeformToolInput,
|
|
23
|
+
unwrapFreeformToolInput,
|
|
24
|
+
} from "../responses/apply-patch-envelope";
|
|
20
25
|
import { encodeCompactionSummary } from "../responses/compaction";
|
|
21
26
|
import { compileCodeModeHelperInput, resolveCodeModeHelperName } from "../responses/code-mode-helper-compat";
|
|
22
27
|
import { isTruncatedStopReason, truncationReasonFor } from "../responses/truncated-stop-reason";
|
|
@@ -145,37 +150,156 @@ export function bridgeToResponsesSSE(
|
|
|
145
150
|
): string => {
|
|
146
151
|
const helper = resolveCodeModeHelperName(codeModeHelperName, toolName, args, namespace, options?.declaredToolNames);
|
|
147
152
|
return helper
|
|
148
|
-
? compileCodeModeHelperInput(args, helper)
|
|
153
|
+
? compileCodeModeHelperInput(args, helper, codeModeHelperName ?? toolName)
|
|
149
154
|
: repairFreeformToolInput(args, toolName, namespace);
|
|
150
155
|
};
|
|
151
156
|
// Best-effort unwrap of a PARTIAL freeform arg buffer for live input streaming
|
|
152
157
|
// (`response.custom_tool_call_input.delta` — codex-rs uses it for UI preview only;
|
|
153
158
|
// the completed custom_tool_call item stays authoritative). Compact `{"input":"...`
|
|
154
159
|
// buffers get their string value progressively unescaped; anything else streams raw.
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
160
|
+
const JSON_WHITESPACE = new Set([" ", "\t", "\n", "\r"]);
|
|
161
|
+
type WrapperOpening =
|
|
162
|
+
| { state: "none" }
|
|
163
|
+
| { state: "prefix" }
|
|
164
|
+
| { state: "open"; valueStart: number };
|
|
165
|
+
/**
|
|
166
|
+
* Where the string value of `{"<key>":"` begins, tolerating the insignificant whitespace
|
|
167
|
+
* `JSON.parse` accepts.
|
|
168
|
+
*
|
|
169
|
+
* The earlier form of this compared the buffer against the compact literal `{"key":"`, so a
|
|
170
|
+
* wrapper written with spaces or newlines matched no prefix at all, streamed as raw JSON
|
|
171
|
+
* deltas and then completed as the unwrapped body. That is the same delta/completion
|
|
172
|
+
* disagreement #5047 closed for compact wrappers, reached through a different spelling:
|
|
173
|
+
* `unwrapFreeformToolInput` reads the completed text with `JSON.parse`, which does not care
|
|
174
|
+
* how the object is laid out, so neither can the streaming side.
|
|
175
|
+
*/
|
|
176
|
+
const wrapperOpening = (args: string, key: string): WrapperOpening => {
|
|
177
|
+
let index = 0;
|
|
178
|
+
for (const token of ["{", `"${key}"`, ":", '"']) {
|
|
179
|
+
while (index < args.length && JSON_WHITESPACE.has(args[index]!)) index++;
|
|
180
|
+
if (index >= args.length) return { state: "prefix" };
|
|
181
|
+
for (const expected of token) {
|
|
182
|
+
if (index >= args.length) return { state: "prefix" };
|
|
183
|
+
if (args[index] !== expected) return { state: "none" };
|
|
184
|
+
index++;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return { state: "open", valueStart: index };
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* Whether a body could still grow into one complete outer Markdown fence.
|
|
191
|
+
*
|
|
192
|
+
* `stripMarkdownCodeFence` removes such a fence at completion for exactly the two tools that
|
|
193
|
+
* own the grammar, so a fenced body's streamed bytes and its completed input disagree unless
|
|
194
|
+
* the stream holds. A buffer that does not open with a fence can never acquire one, so
|
|
195
|
+
* ordinary bodies are unaffected; a buffer that does keeps its preview suppressed for the
|
|
196
|
+
* whole call, because a closing fence can still be followed by more text that withdraws it.
|
|
197
|
+
*/
|
|
198
|
+
const mayBecomeFencedBody = (text: string, toolName: string): boolean => {
|
|
199
|
+
if (toolName !== "exec" && toolName !== "apply_patch") return false;
|
|
200
|
+
const head = text.trimStart();
|
|
201
|
+
if (head === "") return true;
|
|
202
|
+
return head.startsWith("```") || "```".startsWith(head);
|
|
203
|
+
};
|
|
204
|
+
/** The two-character escapes JSON defines, and nothing else. */
|
|
205
|
+
const JSON_ESCAPES = new Map<string, string>([
|
|
206
|
+
['"', '"'], ["\\", "\\"], ["/", "/"],
|
|
207
|
+
["b", "\b"], ["f", "\f"], ["n", "\n"], ["r", "\r"], ["t", "\t"],
|
|
208
|
+
]);
|
|
209
|
+
const LOW_SURROGATE_ESCAPE = /^\\u[dD][c-fC-F][0-9a-fA-F]{2}$/;
|
|
210
|
+
/**
|
|
211
|
+
* The decoded prefix of a JSON string body, stopping at the first byte it cannot resolve.
|
|
212
|
+
*
|
|
213
|
+
* Every stop is a hold rather than a guess, because `JSON.parse` decides the completed value
|
|
214
|
+
* and anything invented here would be retracted. Three of them are not obvious:
|
|
215
|
+
*
|
|
216
|
+
* An escape JSON does not define makes the whole wrapper unparseable no matter what arrives
|
|
217
|
+
* next, so completion falls back to the raw text. Returning `null` for that case stops the
|
|
218
|
+
* preview rather than continuing to decode a value the completed item will never carry.
|
|
219
|
+
* `\\b` and `\\f` are defined, and were previously decoded to the letters b and f.
|
|
220
|
+
*
|
|
221
|
+
* A lone high surrogate is not a character. Emitting one alone puts an unpaired code unit in a
|
|
222
|
+
* delta the client has to decode by itself, so the pair is emitted together or not at all.
|
|
223
|
+
*/
|
|
224
|
+
const decodeJsonStringPrefix = (body: string): string | null => {
|
|
159
225
|
let out = "";
|
|
160
226
|
for (let i = 0; i < body.length; i++) {
|
|
161
227
|
const c = body[i];
|
|
162
228
|
if (c === '"') break; // unescaped closing quote: value complete
|
|
163
|
-
if (c
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
229
|
+
if (c !== "\\") { out += c; continue; }
|
|
230
|
+
const n = body[i + 1];
|
|
231
|
+
if (n === undefined) break; // escape split across chunks: wait for more
|
|
232
|
+
if (n === "u") {
|
|
233
|
+
const hex = body.slice(i + 2, i + 6);
|
|
234
|
+
if (hex.length < 4) break; // split across chunks: wait for more
|
|
235
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex)) return null; // never parses
|
|
236
|
+
const code = parseInt(hex, 16);
|
|
237
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
238
|
+
const low = body.slice(i + 6, i + 12);
|
|
239
|
+
if (!LOW_SURROGATE_ESCAPE.test(low)) break;
|
|
240
|
+
out += String.fromCharCode(code, parseInt(low.slice(2), 16));
|
|
241
|
+
i += 11;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
out += String.fromCharCode(code);
|
|
245
|
+
i += 5;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const escaped = JSON_ESCAPES.get(n);
|
|
249
|
+
if (escaped === undefined) return null; // never parses
|
|
250
|
+
out += escaped;
|
|
251
|
+
i += 1;
|
|
176
252
|
}
|
|
177
253
|
return out;
|
|
178
254
|
};
|
|
255
|
+
/**
|
|
256
|
+
* The value to stream so far, or `null` to HOLD because nothing can be decided yet.
|
|
257
|
+
*
|
|
258
|
+
* `input` is decidable from its prefix: `unwrapFreeformToolInput` returns it whenever the
|
|
259
|
+
* key is present, whatever else the object carries, so its value can be unescaped
|
|
260
|
+
* progressively and never retracted.
|
|
261
|
+
*
|
|
262
|
+
* One case escapes that claim and is accepted rather than fixed: a duplicate `input` key.
|
|
263
|
+
* `JSON.parse` keeps the last one, so `{"input":"a","input":"b"}` streams a and completes with
|
|
264
|
+
* b. Closing it means holding every canonical wrapper until its object parses, which is the
|
|
265
|
+
* progressive streaming this path exists to provide. The completed item stays authoritative,
|
|
266
|
+
* and no model emits a duplicate key in practice.
|
|
267
|
+
*
|
|
268
|
+
* A wrapper that turns invalid AFTER streaming has committed has the same shape and the same
|
|
269
|
+
* answer. `{"input":"a` followed by `\\qb"}` has already published a when the undefined escape
|
|
270
|
+
* arrives, and completion returns the raw text because nothing parses. The preview stops
|
|
271
|
+
* there: no rewind, and no decoded text the completed item does not contain. Bounding the
|
|
272
|
+
* damage is what is available without giving up progressive streaming, and the args are
|
|
273
|
+
* unusable in that case whichever representation wins.
|
|
274
|
+
*
|
|
275
|
+
* A fallback key is not. It only unwraps when it is the SINGLE string field, and a second
|
|
276
|
+
* key can still arrive — so a value emitted early would have to be taken back. That is the
|
|
277
|
+
* rewind this holds instead: stream nothing until the object closes, then publish the one
|
|
278
|
+
* repaired body. The routed passthrough in `responses-custom-tool-repair.ts` already holds
|
|
279
|
+
* any object prefix for the same reason (#5047).
|
|
280
|
+
*/
|
|
281
|
+
const freeformPartialInput = (args: string, toolName: string): string | null => {
|
|
282
|
+
const openings = ["input", ...freeformFallbackKeys(toolName)]
|
|
283
|
+
.map(key => ({ key, opening: wrapperOpening(args, key) }));
|
|
284
|
+
const canonical = openings[0]!.opening;
|
|
285
|
+
if (canonical.state === "open") {
|
|
286
|
+
const decoded = decodeJsonStringPrefix(args.slice(canonical.valueStart));
|
|
287
|
+
if (decoded === null) return null;
|
|
288
|
+
return mayBecomeFencedBody(decoded, toolName) ? null : decoded;
|
|
289
|
+
}
|
|
290
|
+
if (openings.some(entry => entry.opening.state === "open")) {
|
|
291
|
+
// Committed to a fallback wrapper. Undecidable until the object is complete.
|
|
292
|
+
try {
|
|
293
|
+
JSON.parse(args);
|
|
294
|
+
} catch {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
return unwrapFreeformToolInput(args, toolName);
|
|
298
|
+
}
|
|
299
|
+
// Still an ambiguous prefix of some wrapper: which wrapper, if any, is not known yet.
|
|
300
|
+
if (openings.some(entry => entry.opening.state === "prefix")) return null;
|
|
301
|
+
return mayBecomeFencedBody(args, toolName) ? null : args;
|
|
302
|
+
};
|
|
179
303
|
// tool_search_call carries arguments as a JSON object ({query, limit}); parse the model's arg string.
|
|
180
304
|
const parseArgsObj = (args: string): Record<string, unknown> => {
|
|
181
305
|
try { const o = JSON.parse(args); return o && typeof o === "object" ? o : {}; } catch { return {}; }
|
|
@@ -1075,10 +1199,21 @@ export function bridgeToResponsesSSE(
|
|
|
1075
1199
|
});
|
|
1076
1200
|
}
|
|
1077
1201
|
if (currentToolCall.freeform && !currentToolCall.codeModeHelperName) {
|
|
1078
|
-
//
|
|
1079
|
-
//
|
|
1080
|
-
|
|
1081
|
-
|
|
1202
|
+
// `freeformPartialInput` holds while the buffer is still an ambiguous prefix
|
|
1203
|
+
// of a JSON wrapper; otherwise stream only the unwrapped input suffix, never
|
|
1204
|
+
// rewinding on a mode flip.
|
|
1205
|
+
//
|
|
1206
|
+
// The name is dropped for a namespaced tool that does not own the apply-patch
|
|
1207
|
+
// grammar, because `repairFreeformToolInput` drops it at completion for the
|
|
1208
|
+
// same reason. Streaming under a vocabulary the completed item does not use
|
|
1209
|
+
// is the same disagreement in the other direction.
|
|
1210
|
+
const ownsFreeformGrammar = currentToolCall.namespace === undefined
|
|
1211
|
+
|| currentToolCall.namespace === "functions";
|
|
1212
|
+
const full = freeformPartialInput(
|
|
1213
|
+
currentToolCall.args,
|
|
1214
|
+
ownsFreeformGrammar ? currentToolCall.name : "",
|
|
1215
|
+
);
|
|
1216
|
+
if (full !== null) {
|
|
1082
1217
|
const emitted = currentToolCall.inputEmitted ?? "";
|
|
1083
1218
|
// Also hold a buffer that could still become a complete patch envelope:
|
|
1084
1219
|
// at completion such a body is recompiled into an apply_patch helper call,
|
|
@@ -1086,7 +1221,13 @@ export function bridgeToResponsesSSE(
|
|
|
1086
1221
|
const mayCompile = declaresCodeModeExec(options?.declaredToolNames)
|
|
1087
1222
|
&& !currentToolCall.namespace
|
|
1088
1223
|
&& currentToolCall.name === "exec";
|
|
1089
|
-
|
|
1224
|
+
// `apply_patch` holds for a different reason with the same shape:
|
|
1225
|
+
// `normalizeApplyPatchDelimiters` rewrites a decorated `*** Begin Patch ***`
|
|
1226
|
+
// envelope at completion, so streaming the decorated markers would be
|
|
1227
|
+
// replaced by the normalized ones.
|
|
1228
|
+
const mayNormalize = ownsFreeformGrammar && currentToolCall.name === "apply_patch";
|
|
1229
|
+
if (!((mayCompile || mayNormalize) && mayBecomePatchEnvelope(full))
|
|
1230
|
+
&& full.startsWith(emitted) && full.length > emitted.length) {
|
|
1090
1231
|
emit("response.custom_tool_call_input.delta", {
|
|
1091
1232
|
item_id: currentToolCall.itemId, output_index: currentToolCall.outputIndex,
|
|
1092
1233
|
delta: full.slice(emitted.length),
|
|
@@ -1171,7 +1312,7 @@ export function bridgeToResponsesSSE(
|
|
|
1171
1312
|
break;
|
|
1172
1313
|
}
|
|
1173
1314
|
case "done": {
|
|
1174
|
-
if (currentMsg) closeCurrentMessage(event.stopReason ? undefined : "final_answer");
|
|
1315
|
+
if (currentMsg) closeCurrentMessage(isTruncatedStopReason(event.stopReason) ? undefined : "final_answer");
|
|
1175
1316
|
if (currentReasoning) closeCurrentReasoning();
|
|
1176
1317
|
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
1177
1318
|
flushHiddenRawReasoning();
|
|
@@ -86,6 +86,28 @@ export function resolveAutoContext(claudeCode: AutoContextConfigSlice | undefine
|
|
|
86
86
|
return { enabled: true, compactWindow };
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Config value -> ENABLE_TOOL_SEARCH wire value (#4838).
|
|
91
|
+
*
|
|
92
|
+
* Claude Code parses this variable itself and accepts more than a boolean:
|
|
93
|
+
* "true", "auto", "auto:N" (N is a percentage floor for non-deferred tools; its
|
|
94
|
+
* parser rejects anything under 100) and "force", which also overrides an
|
|
95
|
+
* OS-level managed-settings suppression. Passing a string through verbatim keeps
|
|
96
|
+
* that whole vocabulary reachable instead of flattening it to a boolean that
|
|
97
|
+
* would have to be re-expanded here later.
|
|
98
|
+
*
|
|
99
|
+
* `false`, absent and blank inject nothing rather than injecting "false". Every
|
|
100
|
+
* caller injects with user-wins semantics, so an operator's own export survives
|
|
101
|
+
* either way, and writing "false" would be the one path that could quietly
|
|
102
|
+
* disagree with it.
|
|
103
|
+
*/
|
|
104
|
+
export function claudeToolSearchEnv(value: boolean | string | undefined): string | undefined {
|
|
105
|
+
if (value === true) return "true";
|
|
106
|
+
if (typeof value !== "string") return undefined;
|
|
107
|
+
const trimmed = value.trim();
|
|
108
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
89
111
|
/**
|
|
90
112
|
* [1m]-marking predicate. Windows >= 1M always mark (CLI accounts exactly 1M).
|
|
91
113
|
* Auto-context additionally marks windows > 200k that can safely host the compact
|
package/src/claude/outbound.ts
CHANGED
|
@@ -189,7 +189,27 @@ function webSearchPairFromItem(item: Rec): { id: string; input: Rec; resultConte
|
|
|
189
189
|
return { id, input, resultContent, completed };
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
|
|
192
|
+
/**
|
|
193
|
+
* `inputTokenFloor` is this proxy's own count of the prompt it forwarded, used only when the
|
|
194
|
+
* upstream sent no confirmed usage before the first frame.
|
|
195
|
+
*
|
|
196
|
+
* Real Anthropic fills `message_start.message.usage.input_tokens` with the turn's prompt size,
|
|
197
|
+
* and third-party clients read it there — Paseo's context meter takes input from this frame and
|
|
198
|
+
* output from `message_delta`, so a hardcoded zero showed a nearly-empty ring for a turn whose
|
|
199
|
+
* `/context` reported ~97k (#4857). #4891 fixed the destinations that report usage up front;
|
|
200
|
+
* the internal bridge attaches `usage: null` to its lifecycle frames, so those paths had
|
|
201
|
+
* nothing to report and kept sending zero.
|
|
202
|
+
*
|
|
203
|
+
* A floor is a measurement, which is what makes it publishable here: it counts the prompt this
|
|
204
|
+
* proxy actually sent, the same estimate `claude-messages.ts` already trusts as a log floor. It
|
|
205
|
+
* is not a claim about upstream's tokenizer, and it is not final — `message_delta` carries the
|
|
206
|
+
* authoritative count for every reader that waits for it, exactly as before.
|
|
207
|
+
*/
|
|
208
|
+
function messageSnapshot(model: string, confirmedUsage?: Rec, inputTokenFloor?: number): Rec {
|
|
209
|
+
const usage = confirmedUsage
|
|
210
|
+
?? (typeof inputTokenFloor === "number" && Number.isFinite(inputTokenFloor) && inputTokenFloor > 0
|
|
211
|
+
? { input_tokens: Math.trunc(inputTokenFloor), output_tokens: 0 }
|
|
212
|
+
: { input_tokens: 0, output_tokens: 0 });
|
|
193
213
|
return {
|
|
194
214
|
id: `msg_${uuid()}`,
|
|
195
215
|
type: "message",
|
|
@@ -198,7 +218,7 @@ function messageSnapshot(model: string): Rec {
|
|
|
198
218
|
model,
|
|
199
219
|
stop_reason: null,
|
|
200
220
|
stop_sequence: null,
|
|
201
|
-
usage
|
|
221
|
+
usage,
|
|
202
222
|
};
|
|
203
223
|
}
|
|
204
224
|
|
|
@@ -228,7 +248,15 @@ interface OpenBlock {
|
|
|
228
248
|
export function responsesSseToAnthropicSse(
|
|
229
249
|
upstream: ReadableStream<Uint8Array>,
|
|
230
250
|
model: string,
|
|
231
|
-
opts: {
|
|
251
|
+
opts: {
|
|
252
|
+
pingIntervalMs?: number;
|
|
253
|
+
translatorBudget: TranslatorBudget;
|
|
254
|
+
/**
|
|
255
|
+
* This proxy's count of the prompt it forwarded, published on `message_start` only when the
|
|
256
|
+
* upstream sent no confirmed usage before the first frame. See `messageSnapshot` (#4857).
|
|
257
|
+
*/
|
|
258
|
+
inputTokenFloor?: number;
|
|
259
|
+
},
|
|
232
260
|
): ReadableStream<Uint8Array> {
|
|
233
261
|
const translatorBudget = opts.translatorBudget;
|
|
234
262
|
const pingIntervalMs = opts?.pingIntervalMs ?? 20_000;
|
|
@@ -246,6 +274,7 @@ export function responsesSseToAnthropicSse(
|
|
|
246
274
|
let open: OpenBlock | null = null;
|
|
247
275
|
let sawToolUse = false;
|
|
248
276
|
let webSearchRequests = 0;
|
|
277
|
+
let earlyAnthropicUsage: Rec | undefined;
|
|
249
278
|
let pingTimer: ReturnType<typeof setInterval> | undefined;
|
|
250
279
|
let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
|
|
251
280
|
const utf8SliceBytes = (value: string, start: number, end: number): number => {
|
|
@@ -282,7 +311,10 @@ export function responsesSseToAnthropicSse(
|
|
|
282
311
|
const ensureStarted = () => {
|
|
283
312
|
if (started) return;
|
|
284
313
|
started = true;
|
|
285
|
-
emit("message_start", {
|
|
314
|
+
emit("message_start", {
|
|
315
|
+
type: "message_start",
|
|
316
|
+
message: messageSnapshot(model, earlyAnthropicUsage, opts.inputTokenFloor),
|
|
317
|
+
});
|
|
286
318
|
emit("ping", { type: "ping" });
|
|
287
319
|
};
|
|
288
320
|
// Keepalive pings protect remote deployments behind LB/NAT idle timeouts even
|
|
@@ -405,8 +437,17 @@ export function responsesSseToAnthropicSse(
|
|
|
405
437
|
const handleFrame = (eventName: string, data: Rec) => {
|
|
406
438
|
switch (eventName) {
|
|
407
439
|
case "response.created":
|
|
408
|
-
|
|
440
|
+
case "response.in_progress": {
|
|
441
|
+
// Lifecycle preludes do not start Anthropic framing, but some upstreams attach
|
|
442
|
+
// confirmed input usage before semantic output. Retain only its bounded Anthropic
|
|
443
|
+
// projection so message_start can report measurements that already arrived.
|
|
444
|
+
const response = isRec(data.response) ? data.response : {};
|
|
445
|
+
const usage = isRec(response.usage) ? response.usage : undefined;
|
|
446
|
+
if (!started && usage && typeof usage.input_tokens === "number") {
|
|
447
|
+
earlyAnthropicUsage = anthropicUsage(usage);
|
|
448
|
+
}
|
|
409
449
|
break;
|
|
450
|
+
}
|
|
410
451
|
case "response.heartbeat":
|
|
411
452
|
if ((controller.desiredSize ?? 0) > 0) emit("ping", { type: "ping" });
|
|
412
453
|
break;
|
package/src/cli/account-api.ts
CHANGED
|
@@ -370,9 +370,10 @@ async function fetchOAuthRows(
|
|
|
370
370
|
email: a.email,
|
|
371
371
|
active: a.active ?? a.id === activeId,
|
|
372
372
|
needsReauth: a.needsReauth,
|
|
373
|
-
// Forward the server's answer verbatim
|
|
374
|
-
//
|
|
375
|
-
|
|
373
|
+
// Forward the server's answer verbatim. An absent key means the proxy predates tier
|
|
374
|
+
// reporting while `null` means it checked and found no tier — collapsing either
|
|
375
|
+
// direction would destroy the one distinction this field exists to make.
|
|
376
|
+
...(Object.hasOwn(a, "plan") ? { plan: a.plan } : {}),
|
|
376
377
|
...(a.quota !== undefined ? { quota: a.quota } : {}),
|
|
377
378
|
...(a.quotaUnavailable !== undefined ? { quotaUnavailable: a.quotaUnavailable } : {}),
|
|
378
379
|
...(a.quotaUnavailable === true && parseQuotaFailureCode(a.quotaFailure)
|