@bitkyc08/opencodex 2.58.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 +1 -1
- package/src/adapters/base.ts +11 -1
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/effort-map.ts +16 -2
- package/src/adapters/cursor/envelope-echo.ts +55 -2
- package/src/adapters/cursor/message-mapper.ts +3 -2
- package/src/adapters/cursor/protobuf-request.ts +8 -5
- package/src/adapters/cursor/request-builder.ts +14 -3
- package/src/adapters/cursor/thread-continuity.ts +105 -31
- package/src/adapters/cursor/tool-guidance.ts +5 -4
- package/src/adapters/cursor.ts +42 -1
- 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 +8 -1
- package/src/adapters/google.ts +23 -4
- 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 +10 -1
- package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
- package/src/adapters/openai-responses/tool-schema.ts +19 -7
- package/src/adapters/responses-tool-schema.ts +76 -46
- package/src/adapters/run-turn-queue.ts +17 -4
- package/src/bridge/response-json.ts +1 -1
- package/src/bridge/sse.ts +165 -24
- package/src/claude/context-windows.ts +22 -0
- package/src/claude/outbound.ts +35 -4
- 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/dispatch.ts +54 -0
- package/src/cli/doctor.ts +197 -2
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +88 -20
- 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 +2 -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 +16 -12
- 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/history-provider.ts +4 -1
- package/src/codex/history-state-open.ts +105 -0
- package/src/codex/inject/config-toml.ts +44 -2
- package/src/codex/inject.ts +3 -2
- package/src/codex/lineage.ts +83 -32
- package/src/codex/loopback-target.ts +31 -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/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/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 +21 -0
- package/src/config/schema/leaf-validators.ts +64 -17
- package/src/generated/compatibility-version.json +235 -163
- package/src/generated/model-metadata.ts +1 -1
- package/src/lib/bounded-body.ts +4 -2
- 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/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/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-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-extended.ts +96 -0
- package/src/providers/registry/model-seeds.ts +78 -21
- 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/server/auth-cors.ts +26 -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.ts +15 -19
- package/src/server/management/api-access.ts +3 -4
- package/src/server/management/config-routes.ts +31 -6
- package/src/server/management/provider-capability-config.ts +35 -7
- package/src/server/management/provider-routes.ts +70 -18
- 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/compact.ts +60 -11
- package/src/server/responses/core-codex-account.ts +83 -22
- package/src/server/responses/core-normalize.ts +12 -5
- package/src/server/responses/fetch-helpers.ts +68 -2
- package/src/server/responses/passthrough-delivery.ts +10 -1
- package/src/server/responses/passthrough-dispatch.ts +113 -48
- package/src/server/responses/passthrough-execution.ts +11 -1
- package/src/server/responses/request-prepare.ts +29 -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 +2 -1
- 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/system-env-shell.ts +5 -1
- package/src/server/system-env.ts +7 -1
- package/src/server/workflow-refusal.ts +56 -2
- package/src/service/cli.ts +16 -6
- 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 +100 -3
- package/src/types/provider.ts +19 -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-BbrHOIY0.js +0 -128
package/src/service/health.ts
CHANGED
|
@@ -148,6 +148,19 @@ export async function confirmServiceServing(
|
|
|
148
148
|
return { ok: false, port };
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
/**
|
|
152
|
+
* The operation, as a noun, for a sentence that has to say it did not complete.
|
|
153
|
+
*
|
|
154
|
+
* The verb form reads as an accomplished fact ("service repaired"), which is exactly the
|
|
155
|
+
* claim that must not be made when the operation threw.
|
|
156
|
+
*/
|
|
157
|
+
const SERVICE_OPERATION_NOUN = {
|
|
158
|
+
installed: "install",
|
|
159
|
+
started: "start",
|
|
160
|
+
repaired: "repair",
|
|
161
|
+
restarted: "restart",
|
|
162
|
+
} as const;
|
|
163
|
+
|
|
151
164
|
/**
|
|
152
165
|
* Print the outcome of `install` / `start` / `repair` in terms of what the user cares
|
|
153
166
|
* about — is it serving? — instead of whether the manager accepted the registration.
|
|
@@ -156,10 +169,18 @@ export async function confirmServiceServing(
|
|
|
156
169
|
* worker reads the child's exit status, so a registered-but-silent service now makes it
|
|
157
170
|
* fall back to a direct proxy start rather than reporting a successful update over a
|
|
158
171
|
* dead port.
|
|
172
|
+
*
|
|
173
|
+
* `precedingFailure` is the error the operation itself threw, when it threw. The serving
|
|
174
|
+
* probe still runs — a rollback or a preserve/restart protocol may well have left the
|
|
175
|
+
* previous job answering, and the operator needs that half of the answer (#4236). But the
|
|
176
|
+
* two halves have to be ONE sentence. Printing the failure separately and then reaching
|
|
177
|
+
* the success line here reported both outcomes for the same run and credited work that did
|
|
178
|
+
* not happen: the registration was kept, not repaired (#4914).
|
|
159
179
|
*/
|
|
160
180
|
export async function reportServiceServing(
|
|
161
181
|
verb: "installed" | "started" | "repaired" | "restarted",
|
|
162
182
|
deps: Parameters<typeof confirmServiceServing>[0] = {},
|
|
183
|
+
precedingFailure?: unknown,
|
|
163
184
|
): Promise<void> {
|
|
164
185
|
const healthBudgetMs = deps.timeoutMs ?? serviceInstallHealthMs();
|
|
165
186
|
// Timed here rather than reported from the budget. confirmServiceServing knocks once
|
|
@@ -171,6 +192,28 @@ export async function reportServiceServing(
|
|
|
171
192
|
const startedAt = now();
|
|
172
193
|
const serving = await confirmServiceServing({ ...deps, timeoutMs: healthBudgetMs });
|
|
173
194
|
const waitedMs = Math.max(0, now() - startedAt);
|
|
195
|
+
const failureDetail = precedingFailure === undefined
|
|
196
|
+
? null
|
|
197
|
+
: precedingFailure instanceof Error ? precedingFailure.message : String(precedingFailure);
|
|
198
|
+
const operation = SERVICE_OPERATION_NOUN[verb];
|
|
199
|
+
if (failureDetail !== null) {
|
|
200
|
+
// Serving or not, the operation did not complete, so neither branch may print a
|
|
201
|
+
// checkmark. What differs is whether anything is answering, which is the fact the
|
|
202
|
+
// operator acts on next.
|
|
203
|
+
console.error(
|
|
204
|
+
serving.ok
|
|
205
|
+
? `⚠️ Service ${operation} did not complete: ${failureDetail}\n`
|
|
206
|
+
+ ` A proxy is answering on port ${serving.port}, so the existing registration was kept rather than replaced.\n`
|
|
207
|
+
+ ` The ${operation} did not take effect; rerun it once the reported cause is resolved.\n`
|
|
208
|
+
+ ` Log: ${serviceLogPath()}`
|
|
209
|
+
: `❌ Service ${operation} failed: ${failureDetail}\n`
|
|
210
|
+
+ ` No proxy answered on port ${serving.port} after ${Math.round(waitedMs / 1000)}s either.\n`
|
|
211
|
+
+ ` Log: ${serviceLogPath()}\n`
|
|
212
|
+
+ ` Meanwhile: ocx start (serves in the foreground)`,
|
|
213
|
+
);
|
|
214
|
+
process.exitCode = 1;
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
174
217
|
if (serving.ok) {
|
|
175
218
|
console.log(`✅ opencodex service ${verb} and serving on port ${serving.port}.`);
|
|
176
219
|
return;
|
package/src/service/state.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { delimiter, dirname, isAbsolute, join, posix, resolve, win32 } from "node:path";
|
|
4
4
|
import { expandUserPath, getConfigDir } from "../config";
|
|
5
5
|
import { resolveCodexHomeDir, type CodexHomeDeps } from "../codex/home";
|
|
6
|
+
import { resolveCodexSqliteHome } from "../codex/paths";
|
|
6
7
|
import { durableBunRuntime, type BunRuntimeSource, type DurableBunRuntime } from "../lib/bun-runtime";
|
|
7
8
|
import { WINSW_SHA256, WINSW_VERSION } from "../lib/winsw";
|
|
8
9
|
import { hardenSecretPath } from "../lib/windows-secret-acl";
|
|
@@ -218,6 +219,8 @@ export interface ServiceInstallState {
|
|
|
218
219
|
version: 1 | 2;
|
|
219
220
|
codexHome: string;
|
|
220
221
|
opencodexHome: string;
|
|
222
|
+
/** Effective Codex SQLite home used by this service's history integration. */
|
|
223
|
+
codexSqliteHome?: string;
|
|
221
224
|
/** Baked at install; lets status flag paths gone stale after npm prefix/nvm moves. */
|
|
222
225
|
bunPath?: string;
|
|
223
226
|
cliPath?: string;
|
|
@@ -241,7 +244,7 @@ export function parseServiceInstallState(value: unknown): ServiceInstallState |
|
|
|
241
244
|
if (state.version !== 1 && state.version !== 2) return null;
|
|
242
245
|
if (typeof state.codexHome !== "string" || state.codexHome.length === 0) return null;
|
|
243
246
|
if (typeof state.opencodexHome !== "string" || state.opencodexHome.length === 0) return null;
|
|
244
|
-
for (const key of ["bunPath", "cliPath", "launcherPath", "winswVersion", "winswSha256"] as const) {
|
|
247
|
+
for (const key of ["codexSqliteHome", "bunPath", "cliPath", "launcherPath", "winswVersion", "winswSha256"] as const) {
|
|
245
248
|
if (state[key] !== undefined && (typeof state[key] !== "string" || state[key].length === 0)) return null;
|
|
246
249
|
}
|
|
247
250
|
if (state.version === 1) {
|
|
@@ -254,10 +257,12 @@ export function parseServiceInstallState(value: unknown): ServiceInstallState |
|
|
|
254
257
|
|
|
255
258
|
export function writeServiceInstallState(backend: ServiceBackend = "scheduler", launcherPath?: string | null): void {
|
|
256
259
|
const { bun, cli } = cliEntry();
|
|
260
|
+
const codexHome = currentCodexHome();
|
|
257
261
|
const state: ServiceInstallState = {
|
|
258
262
|
version: 2,
|
|
259
|
-
codexHome
|
|
263
|
+
codexHome,
|
|
260
264
|
opencodexHome: currentOpenCodexHome(),
|
|
265
|
+
codexSqliteHome: resolveCodexSqliteHome({ codexHome }),
|
|
261
266
|
bunPath: bun,
|
|
262
267
|
cliPath: cli,
|
|
263
268
|
...(launcherPath ? { launcherPath } : {}),
|
package/src/types/accounts.ts
CHANGED
|
@@ -19,6 +19,10 @@ export interface CodexAccount {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export interface CodexAccountCredentials {
|
|
22
|
+
/** Read-only Orca auth.json link; Orca exclusively owns OAuth refresh. */
|
|
23
|
+
sourceAuthPath?: string;
|
|
24
|
+
/** Bind the link to the originally imported OAuth subject. */
|
|
25
|
+
sourceSubject?: string;
|
|
22
26
|
accessToken: string;
|
|
23
27
|
refreshToken: string;
|
|
24
28
|
expiresAt: number;
|
package/src/types/config.ts
CHANGED
|
@@ -93,6 +93,31 @@ export interface OcxClaudeCodeConfig {
|
|
|
93
93
|
* forcing effort on every request can leak reasoning params to non-reasoning routes.
|
|
94
94
|
*/
|
|
95
95
|
alwaysEnableEffort?: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Opt-in ENABLE_TOOL_SEARCH injection for launched Claude Code sessions (#4838).
|
|
98
|
+
*
|
|
99
|
+
* Claude Code turns MCP tool deferral off whenever ANTHROPIC_BASE_URL names a
|
|
100
|
+
* non-first-party host, so an `ocx claude` session ships every tool schema in
|
|
101
|
+
* full on every request. Its own diagnostic states the precondition for turning
|
|
102
|
+
* that back on: "Set ENABLE_TOOL_SEARCH=true (or auto / auto:N) if your proxy
|
|
103
|
+
* forwards tool_reference blocks."
|
|
104
|
+
*
|
|
105
|
+
* Default OFF, because opencodex only forwards them on the NATIVE ANTHROPIC
|
|
106
|
+
* PASSTHROUGH route, where the body reaches Anthropic untouched. On a translated
|
|
107
|
+
* route the deferral shape is not representable: compatibility.ts marks
|
|
108
|
+
* tool_search/tool_reference/deferred_tools unsupported, and toolsToResponses
|
|
109
|
+
* drops the tool_search server tool while ignoring defer_loading — deferred tools
|
|
110
|
+
* still carry input_schema on the wire, so the routed provider receives every
|
|
111
|
+
* schema anyway while Claude Code stops accounting for them and therefore stops
|
|
112
|
+
* compacting. Under `claudeCode.compatibility: "enforce"` the same request is
|
|
113
|
+
* rejected with 400 instead.
|
|
114
|
+
*
|
|
115
|
+
* `true` injects "true"; a string is passed through verbatim so Claude Code's own
|
|
116
|
+
* vocabulary (`auto`, `auto:N`, `force`) stays reachable. `false` and absent
|
|
117
|
+
* inject nothing — they do not force the variable off, because a value the
|
|
118
|
+
* operator exported themselves always wins.
|
|
119
|
+
*/
|
|
120
|
+
toolSearch?: boolean | string;
|
|
96
121
|
/**
|
|
97
122
|
* Subagent tier slots (devlog 260712 B2): injected as ANTHROPIC_DEFAULT_*_MODEL so
|
|
98
123
|
* Claude Code's Agent-tool aliases (opus/sonnet/haiku/fable + parent-inherit) route
|
|
@@ -722,9 +747,11 @@ export interface OcxConfig {
|
|
|
722
747
|
| { enabled: false }
|
|
723
748
|
| { enabled: true; port?: number };
|
|
724
749
|
/**
|
|
725
|
-
* Outbound
|
|
726
|
-
* "${HTTPS_PROXY}"
|
|
727
|
-
*
|
|
750
|
+
* Outbound proxy URL for provider requests. HTTP(S) example: "http://user:pass@proxy:8080"
|
|
751
|
+
* or "${HTTPS_PROXY}". SOCKS5 example: "socks5://127.0.0.1:10808" (`ocx start --socks5`).
|
|
752
|
+
* HTTP URLs are mirrored into HTTP_PROXY/HTTPS_PROXY when unset. SOCKS5 URLs are mirrored
|
|
753
|
+
* into ALL_PROXY, clear inherited HTTP(S)_PROXY, and use OpenCodex's SOCKS5 transport.
|
|
754
|
+
* Loopback stays in NO_PROXY.
|
|
728
755
|
* The literal `"auto"` reads the Windows WinINET static proxy (`ProxyEnable`/`ProxyServer`)
|
|
729
756
|
* once at process start; on other platforms, or when the system proxy is off, SOCKS-only,
|
|
730
757
|
* or unreadable, it degrades to direct egress with one log line (#1525). PAC/WPAD and live
|
|
@@ -778,6 +805,20 @@ export interface OcxConfig {
|
|
|
778
805
|
* built-in `openai` provider, so Codex does not select native remote compaction. Default off.
|
|
779
806
|
*/
|
|
780
807
|
codexClientCompaction?: boolean;
|
|
808
|
+
/**
|
|
809
|
+
* Label Codex shows for the injected `opencodex` provider. Defaults to `OpenCodex Proxy`.
|
|
810
|
+
*
|
|
811
|
+
* Presentation only. Routing is keyed on the provider id `opencodex` — the root
|
|
812
|
+
* `model_provider = "opencodex"` line and the `[model_providers.opencodex]` header — and this
|
|
813
|
+
* setting never touches either, so renaming the label cannot reroute or orphan a thread whose
|
|
814
|
+
* row already names that id.
|
|
815
|
+
*
|
|
816
|
+
* There is no way to emit an empty label: Codex rejects a provider with no name, so a blank,
|
|
817
|
+
* over-long, or control-character value falls back to the default rather than writing a config
|
|
818
|
+
* Codex would refuse to load. "Suppressing" the OpenCodex branding therefore means choosing a
|
|
819
|
+
* neutral label, not removing the field.
|
|
820
|
+
*/
|
|
821
|
+
codexProviderDisplayName?: string;
|
|
781
822
|
/**
|
|
782
823
|
* Compatibility mode: temporarily rewrite Codex resume-history metadata while the proxy is active
|
|
783
824
|
* so Codex App can show old OpenAI chats and opencodex-created exec chats under its default
|
|
@@ -811,6 +852,16 @@ export interface OcxConfig {
|
|
|
811
852
|
* absence is the only default state this policy has.
|
|
812
853
|
*/
|
|
813
854
|
codexPool?: OcxCodexPoolConfig;
|
|
855
|
+
/**
|
|
856
|
+
* Durable token ceilings for the spend-reservation ledger (#4546). Absent means the
|
|
857
|
+
* historical behaviour exactly: token spend is still accounted and journalled, and nothing
|
|
858
|
+
* is refused on it.
|
|
859
|
+
*
|
|
860
|
+
* Not in `getDefaultConfig()` on purpose, and deliberately shipped with no default figure.
|
|
861
|
+
* The ledger is on by default, so a default ceiling would start refusing real traffic on
|
|
862
|
+
* upgrade against a number nobody chose.
|
|
863
|
+
*/
|
|
864
|
+
spend?: OcxSpendConfig;
|
|
814
865
|
/** Opt-in per-account activation of newly reset Codex quota windows. */
|
|
815
866
|
codexQuotaAutoRefresh?: Record<string, {
|
|
816
867
|
fiveHour?: boolean;
|
|
@@ -1385,3 +1436,49 @@ export interface OcxCatalogAutoRefreshConfig {
|
|
|
1385
1436
|
*/
|
|
1386
1437
|
intervalMinutes?: number;
|
|
1387
1438
|
}
|
|
1439
|
+
|
|
1440
|
+
/**
|
|
1441
|
+
* One scope's token ceiling.
|
|
1442
|
+
*
|
|
1443
|
+
* An object rather than a bare number because the ledger's scope limit is already a record in
|
|
1444
|
+
* `SpendReservationPolicy`, and a config shape that mirrors the runtime one cannot drift from
|
|
1445
|
+
* it silently. Absent `maxTokens` is the same as an absent scope: observe only.
|
|
1446
|
+
*/
|
|
1447
|
+
export interface OcxSpendScopeConfig {
|
|
1448
|
+
/**
|
|
1449
|
+
* Tokens the scope may hold at once, counting settled spend, open reservations and
|
|
1450
|
+
* unresolved spend. A reservation is the request's whole input plus its enforceable output
|
|
1451
|
+
* ceiling, so this is compared against a number that assumes every cached prefix misses.
|
|
1452
|
+
*
|
|
1453
|
+
* There is no default. A ceiling is a number only the operator knows -- it depends on the
|
|
1454
|
+
* plan, the account roster and what the install is for -- and the recorded lesson from the
|
|
1455
|
+
* observational phase of #4546 is that guessing one is worse than shipping none.
|
|
1456
|
+
*/
|
|
1457
|
+
maxTokens?: number;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
/**
|
|
1461
|
+
* Durable spend ceilings (#4546).
|
|
1462
|
+
*
|
|
1463
|
+
* The three scopes intersect: a request is admitted only when its own root workflow, the
|
|
1464
|
+
* identity that would serve it, and the pool it would draw from all have room. That is what
|
|
1465
|
+
* makes the ceiling hold against a caller that mints a fresh root id per request -- the root
|
|
1466
|
+
* is new, the identity and pool are not.
|
|
1467
|
+
*
|
|
1468
|
+
* Every field is optional and an empty section is the same as no section at all.
|
|
1469
|
+
*/
|
|
1470
|
+
export interface OcxSpendConfig {
|
|
1471
|
+
/** Ceiling for one root workflow -- the user-visible task, including its whole fan-out. */
|
|
1472
|
+
root?: OcxSpendScopeConfig;
|
|
1473
|
+
/** Ceiling for one authenticated identity, across every root it serves. */
|
|
1474
|
+
identity?: OcxSpendScopeConfig;
|
|
1475
|
+
/** Ceiling for one account pool, across every identity in it. */
|
|
1476
|
+
pool?: OcxSpendScopeConfig;
|
|
1477
|
+
/**
|
|
1478
|
+
* Days a dormant scope's accounting is retained. Default 7.
|
|
1479
|
+
*
|
|
1480
|
+
* A scope is dropped only when it is both idle and under its ceiling, so shortening this
|
|
1481
|
+
* cannot hand an exhausted scope a fresh allowance.
|
|
1482
|
+
*/
|
|
1483
|
+
retentionDays?: number;
|
|
1484
|
+
}
|
package/src/types/provider.ts
CHANGED
|
@@ -618,6 +618,8 @@ export interface OcxProviderConfig {
|
|
|
618
618
|
reasoningEfforts?: string[];
|
|
619
619
|
/** Model-specific Codex-visible reasoning tiers. An empty array means “do not expose effort”. */
|
|
620
620
|
modelReasoningEfforts?: Record<string, string[]>;
|
|
621
|
+
/** Catalog-only: do not synthesize a missing max rung for matching routed models. */
|
|
622
|
+
modelSuppressSyntheticMax?: Record<string, boolean>;
|
|
621
623
|
/** Model-specific default Codex reasoning tier; must also be present in the visible tier list. */
|
|
622
624
|
modelDefaultReasoningEfforts?: Record<string, string>;
|
|
623
625
|
/** Operator-owned effort override; none omits effort and uses the provider default. */
|
|
@@ -677,6 +679,23 @@ export interface OcxProviderConfig {
|
|
|
677
679
|
* apply_patch passthrough compatibility for OpenAI and unclassified gateways.
|
|
678
680
|
*/
|
|
679
681
|
supportsResponsesCustomTools?: boolean;
|
|
682
|
+
/**
|
|
683
|
+
* Hosted tool declarations this Responses destination rejects, so they are stripped from
|
|
684
|
+
* the request instead of being forwarded and 400'd.
|
|
685
|
+
*
|
|
686
|
+
* This is how an OpenAI-compatible gateway with a narrower capability set than OpenAI
|
|
687
|
+
* describes itself. Before it existed, a destination that accepted plain Responses and
|
|
688
|
+
* `function` tools but rejected hosted `web_search` could only be handled by adding a
|
|
689
|
+
* hard-coded baseUrl rule to `src/responses/hosted-tool-policy.ts`, so every such gateway
|
|
690
|
+
* needed a proxy release; a text-only prompt like "Reply exactly with OK" failed before
|
|
691
|
+
* the model answered because the hosted declaration travelled with it (#5002).
|
|
692
|
+
*
|
|
693
|
+
* Values come from `DECLARABLE_HOSTED_TOOL_TYPES`. Spelling variants of one capability
|
|
694
|
+
* are aliased, so `["web_search"]` also denies `web_search_preview`. Pair this with
|
|
695
|
+
* `supportsResponsesCustomTools: false` for a gateway that also rejects native custom
|
|
696
|
+
* tools; the two capabilities are independent and denied independently.
|
|
697
|
+
*/
|
|
698
|
+
unsupportedHostedTools?: string[];
|
|
680
699
|
/**
|
|
681
700
|
* Provider-local repair for Responses gateways whose lifecycle snapshots omit canonical
|
|
682
701
|
* fields or closing events (#893). Disabled by default and applied only to client-facing
|
package/src/types/request.ts
CHANGED
|
@@ -68,6 +68,12 @@ export interface OcxParsedRequest {
|
|
|
68
68
|
_cursorConversationId?: string;
|
|
69
69
|
/** Stable upstream client thread identity, used only to derive provider-scoped continuation ids. */
|
|
70
70
|
_clientThreadId?: string;
|
|
71
|
+
/**
|
|
72
|
+
* This request's OWN Codex thread id (`thread-id`), as opposed to `_clientThreadId`, which
|
|
73
|
+
* carries `x-codex-parent-thread-id` and is therefore shared by every parallel child of one
|
|
74
|
+
* parent. Only a surface that must distinguish siblings should read it.
|
|
75
|
+
*/
|
|
76
|
+
_codexOwnThreadId?: string;
|
|
71
77
|
/** True when promptCacheKey identifies a shared cache cohort rather than one conversation. */
|
|
72
78
|
_promptCacheKeyIsSharedCohort?: boolean;
|
|
73
79
|
/** Cursor-only thread owner; may be an opaque process-local Desktop session/thread identity. */
|
|
@@ -312,7 +318,7 @@ export interface OcxProviderContinuationState {
|
|
|
312
318
|
}
|
|
313
319
|
|
|
314
320
|
export type AdapterEvent =
|
|
315
|
-
| { type: "heartbeat" }
|
|
321
|
+
| { type: "heartbeat"; replayUnsafe?: true }
|
|
316
322
|
| { type: "text_delta"; text: string; phase?: OcxMessagePhase }
|
|
317
323
|
| { type: "thinking_delta"; thinking: string }
|
|
318
324
|
// Anthropic extended-thinking round-trip: signature_delta for the current thinking block, and
|
package/src/types/wire.ts
CHANGED
|
@@ -46,7 +46,15 @@ export const MODEL_ADAPTER_OVERRIDE_ALLOWED: ReadonlySet<string> = new Set([
|
|
|
46
46
|
* Anthropic for these models.
|
|
47
47
|
*/
|
|
48
48
|
const ANTHROPIC_WIRE_MODELS: Record<string, ReadonlySet<string>> = {
|
|
49
|
-
"opencode-go": new Set([
|
|
49
|
+
"opencode-go": new Set([
|
|
50
|
+
"minimax-m2.5",
|
|
51
|
+
"minimax-m2.7",
|
|
52
|
+
"minimax-m3",
|
|
53
|
+
// OpenCode's catalog identifies Union Alpha as @ai-sdk/anthropic while the
|
|
54
|
+
// provider defaults to OpenAI-compatible; direct Chat returns 500 and direct
|
|
55
|
+
// Messages reaches the session check (#4847).
|
|
56
|
+
"union-alpha",
|
|
57
|
+
]),
|
|
50
58
|
};
|
|
51
59
|
|
|
52
60
|
function anthropicWireModelsForProvider(providerName: string): ReadonlySet<string> | undefined {
|
|
@@ -124,6 +124,16 @@ const META_MUSE_SPARK_13_CONTRIBUTOR: Cost4 = { input: 0.1, output: 0.2, cacheRe
|
|
|
124
124
|
const META_SPARK_SOURCE = `Meta Model API published price ${META_MODEL_PRICING}`;
|
|
125
125
|
const META_SPARK_CONTRIBUTOR_SOURCE = `Meta Model API published Contributor-tier price ${META_MODEL_PRICING}; data-sharing discount tier`;
|
|
126
126
|
const DEEPSEEK_PRICING = "https://api-docs.deepseek.com/quick_start/pricing-details-usd; V4 Flash alias transition scheduled 2026-07-24 — re-verify after";
|
|
127
|
+
/*
|
|
128
|
+
* DeepSeek V4.1-Flash list prices (USD / 1M tokens), verified 2026-09-17 against
|
|
129
|
+
* https://api-docs.deepseek.com/quick_start/pricing. The page prices a peak window
|
|
130
|
+
* (09:30-24:00 Beijing) and an off-peak window; the tuple below is the peak-window
|
|
131
|
+
* list rate and the off-peak discount (0.15 / 0.60, cache-hit 0.003) is deliberately
|
|
132
|
+
* not baked in — the same rule as the Devin time-boxed promos. cacheWrite=0 follows
|
|
133
|
+
* the existing deepseek-chat / deepseek-reasoner rows: DeepSeek publishes no
|
|
134
|
+
* cache-write charge.
|
|
135
|
+
*/
|
|
136
|
+
const DEEPSEEK_V41_FLASH: Cost4 = { input: 0.3, output: 1.2, cacheRead: 0.006, cacheWrite: 0 };
|
|
127
137
|
// Kimi official tables publish input/output/cache-hit only; cacheWrite is mapped to the
|
|
128
138
|
// cache-miss input price (Kimi auto-caches with no separate write billing). 2026-07-20 re-verified.
|
|
129
139
|
const KIMI_PRICING = "https://platform.kimi.ai/docs/pricing (official table; cacheWrite derived = input, Kimi auto-cache has no write billing)";
|
|
@@ -142,6 +152,14 @@ const BIGMODEL_NOTE = "z.ai international list price shown as estimate; domestic
|
|
|
142
152
|
// anywhere. Cache stays 0 rather than inheriting the reseller's 0.15 — a reseller number
|
|
143
153
|
// under a vendor-price label would be a wrong value wearing a verified badge.
|
|
144
154
|
const QWEN38_MAX_PRICING = "https://qwen.ai/blog?id=qwen3.8 (Qwen release announcement; no Model Studio billing row yet; cache rates unpublished -> 0)";
|
|
155
|
+
// Qwen-published qwen3.8-flash rate ($0.16 in / $0.47 out per 1M tokens) via the
|
|
156
|
+
// Qwen3.8 release announcement, corroborated by API-vendor price tables. No cache
|
|
157
|
+
// rate is published anywhere, so cache stays 0 rather than borrowing a reseller's
|
|
158
|
+
// number — the same hold QWEN38_MAX takes. The announcement marks API availability
|
|
159
|
+
// as coming soon, but the id is already served (and logged) on OpenCode Go, so the
|
|
160
|
+
// estimate applies to real usage rows now.
|
|
161
|
+
const QWEN38_FLASH: Cost4 = { input: 0.16, output: 0.47, cacheRead: 0, cacheWrite: 0 };
|
|
162
|
+
const QWEN38_FLASH_PRICING = "https://qwen.ai/blog?id=qwen3.8-2026 (Qwen release announcement; API marked coming soon at announcement; cache rates unpublished -> 0; input/output corroborated by https://docs.b.ai/guides/models/qwen/qwen3.8-flash)";
|
|
145
163
|
|
|
146
164
|
/*
|
|
147
165
|
* Cognition/Devin list prices (USD / 1M tokens), verified 2026-09-13 against the
|
|
@@ -289,6 +307,16 @@ export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [
|
|
|
289
307
|
// for what that source does and does not cover.
|
|
290
308
|
{ provider: "alibaba-token-plan", modelId: "qwen3.8-max", cost4: QWEN38_MAX, source: QWEN38_MAX_PRICING, verifiedAt: "2026-08-04", status: "verified" },
|
|
291
309
|
{ provider: "alibaba-token-plan-intl", modelId: "qwen3.8-max", cost4: QWEN38_MAX, source: QWEN38_MAX_PRICING, verifiedAt: "2026-08-04", status: "verified" },
|
|
310
|
+
// OpenCode Go — five served ids with no jawcode bundle row and no vendor-level
|
|
311
|
+
// fallback (the fallback only searches jawcode metadata, never overlays), so the
|
|
312
|
+
// Usage estimated-cost column and the per-model breakdown rendered an em dash for
|
|
313
|
+
// every request through them. Each row reuses the vendor's own published list
|
|
314
|
+
// price as an estimate: Go itself is subscription-billed, hence verified-derived.
|
|
315
|
+
{ provider: "opencode-go", modelId: "qwen3.8-max", cost4: QWEN38_MAX, source: `vendor list price applied to the OpenCode Go surface; ${QWEN38_MAX_PRICING}`, verifiedAt: "2026-09-17", status: "verified-derived" },
|
|
316
|
+
{ provider: "opencode-go", modelId: "qwen3.8-flash", cost4: QWEN38_FLASH, source: `vendor list price applied to the OpenCode Go surface; ${QWEN38_FLASH_PRICING}`, verifiedAt: "2026-09-17", status: "verified-derived" },
|
|
317
|
+
{ provider: "opencode-go", modelId: "deepseek-v4.1-flash", cost4: DEEPSEEK_V41_FLASH, source: `peak-window list rate applied to the OpenCode Go surface (off-peak 0.15/0.60 + cache-hit 0.003 not baked in); ${DEEPSEEK_PRICING}`, verifiedAt: "2026-09-17", status: "verified-derived" },
|
|
318
|
+
{ provider: "opencode-go", modelId: "glm-5.3-flash", cost4: GLM_53_FLASH, source: `z.ai list price applied to the OpenCode Go surface as an estimate; ${ZAI_PRICING}`, verifiedAt: "2026-09-17", status: "verified-derived" },
|
|
319
|
+
{ provider: "opencode-go", modelId: "muse-spark-1.3-contributor", cost4: META_MUSE_SPARK_13_CONTRIBUTOR, source: `Meta Model API Contributor-tier price applied to the OpenCode Go surface as an estimate; ${META_SPARK_CONTRIBUTOR_SOURCE}`, verifiedAt: "2026-09-17", status: "verified-derived" },
|
|
292
320
|
// Cursor Auto router — Cursor's published fixed token price (verified).
|
|
293
321
|
{ provider: "cursor", modelId: "auto", cost4: { input: 1.25, output: 6, cacheRead: 0.25, cacheWrite: 1.25 }, source: "https://docs.cursor.com/account/pricing + https://cursor.com/blog/aug-2025-pricing", verifiedAt: "2026-07-20", status: "verified" },
|
|
294
322
|
// Z.AI GLM family — the zai bundle's rows are all-zero upstream, and the four
|
package/src/usage/log.ts
CHANGED
|
@@ -77,6 +77,26 @@ export type AttemptRecoveryKind =
|
|
|
77
77
|
| "empty-completion"
|
|
78
78
|
| "reasoning-effort-downgrade";
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Why a recovery this request was otherwise willing to make did not happen.
|
|
82
|
+
*
|
|
83
|
+
* Recorded separately from `recoveryKinds` and from `sendCount`, because the question it
|
|
84
|
+
* answers is different from either. A log showing one physical send and no recovery kind used
|
|
85
|
+
* to be ambiguous: it could mean nothing was eligible, or that something was eligible and the
|
|
86
|
+
* send budget withheld it. Those need opposite follow-ups, and the second one was invisible
|
|
87
|
+
* (#5044).
|
|
88
|
+
*
|
|
89
|
+
* `sendCount` deliberately does not move for these. A refused attempt is not a physical send,
|
|
90
|
+
* and inflating the count to signal the refusal would corrupt the one number that means
|
|
91
|
+
* "requests this proxy actually made".
|
|
92
|
+
*
|
|
93
|
+
* Bounded vocabulary on purpose: it is a wire value a maintainer reads, never a credential, an
|
|
94
|
+
* account id, an upstream body, prompt content, or exception text.
|
|
95
|
+
*/
|
|
96
|
+
export type AttemptRecoveryWithheld =
|
|
97
|
+
| "retry-send-budget"
|
|
98
|
+
| "rotation-send-budget";
|
|
99
|
+
|
|
80
100
|
/** Request-time upstream credential class, never a credential or account identifier. */
|
|
81
101
|
export type UsageCredentialSource = "grok-oauth" | "xai-api-key";
|
|
82
102
|
|
|
@@ -139,6 +159,11 @@ export interface PersistedUsageAttempt {
|
|
|
139
159
|
firstOutputMs?: number;
|
|
140
160
|
sendCount: number;
|
|
141
161
|
recoveryKinds: AttemptRecoveryKind[];
|
|
162
|
+
/**
|
|
163
|
+
* Recoveries this attempt was eligible for and did not make. Absent on ordinary attempts so
|
|
164
|
+
* old rows keep their exact shape.
|
|
165
|
+
*/
|
|
166
|
+
recoveryWithheld?: AttemptRecoveryWithheld[];
|
|
142
167
|
usageStatus: UsageStatus;
|
|
143
168
|
/**
|
|
144
169
|
* True when the proxy answered this turn locally and issued no upstream request. It travels on
|
|
@@ -487,6 +512,10 @@ const ATTEMPT_RECOVERY_KINDS = new Set<AttemptRecoveryKind>([
|
|
|
487
512
|
"empty-completion",
|
|
488
513
|
"reasoning-effort-downgrade",
|
|
489
514
|
]);
|
|
515
|
+
const ATTEMPT_RECOVERY_WITHHELD = new Set<AttemptRecoveryWithheld>([
|
|
516
|
+
"retry-send-budget",
|
|
517
|
+
"rotation-send-budget",
|
|
518
|
+
]);
|
|
490
519
|
const USAGE_STATUSES = new Set<UsageStatus>([
|
|
491
520
|
"reported",
|
|
492
521
|
"unreported",
|
|
@@ -620,6 +649,14 @@ function normalizeUsageAttempt(raw: unknown): PersistedUsageAttempt | null {
|
|
|
620
649
|
&& ATTEMPT_RECOVERY_KINDS.has(value as AttemptRecoveryKind),
|
|
621
650
|
))]
|
|
622
651
|
: [];
|
|
652
|
+
// Same shape as `recoveryKinds`: unknown values are dropped rather than failing the row, so a
|
|
653
|
+
// log written by a newer build stays readable by an older one.
|
|
654
|
+
const recoveryWithheld = Array.isArray(attempt.recoveryWithheld)
|
|
655
|
+
? [...new Set(attempt.recoveryWithheld.filter(
|
|
656
|
+
(value): value is AttemptRecoveryWithheld => typeof value === "string"
|
|
657
|
+
&& ATTEMPT_RECOVERY_WITHHELD.has(value as AttemptRecoveryWithheld),
|
|
658
|
+
))]
|
|
659
|
+
: [];
|
|
623
660
|
return {
|
|
624
661
|
ordinal: attempt.ordinal as number,
|
|
625
662
|
provider: attempt.provider,
|
|
@@ -639,6 +676,7 @@ function normalizeUsageAttempt(raw: unknown): PersistedUsageAttempt | null {
|
|
|
639
676
|
: {}),
|
|
640
677
|
sendCount: attempt.sendCount as number,
|
|
641
678
|
recoveryKinds,
|
|
679
|
+
...(recoveryWithheld.length ? { recoveryWithheld } : {}),
|
|
642
680
|
usageStatus: attempt.usageStatus as UsageStatus,
|
|
643
681
|
...(isCodexUsageAccountLogLabel(attempt.accountLogLabel)
|
|
644
682
|
? { accountLogLabel: attempt.accountLogLabel }
|
|
@@ -852,18 +890,61 @@ function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
|
|
|
852
890
|
};
|
|
853
891
|
}
|
|
854
892
|
|
|
855
|
-
|
|
893
|
+
// Bound hot-path filesystem hardening to once per second while ensuring an external mode
|
|
894
|
+
// widening cannot suppress write-triggered repair for the lifetime of the process.
|
|
895
|
+
const USAGE_LOG_PERMISSION_RECHECK_MS = 1_000;
|
|
896
|
+
|
|
897
|
+
type UsageLogPermissionCheck = {
|
|
898
|
+
path: string;
|
|
899
|
+
checkedAt: number;
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
let ensuredUsageLogDir: UsageLogPermissionCheck | null = null;
|
|
903
|
+
let ensuredUsageLogFile: UsageLogPermissionCheck | null = null;
|
|
904
|
+
|
|
905
|
+
function usageLogPermissionCheckIsCurrent(
|
|
906
|
+
check: UsageLogPermissionCheck | null,
|
|
907
|
+
path: string,
|
|
908
|
+
now: number,
|
|
909
|
+
): boolean {
|
|
910
|
+
return check?.path === path
|
|
911
|
+
&& now >= check.checkedAt
|
|
912
|
+
&& now - check.checkedAt < USAGE_LOG_PERMISSION_RECHECK_MS;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function ensureUsageLogDir(now: number): void {
|
|
856
916
|
const dir = getConfigDir();
|
|
917
|
+
if (usageLogPermissionCheckIsCurrent(ensuredUsageLogDir, dir, now)) return;
|
|
857
918
|
recordOwnedConfigPath(dir, usageLogPath());
|
|
858
919
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
859
920
|
try { chmodSync(dir, 0o700); } catch { /* best-effort on platforms that ignore chmod */ }
|
|
921
|
+
ensuredUsageLogDir = { path: dir, checkedAt: now };
|
|
860
922
|
}
|
|
861
923
|
|
|
862
924
|
export function appendUsageEntry(entry: PersistedUsageEntry): void {
|
|
863
|
-
|
|
925
|
+
const line = `${JSON.stringify(normalizeUsageEntry(entry))}\n`;
|
|
864
926
|
const path = usageLogPath();
|
|
865
|
-
|
|
866
|
-
|
|
927
|
+
const now = Date.now();
|
|
928
|
+
const doAppend = (): void => {
|
|
929
|
+
ensureUsageLogDir(now);
|
|
930
|
+
const filePermissionsCurrent = usageLogPermissionCheckIsCurrent(ensuredUsageLogFile, path, now);
|
|
931
|
+
appendFileSync(path, line, { encoding: "utf-8", mode: 0o600 });
|
|
932
|
+
if (!filePermissionsCurrent) {
|
|
933
|
+
try { chmodSync(path, 0o600); } catch { /* best-effort on platforms that ignore chmod */ }
|
|
934
|
+
ensuredUsageLogFile = { path, checkedAt: now };
|
|
935
|
+
}
|
|
936
|
+
};
|
|
937
|
+
try {
|
|
938
|
+
doAppend();
|
|
939
|
+
} catch (error: any) {
|
|
940
|
+
if (error?.code === "ENOENT") {
|
|
941
|
+
ensuredUsageLogDir = null;
|
|
942
|
+
ensuredUsageLogFile = null;
|
|
943
|
+
doAppend();
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
throw error;
|
|
947
|
+
}
|
|
867
948
|
}
|
|
868
949
|
|
|
869
950
|
export type UsageLogRevision = {
|
|
@@ -1057,6 +1138,8 @@ export function resetUsageReadCacheForTests(): void {
|
|
|
1057
1138
|
managementUsageReadInflight?.abort.abort();
|
|
1058
1139
|
managementUsageReadInflight = null;
|
|
1059
1140
|
retainedUsageSnapshot = null;
|
|
1141
|
+
ensuredUsageLogDir = null;
|
|
1142
|
+
ensuredUsageLogFile = null;
|
|
1060
1143
|
}
|
|
1061
1144
|
|
|
1062
1145
|
function readExactly(fd: number, length: number, position: number): Buffer | null {
|
|
@@ -75,6 +75,7 @@ import {
|
|
|
75
75
|
} from "./sidecar-providers";
|
|
76
76
|
import { providerDestinationConfigError } from "../lib/destination-policy";
|
|
77
77
|
import { redactSecretString } from "../lib/redact";
|
|
78
|
+
import { rememberBridgeSearchReplay } from "../responses/bridge-search-replay-cache";
|
|
78
79
|
|
|
79
80
|
/** Canonical Ollama Cloud origin. The only origin the "ollama" backend derives on its own. */
|
|
80
81
|
export const OLLAMA_CLOUD_ORIGIN = "https://ollama.com";
|
|
@@ -377,11 +378,18 @@ export interface PassthroughWebSearchBridgeStreamOptions {
|
|
|
377
378
|
/** Sends one continuation leg and resolves with its response. */
|
|
378
379
|
send: (body: string) => Promise<Response>;
|
|
379
380
|
execute: PassthroughWebSearchBridgeExecutor;
|
|
381
|
+
/**
|
|
382
|
+
* Destination identity for the executed-search memo (#4587). When absent nothing is recorded,
|
|
383
|
+
* and the next turn replays the hosted cell exactly as it does today.
|
|
384
|
+
*/
|
|
385
|
+
destinationScope?: string;
|
|
380
386
|
/**
|
|
381
387
|
* Re-applies the caller's outbound body ceiling to a continuation body. Returns a refusal
|
|
382
388
|
* message when the extended body may not be sent, or undefined when it is admitted.
|
|
383
389
|
*/
|
|
384
390
|
checkOutboundBody?: (body: string) => string | undefined;
|
|
391
|
+
/** Releases request-scoped resources when the stream completes, fails, or is cancelled. */
|
|
392
|
+
onFinalize?: () => void;
|
|
385
393
|
signal?: AbortSignal;
|
|
386
394
|
}
|
|
387
395
|
|
|
@@ -1094,11 +1102,22 @@ async function* bridgeStreamBlocks(
|
|
|
1094
1102
|
outcome = await options.execute(queries, options.signal);
|
|
1095
1103
|
}
|
|
1096
1104
|
yield* emit(state.searchEndFrames(call, queries, outcome));
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1105
|
+
// The model needs a readable result either way; an executor error is reported as the
|
|
1106
|
+
// tool result rather than as a turn failure, so it can still answer without the search.
|
|
1107
|
+
const output = outcome.error ? "Web search failed: " + outcome.error : outcome.text;
|
|
1108
|
+
turns.push({ call, output });
|
|
1109
|
+
// Record what a continuation leg WOULD put on the wire, whether or not this leg sends one
|
|
1110
|
+
// (#4587). The caller keeps the hosted cell and replays it next turn; the pre-dispatch
|
|
1111
|
+
// rewrite in the Responses adapter uses this to hand the destination back its own call and
|
|
1112
|
+
// result instead of an item type it never produced. Recording the same text that
|
|
1113
|
+
// appendBridgeSearchTurn would append is what keeps a replayed turn and a continued turn
|
|
1114
|
+
// showing the destination one consistent conversation.
|
|
1115
|
+
rememberBridgeSearchReplay(options.destinationScope, call.cellItemId, {
|
|
1116
|
+
callId: call.callId,
|
|
1117
|
+
sourceItemId: call.sourceItemId,
|
|
1118
|
+
name: WEB_SEARCH_TOOL_NAME,
|
|
1119
|
+
argumentsText: call.argumentsText,
|
|
1120
|
+
output,
|
|
1102
1121
|
});
|
|
1103
1122
|
}
|
|
1104
1123
|
|
|
@@ -1170,21 +1189,36 @@ export function createPassthroughWebSearchBridgeStream(
|
|
|
1170
1189
|
const aborted = (): boolean => cancelled || options.signal?.aborted === true;
|
|
1171
1190
|
const iterator = bridgeStreamBlocks(options, aborted)[Symbol.asyncIterator]();
|
|
1172
1191
|
const encoder = new TextEncoder();
|
|
1192
|
+
let finalized = false;
|
|
1193
|
+
const finalize = (): void => {
|
|
1194
|
+
if (finalized) return;
|
|
1195
|
+
finalized = true;
|
|
1196
|
+
options.onFinalize?.();
|
|
1197
|
+
};
|
|
1173
1198
|
return new ReadableStream<Uint8Array>({
|
|
1174
1199
|
async pull(controller) {
|
|
1175
1200
|
try {
|
|
1176
1201
|
const next = await iterator.next();
|
|
1177
1202
|
if (next.done) {
|
|
1203
|
+
finalize();
|
|
1178
1204
|
controller.close();
|
|
1179
1205
|
return;
|
|
1180
1206
|
}
|
|
1181
1207
|
controller.enqueue(encoder.encode(next.value));
|
|
1182
1208
|
} catch (error) {
|
|
1209
|
+
finalize();
|
|
1183
1210
|
controller.error(error);
|
|
1184
1211
|
}
|
|
1185
1212
|
},
|
|
1186
1213
|
cancel(reason) {
|
|
1187
1214
|
cancelled = true;
|
|
1215
|
+
// Release request-scoped authority immediately. An async generator cannot
|
|
1216
|
+
// process a queued return() while its active next() is blocked on an
|
|
1217
|
+
// upstream read, so deferring finalize until that settles would hold the
|
|
1218
|
+
// sidecar probe lease for as long as the abandoned upstream leg does.
|
|
1219
|
+
// Optional chaining would also skip finalize entirely for an iterator
|
|
1220
|
+
// with no return method.
|
|
1221
|
+
finalize();
|
|
1188
1222
|
void iterator.return?.(reason);
|
|
1189
1223
|
},
|
|
1190
1224
|
});
|