@bitkyc08/opencodex 2.59.0 → 2.60.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/gui/dist/assets/index-BTuCbqQd.css +1 -0
- package/gui/dist/assets/index-DoBVdPHP.js +134 -0
- package/gui/dist/index.html +2 -2
- package/package.json +4 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +4 -1
- package/src/adapters/base.ts +23 -0
- package/src/adapters/coding-agent/turn.ts +22 -2
- package/src/adapters/command-code.ts +50 -3
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/request-builder.ts +7 -1
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +3 -1
- package/src/adapters/devin/cloud-direct/chat.ts +3 -1
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
- package/src/adapters/devin.ts +50 -13
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +29 -5
- package/src/adapters/google-http.ts +49 -10
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +37 -6
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +5 -1
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +15 -3
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/bridge/sse.ts +3 -151
- package/src/cli/account-extended.ts +4 -4
- package/src/cli/dispatch.ts +3 -3
- package/src/cli/doctor.ts +28 -9
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +7 -2
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/client/machine-api.ts +2 -2
- package/src/client/machine-listener.ts +2 -2
- package/src/client/runtime.ts +26 -2
- package/src/codex/account-store.ts +65 -0
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/pool-quota-probe.ts +30 -7
- package/src/codex/catalog/gather-capture.ts +21 -2
- package/src/codex/catalog/model-hints.ts +29 -28
- package/src/codex/catalog/parsing.ts +7 -0
- package/src/codex/catalog/provider-models.ts +19 -2
- package/src/codex/catalog/retained-sync.ts +22 -26
- package/src/codex/catalog/routed-gather.ts +19 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +20 -166
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/model-cache.ts +47 -0
- package/src/codex/model-entitlements.ts +29 -10
- package/src/codex/observed-model-denials.ts +101 -8
- package/src/codex/prompt-text-probe.ts +9 -6
- package/src/codex/routing.ts +7 -1
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +22 -4
- package/src/combos/failover.ts +3 -0
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/diagnostics.ts +22 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +18 -0
- package/src/config/proxy-env.ts +8 -2
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +4 -0
- package/src/config/schema/leaf-validators.ts +12 -0
- package/src/config.ts +2 -2
- package/src/generated/compatibility-version.json +249 -169
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/mutation-plan.ts +815 -0
- package/src/integrations/writer.ts +85 -99
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +23 -0
- package/src/lib/errors.ts +79 -0
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-outbound.ts +50 -2
- package/src/lib/socks5-fetch.ts +136 -26
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +218 -27
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +1 -0
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +3 -107
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/key-failover.ts +66 -18
- package/src/providers/model-rename-migration.ts +55 -1
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/vendor-probes-key.ts +1 -1
- package/src/providers/registry/entries-core.ts +39 -17
- package/src/providers/registry/entries-extended.ts +35 -1
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +9 -0
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/responses/apply-patch-envelope.ts +0 -12
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +91 -115
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/auth-cors.ts +3 -0
- package/src/server/chat-completions.ts +5 -2
- package/src/server/claude-messages.ts +6 -3
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/index/serve-options.ts +86 -29
- package/src/server/index/spend-ledger-lifecycle.ts +66 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +16 -13
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/config-routes.ts +18 -2
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/route-registry.ts +14 -0
- package/src/server/management/shared.ts +10 -3
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/management-auth.ts +15 -1
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +119 -10
- package/src/server/request-log.ts +55 -2
- package/src/server/request-metrics.ts +236 -0
- package/src/server/responses/adapter-continuation.ts +3 -3
- package/src/server/responses/adapter-dispatch.ts +11 -6
- package/src/server/responses/compact.ts +32 -10
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +8 -3
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +6 -12
- package/src/server/responses/core-opaque-recovery.ts +1 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/passthrough-delivery.ts +31 -14
- package/src/server/responses/passthrough-dispatch.ts +36 -9
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +67 -17
- package/src/server/responses/request-send-budget.ts +5 -1
- package/src/server/responses/request-sidecar-auth.ts +1 -1
- package/src/server/responses/request-transport.ts +2 -2
- package/src/server/responses/run-turn-execution.ts +25 -3
- package/src/server/responses/sidecar-execution.ts +17 -2
- package/src/server/responses/ws-upstream.ts +14 -27
- package/src/server/responses-custom-tool-repair.ts +27 -54
- package/src/server/responses-undeclared-tool-guard.ts +31 -1
- package/src/server/sse-payload-rewrite.ts +1 -1
- package/src/tray/windows-tray.ps1 +155 -3
- package/src/types/config.ts +11 -3
- package/src/types/provider.ts +18 -0
- package/src/types/request.ts +2 -0
- package/src/types/tools.ts +14 -0
- package/src/types.ts +1 -0
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/gui/dist/assets/index-C5IebErG.js +0 -136
- package/gui/dist/assets/index-OESInAjC.css +0 -1
|
@@ -38,8 +38,17 @@ export type ExemptionReason =
|
|
|
38
38
|
| "test-seam"
|
|
39
39
|
/** The CLI reaches the same data through a local transport instead of HTTP. */
|
|
40
40
|
| "local-transport"
|
|
41
|
+
/** Machine scrape target whose HTTP exposition is the operator contract. */
|
|
42
|
+
| "scrape-target"
|
|
41
43
|
/** Older clients use this alias; the current CLI drives its declared replacement. */
|
|
42
44
|
| "compatibility-alias"
|
|
45
|
+
/**
|
|
46
|
+
* A read-only POST whose purpose is to bind an interactive confirmation to the mutation that
|
|
47
|
+
* immediately follows it. There is no standalone thing for a CLI to do with one: the plan is
|
|
48
|
+
* only meaningful to the caller that is about to commit it, and a scripted caller drives the
|
|
49
|
+
* mutation directly.
|
|
50
|
+
*/
|
|
51
|
+
| "interactive-preview"
|
|
43
52
|
/** Unreachable in the live dispatch order; delete rather than expose. */
|
|
44
53
|
| "dead"
|
|
45
54
|
/**
|
|
@@ -159,6 +168,7 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
|
|
|
159
168
|
{ method: "GET", path: "/api/client-integrations/aside/profiles/journal", module: "server/management/aside-profile-routes", mutates: false, mechanism: "prefix-decode" },
|
|
160
169
|
{ method: "DELETE", path: "/api/client-integrations/aside/profiles/journal", module: "server/management/aside-profile-routes", mutates: true, mechanism: "prefix-decode", exempt: { reason: "deferred-verb", why: "Aside history deletion uses the dashboard journal cleanup; the CLI has history and restore but no deletion verb yet.", owner: "260904_priority65_closeout WP7", ownerDoc: "devlog/_fin/260904_priority65_closeout/060_wp7_rollback_journal_crud.md" } },
|
|
161
170
|
{ method: "GET", path: "/api/client-integrations/aside/profiles/{profileId}/journal", module: "server/management/aside-profile-routes", mutates: false, mechanism: "prefix-decode" },
|
|
171
|
+
{ method: "POST", path: "/api/client-integrations/aside/profiles/{profileId}/preview", module: "server/management/aside-profile-routes", mutates: false, mechanism: "prefix-decode", exempt: { reason: "interactive-preview", why: "Plans one Aside profile's change so a dashboard confirmation can show what would change and bind to it. Read-only, and useful only to the caller about to commit; a scripted caller drives the profile toggle or restore directly." } },
|
|
162
172
|
{ method: "DELETE", path: "/api/client-integrations/aside/profiles/{profileId}/journal", module: "server/management/aside-profile-routes", mutates: true, mechanism: "prefix-decode", exempt: { reason: "deferred-verb", why: "Aside profile history deletion uses the dashboard journal cleanup; the CLI has scoped history and restore but no deletion verb yet.", owner: "260904_priority65_closeout WP7", ownerDoc: "devlog/_fin/260904_priority65_closeout/060_wp7_rollback_journal_crud.md" } },
|
|
163
173
|
{ method: "POST", path: "/api/client-integrations/aside/profiles/{profileId}/restore", module: "server/management/aside-profile-routes", mutates: true, mechanism: "prefix-decode" },
|
|
164
174
|
// server/management/codex-prompt-routes
|
|
@@ -197,6 +207,8 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
|
|
|
197
207
|
{ method: "GET", path: "/api/client-integrations/journal", module: "server/management/integration-routes", mutates: false },
|
|
198
208
|
{ method: "DELETE", path: "/api/client-integrations/journal", module: "server/management/integration-routes", mutates: true, exempt: { reason: "deferred-verb", why: "Retiring one rollback row is a dashboard-local cleanup; the CLI verb that would drive it is owed by a later work-phase and is not implemented here.", owner: "260904_priority65_closeout WP7", ownerDoc: "devlog/_fin/260904_priority65_closeout/060_wp7_rollback_journal_crud.md" } },
|
|
199
209
|
{ method: "POST", path: "/api/client-integrations/restore", module: "server/management/integration-routes", mutates: true },
|
|
210
|
+
{ method: "POST", path: "/api/client-integrations/preview", module: "server/management/integration-routes", mutates: false, exempt: { reason: "interactive-preview", why: "Plans an apply, overwrite or disable so a dashboard confirmation can show what would change and bind to it. Read-only, and useful only to the caller about to commit; a scripted caller drives PUT /api/client-integrations/{clientId} directly." } },
|
|
211
|
+
{ method: "POST", path: "/api/client-integrations/restore/preview", module: "server/management/integration-routes", mutates: false, exempt: { reason: "interactive-preview", why: "Plans an undo so drift is shown before a restore rather than discovered by a rejected mutation. Read-only, and useful only to the caller about to commit; a scripted caller drives POST /api/client-integrations/restore directly." } },
|
|
200
212
|
// server/management/lab-automation-routes
|
|
201
213
|
{ method: "GET", path: "/api/lab/automation", module: "server/management/lab-automation-routes", mutates: false, exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } },
|
|
202
214
|
{ method: "GET", path: "/api/lab/automation/runs", module: "server/management/lab-automation-routes", mutates: false, exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } },
|
|
@@ -234,6 +246,8 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
|
|
|
234
246
|
{ method: "POST", path: "/api/storage/trash/restore", module: "server/management/logs-usage-routes", mutates: true },
|
|
235
247
|
{ method: "PUT", path: "/api/debug", module: "server/management/logs-usage-routes", mutates: true },
|
|
236
248
|
{ method: "PUT", path: "/api/storage/cleanup-policy", module: "server/management/logs-usage-routes", mutates: true },
|
|
249
|
+
// server/management/metrics-routes
|
|
250
|
+
{ method: "GET", path: "/api/metrics", module: "server/management/metrics-routes", mutates: false, exempt: { reason: "scrape-target", why: "This machine scrape target exposes authenticated text exposition for monitoring systems; a CLI JSON verb would be a different contract." } },
|
|
237
251
|
// server/management/model-routes
|
|
238
252
|
{ method: "GET", path: "/api/aliases", module: "server/management/model-routes", mutates: false },
|
|
239
253
|
{ method: "GET", path: "/api/catalog", module: "server/management/model-routes", mutates: false },
|
|
@@ -245,12 +245,19 @@ export function requestLogDto(
|
|
|
245
245
|
* share the same fetch, the same per-provider cache (dedups Codex's frequent /v1/models polling),
|
|
246
246
|
* and the same stale fallback when a provider blips, instead of a parallel uncached copy.
|
|
247
247
|
*/
|
|
248
|
-
export async function fetchAllModels(
|
|
248
|
+
export async function fetchAllModels(
|
|
249
|
+
config: OcxConfig,
|
|
250
|
+
/** Filled with each provider's content revision as of the moment its rows were chosen. */
|
|
251
|
+
providerContentRevisions?: Map<string, string>,
|
|
252
|
+
): Promise<CatalogModel[]> {
|
|
249
253
|
const { gatherRoutedModels } = await import("../../codex/catalog");
|
|
250
254
|
const baseline = captureInitialSelectionBaseline(config);
|
|
251
|
-
if (!baseline) return gatherRoutedModels(config);
|
|
255
|
+
if (!baseline) return gatherRoutedModels(config, providerContentRevisions ? { providerContentRevisions } : undefined);
|
|
252
256
|
const outcomes: Array<{ provider: string; state: "authoritative" | "degraded" }> = [];
|
|
253
|
-
const models = await gatherRoutedModels(config, {
|
|
257
|
+
const models = await gatherRoutedModels(config, {
|
|
258
|
+
providerModelOutcomes: outcomes,
|
|
259
|
+
...(providerContentRevisions ? { providerContentRevisions } : {}),
|
|
260
|
+
});
|
|
254
261
|
finalizeInitialModelSelection(config, baseline, uniqueCatalogModelsForPublicList(models),
|
|
255
262
|
outcomes.filter(outcome => outcome.state === "authoritative").map(outcome => outcome.provider));
|
|
256
263
|
return models;
|
|
@@ -36,6 +36,7 @@ import { isServiceViable } from "../../service";
|
|
|
36
36
|
import { readRuntimePort } from "../../config/process-state";
|
|
37
37
|
import { withProcessRuntimeProvenance } from "../../lib/bun-runtime";
|
|
38
38
|
import { selfLaunchArgv } from "../../lib/self-launch-argv";
|
|
39
|
+
import { spendLedgerRestartEnvironment } from "../../lib/spend-ledger-owner";
|
|
39
40
|
import {
|
|
40
41
|
MEMORY_DRAIN_RESTART_MS,
|
|
41
42
|
REPLACEMENT_READY_TIMEOUT_MS,
|
|
@@ -225,8 +226,12 @@ function spawnDetachedStart(
|
|
|
225
226
|
return new Promise<void>((resolve, reject) => {
|
|
226
227
|
let child: ReturnType<typeof spawn>;
|
|
227
228
|
try {
|
|
228
|
-
const
|
|
229
|
-
delete
|
|
229
|
+
const sourceEnv: NodeJS.ProcessEnv = { ...process.env };
|
|
230
|
+
delete sourceEnv.OCX_SERVICE;
|
|
231
|
+
const env = spendLedgerRestartEnvironment(
|
|
232
|
+
sourceEnv,
|
|
233
|
+
waitForHealthBeforeParentExit ? undefined : process.pid,
|
|
234
|
+
);
|
|
230
235
|
child = spawn(process.execPath, launchArgs, {
|
|
231
236
|
detached: true,
|
|
232
237
|
stdio: "ignore",
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
} from "../../lib/codex-restart-contract";
|
|
40
40
|
import { jsonResponse } from "../auth-cors";
|
|
41
41
|
import { getInspectionCounters } from "../relay";
|
|
42
|
+
import { spendLedgerDiagnosticsSnapshot } from "../../lib/spend-reservation-ledger";
|
|
42
43
|
import type {
|
|
43
44
|
performCodexRestart,
|
|
44
45
|
readCodexAppServerState,
|
|
@@ -60,6 +61,7 @@ export async function handleSystemRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
60
61
|
version,
|
|
61
62
|
uptime: process.uptime(),
|
|
62
63
|
pid: process.pid,
|
|
64
|
+
spendLedger: spendLedgerDiagnosticsSnapshot(),
|
|
63
65
|
});
|
|
64
66
|
}
|
|
65
67
|
if (url.pathname === "/api/system/memory" && req.method === "GET") {
|
|
@@ -66,6 +66,7 @@ const pinnedAggregates = new Set<RetainedUsageAggregate>();
|
|
|
66
66
|
let baseFlight: Promise<UsageAggregateResult> | null = null;
|
|
67
67
|
const filteredFlights = new Map<string, Promise<UsageAggregateResult>>();
|
|
68
68
|
const retainedFilteredAggregates = new Map<string, RetainedUsageAggregate>();
|
|
69
|
+
const MAX_CONCURRENT_FILTERED_AGGREGATES = 4;
|
|
69
70
|
|
|
70
71
|
function currentTimeZone(): string {
|
|
71
72
|
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
@@ -283,6 +284,9 @@ export async function getFilteredUsageAggregate(filter: {
|
|
|
283
284
|
]);
|
|
284
285
|
const existing = filteredFlights.get(key);
|
|
285
286
|
if (existing) return existing;
|
|
287
|
+
if (filteredFlights.size >= MAX_CONCURRENT_FILTERED_AGGREGATES) {
|
|
288
|
+
throw new Error("too many concurrent filtered usage aggregates");
|
|
289
|
+
}
|
|
286
290
|
|
|
287
291
|
const flight = refreshFilteredAggregate(key, normalizedFilter, fixedWindow);
|
|
288
292
|
filteredFlights.set(key, flight);
|
|
@@ -63,6 +63,7 @@ import { handleLogsUsageRoutes } from "./management/logs-usage-routes";
|
|
|
63
63
|
import { handleStorageLogGuardRoutes } from "./management/storage-log-guard-routes";
|
|
64
64
|
import { handleRequestHistoryRoutes } from "./management/request-history-routes";
|
|
65
65
|
import { handleRoutingAnalyticsRoutes } from "./management/routing-analytics-routes";
|
|
66
|
+
import { handleMetricsRoutes } from "./management/metrics-routes";
|
|
66
67
|
import { handleProviderRoutes } from "./management/provider-routes";
|
|
67
68
|
import { handleModelRoutes } from "./management/model-routes";
|
|
68
69
|
import { handleAgentSettingsRoutes } from "./management/agent-settings-routes";
|
|
@@ -276,6 +277,7 @@ export async function handleManagementAPI(
|
|
|
276
277
|
?? (await handleQuotaResetRoutesOnDemand(ctx))
|
|
277
278
|
?? (await handleWorkflowBudgetRoutesOnDemand(ctx))
|
|
278
279
|
?? (await handleGrokCouponRoutesOnDemand(ctx))
|
|
280
|
+
?? handleMetricsRoutes(ctx)
|
|
279
281
|
?? (await handleRoutingAnalyticsRoutes(ctx))
|
|
280
282
|
?? (await handleRoutingProfileRoutesOnDemand(ctx))
|
|
281
283
|
?? (await handleProviderRoutes(ctx))
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
writeFileSync,
|
|
13
13
|
} from "node:fs";
|
|
14
14
|
import { dirname, join } from "node:path";
|
|
15
|
-
import { adminApiTokenFilePath } from "../lib/admin-secrets";
|
|
15
|
+
import { adminApiTokenFilePath, opencodeCatalogToken } from "../lib/admin-secrets";
|
|
16
16
|
import {
|
|
17
17
|
LOCAL_MANAGEMENT_CAPABILITY_HEADER,
|
|
18
18
|
LOCAL_MANAGEMENT_CAPABILITY_EXPIRES_AT_HEADER,
|
|
@@ -297,6 +297,7 @@ export function createManagementSessionControl(state: ManagementAuthState): Mana
|
|
|
297
297
|
*/
|
|
298
298
|
export type ManagementPrincipal =
|
|
299
299
|
| "admin-token"
|
|
300
|
+
| "opencode-catalog-token"
|
|
300
301
|
| "gui-session"
|
|
301
302
|
| "gui-pair-capability"
|
|
302
303
|
| "local-read-capability"
|
|
@@ -484,6 +485,16 @@ function requestManagementCredential(req: Request): string | null {
|
|
|
484
485
|
|| null;
|
|
485
486
|
}
|
|
486
487
|
|
|
488
|
+
function isOpencodeCatalogRequest(req: Request): boolean {
|
|
489
|
+
if (req.method !== "GET") return false;
|
|
490
|
+
try {
|
|
491
|
+
const url = new URL(req.url);
|
|
492
|
+
return url.pathname === "/api/models" && url.search === "";
|
|
493
|
+
} catch {
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
487
498
|
function resolveManagementAdmission(
|
|
488
499
|
req: Request,
|
|
489
500
|
state: ManagementAuthState,
|
|
@@ -500,6 +511,9 @@ function resolveManagementAdmission(
|
|
|
500
511
|
else if (state.available) {
|
|
501
512
|
const actual = requestManagementCredential(req);
|
|
502
513
|
if (actual && equalSecret(actual, state.token)) principal = "admin-token";
|
|
514
|
+
else if (actual && isOpencodeCatalogRequest(req) && equalSecret(actual, opencodeCatalogToken(state.token))) {
|
|
515
|
+
principal = "opencode-catalog-token";
|
|
516
|
+
}
|
|
503
517
|
else if (config && authorizeGuiSessionRequest(req, config, state).ok) principal = "gui-session";
|
|
504
518
|
}
|
|
505
519
|
if (principal) admittedManagementRequests.set(req, principal);
|
package/src/server/readiness.ts
CHANGED
|
@@ -3,10 +3,30 @@
|
|
|
3
3
|
*
|
|
4
4
|
* `GET /healthz` answers "is the process alive and serving HTTP?" the instant the
|
|
5
5
|
* listener binds. Readiness is stricter: the proxy is "ready" only after the
|
|
6
|
-
* post-startup Codex catalog/config sync (`syncModelsToCodex`) has
|
|
7
|
-
* `ok=true
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* post-startup Codex catalog/config sync (`syncModelsToCodex`) has SETTLED and
|
|
7
|
+
* reported `ok=true`. Until it settles the process is live (Codex can open a
|
|
8
|
+
* socket) but not ready — a request would race the sync — so clients back off.
|
|
9
|
+
*
|
|
10
|
+
* What readiness is NOT (#5181): it is not a verdict on the local Codex client's
|
|
11
|
+
* artifacts. A nonempty `warning` used to be terminal here, which made a
|
|
12
|
+
* degradation of files written into the local Codex home permanently un-ready a
|
|
13
|
+
* proxy that was serving every other provider correctly. In a single-replica
|
|
14
|
+
* Kubernetes deployment that removed the only Service endpoint.
|
|
15
|
+
*
|
|
16
|
+
* `ok` and `warning` are separate fields in the sync result because they answer
|
|
17
|
+
* separate questions, and the gate must not conflate them. `ok` is the sync's own
|
|
18
|
+
* verdict on whether the essential work — config injection, and the write
|
|
19
|
+
* admission that precedes it — succeeded. `warning` names a degradation the sync
|
|
20
|
+
* itself decided to continue past: no catalog source so Codex keeps its native
|
|
21
|
+
* catalog, combos omitted from the catalog, a conversation-history relabel left
|
|
22
|
+
* to Codex's own writer, or a caught catalog-refresh exception after which
|
|
23
|
+
* injection still runs and still reports its own `ok`. None of those stops this
|
|
24
|
+
* process from accepting HTTP or routing to a provider, so none of them may close
|
|
25
|
+
* the gate.
|
|
26
|
+
*
|
|
27
|
+
* That boundary is not new, only extended: the Claude Code roster reconciliation
|
|
28
|
+
* in `src/cli/claude-agent-startup-sync.ts` already delays the ready transition
|
|
29
|
+
* without being allowed to fail it, on the same reasoning.
|
|
10
30
|
*
|
|
11
31
|
* Design contract (per P1 review):
|
|
12
32
|
* - NO module-global mutable state. Each `startServer` invocation gets its own
|
|
@@ -65,8 +85,11 @@ export interface SyncOutcomeLike {
|
|
|
65
85
|
|
|
66
86
|
/**
|
|
67
87
|
* Drive the gate from the post-startup sync. Awaits `syncFn`; the gate goes to
|
|
68
|
-
* `ready`
|
|
69
|
-
*
|
|
88
|
+
* `ready` on `ok=true`. A throw, `null`, or `ok !== true` transitions to
|
|
89
|
+
* `failed`; a nonempty `warning` does not, because the sync that produced it
|
|
90
|
+
* still reported the essential work done (see the file header, #5181). A throw
|
|
91
|
+
* and `null` stay terminal because neither is a classified outcome: the startup
|
|
92
|
+
* path did not reach a verdict, so the gate cannot claim one. Used by `handleStart`
|
|
70
93
|
* so the startup transition is unit-testable without spawning the proxy. Returns
|
|
71
94
|
* the raw sync outcome so a caller that also needs the #1046 write flags (did the
|
|
72
95
|
* sync actually write the catalog/cache?) can keep them without a second call.
|
|
@@ -90,10 +113,6 @@ export async function runStartupReadinessSync(
|
|
|
90
113
|
gate.markFailed();
|
|
91
114
|
return result;
|
|
92
115
|
}
|
|
93
|
-
if (result.warning !== undefined && result.warning !== "") {
|
|
94
|
-
gate.markFailed();
|
|
95
|
-
return result;
|
|
96
|
-
}
|
|
97
116
|
gate.markReady();
|
|
98
117
|
return result;
|
|
99
118
|
}
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
createSseTerminalOutputBoundary,
|
|
31
31
|
doneFrame,
|
|
32
32
|
failedTailFrame,
|
|
33
|
+
refusalFailedTailFrame,
|
|
33
34
|
upstreamErrorTailFrame,
|
|
34
35
|
} from "./relay";
|
|
35
36
|
import {
|
|
@@ -127,6 +128,23 @@ export function relaySseEagerBounded(
|
|
|
127
128
|
return null;
|
|
128
129
|
}
|
|
129
130
|
};
|
|
131
|
+
/**
|
|
132
|
+
* Same tail, except that a refusal the boundary already captured outranks the
|
|
133
|
+
* read failure that followed it — the upstream ended the turn before the
|
|
134
|
+
* socket did (#5176).
|
|
135
|
+
*/
|
|
136
|
+
const encodeTerminalTail = (error: unknown): Uint8Array | null => {
|
|
137
|
+
const refusalCode = terminalBoundary.upstreamRefusalCode();
|
|
138
|
+
const refusalMessage = terminalBoundary.upstreamError();
|
|
139
|
+
if (refusalCode === undefined || refusalMessage === undefined) {
|
|
140
|
+
return encodeFailedTail(error);
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
return refusalFailedTailFrame(terminalEncoder, refusalMessage, refusalCode);
|
|
144
|
+
} catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
130
148
|
const rewriteDecoder = activeRewrite ? new TextDecoder() : null;
|
|
131
149
|
const rewriteEncoder = activeRewrite ? new TextEncoder() : null;
|
|
132
150
|
const rewriteBudget = opts?.rewriteBudget;
|
|
@@ -313,7 +331,11 @@ export function relaySseEagerBounded(
|
|
|
313
331
|
const upstreamError = terminalBoundary.upstreamError() ?? opts?.upstreamError;
|
|
314
332
|
const upstreamErrorFrame = upstreamError === undefined
|
|
315
333
|
? adapterEofFrame
|
|
316
|
-
: upstreamErrorTailFrame(
|
|
334
|
+
: upstreamErrorTailFrame(
|
|
335
|
+
terminalEncoder,
|
|
336
|
+
upstreamError,
|
|
337
|
+
terminalBoundary.upstreamRefusalCode(),
|
|
338
|
+
);
|
|
317
339
|
queuedBytes += upstreamErrorFrame.byteLength + terminalSentinel.byteLength;
|
|
318
340
|
try {
|
|
319
341
|
controllerRef?.enqueue(upstreamErrorFrame);
|
|
@@ -439,7 +461,7 @@ export function relaySseEagerBounded(
|
|
|
439
461
|
// getters, toString) that re-entrantly cancel the client or abort the
|
|
440
462
|
// upstream. Build the tail FIRST, then re-check eligibility before
|
|
441
463
|
// committing to the synthetic terminal (adversarial review blocker).
|
|
442
|
-
const tail =
|
|
464
|
+
const tail = encodeTerminalTail(err);
|
|
443
465
|
if (tail && canDeliver()) {
|
|
444
466
|
// Inspection and client framing have separate bounded parsers. If
|
|
445
467
|
// inspection resynchronized after an oversized frame and observed a
|
package/src/server/relay.ts
CHANGED
|
@@ -5,6 +5,9 @@ import {
|
|
|
5
5
|
CYBER_POLICY_FALLBACK_MESSAGE,
|
|
6
6
|
isCyberPolicyCode,
|
|
7
7
|
isCyberPolicyMessage,
|
|
8
|
+
isTerminalRefusalCode,
|
|
9
|
+
safetyRefusalCodeFromMessage,
|
|
10
|
+
terminalRefusalFallbackMessage,
|
|
8
11
|
upstreamErrorMessageFromPayload,
|
|
9
12
|
} from "../lib/errors";
|
|
10
13
|
import { redactSecretString } from "../lib/redact";
|
|
@@ -150,16 +153,61 @@ export function failedTailFrame(encoder: TextEncoder, err: unknown): Uint8Array
|
|
|
150
153
|
return encoder.encode(`\n\nevent: response.failed\ndata: ${payload}\n\n${DONE_SSE_FRAME_TEXT}`);
|
|
151
154
|
}
|
|
152
155
|
|
|
153
|
-
|
|
156
|
+
/**
|
|
157
|
+
* Close a turn the upstream ended without a Responses terminal.
|
|
158
|
+
*
|
|
159
|
+
* `refusalCode` carries the upstream's own verdict when it gave one. Codex
|
|
160
|
+
* classifies this terminal by `error.code` alone and retries everything outside
|
|
161
|
+
* its fatal set (codex-rs/codex-api/src/sse/responses.rs:423-450), so stamping
|
|
162
|
+
* `upstream_server_error` on a refusal delivered it as a retryable disconnect
|
|
163
|
+
* and drove the reconnect loop in #5176. Without a refusal code the terminal is
|
|
164
|
+
* unchanged: a genuine transport failure stays retryable, which is what it is.
|
|
165
|
+
*/
|
|
166
|
+
export function upstreamErrorTailFrame(
|
|
167
|
+
encoder: TextEncoder,
|
|
168
|
+
message: string,
|
|
169
|
+
refusalCode?: string,
|
|
170
|
+
): Uint8Array {
|
|
171
|
+
return encoder.encode(
|
|
172
|
+
`event: response.failed\ndata: ${upstreamErrorFailedPayload(message, refusalCode)}\n\n`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function upstreamErrorFailedPayload(message: string, refusalCode?: string): string {
|
|
154
177
|
const error = {
|
|
155
|
-
type: "upstream_error",
|
|
156
|
-
code: "upstream_server_error",
|
|
178
|
+
type: refusalCode === undefined ? "upstream_error" : "invalid_request_error",
|
|
179
|
+
code: refusalCode ?? "upstream_server_error",
|
|
157
180
|
message: redactSecretString(message).slice(0, MAX_TAIL_ERROR_MESSAGE_CHARS),
|
|
158
181
|
};
|
|
159
|
-
return
|
|
182
|
+
return JSON.stringify({
|
|
160
183
|
type: "response.failed",
|
|
161
|
-
response: {
|
|
162
|
-
|
|
184
|
+
response: {
|
|
185
|
+
status: "failed",
|
|
186
|
+
error,
|
|
187
|
+
last_error: error,
|
|
188
|
+
...(refusalCode === undefined ? {} : { retryable: false }),
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Terminal for a read that failed after the upstream had already refused.
|
|
195
|
+
*
|
|
196
|
+
* Framed exactly like {@link failedTailFrame} — leading blank line to close a
|
|
197
|
+
* partial block, then the sentinel — but carrying the refusal instead of the
|
|
198
|
+
* generic reset. The refusal is the real outcome of the turn and the socket
|
|
199
|
+
* teardown that followed it is not, so reporting `upstream_reset` here would
|
|
200
|
+
* restart a turn the upstream has already ended (#5176).
|
|
201
|
+
*/
|
|
202
|
+
export function refusalFailedTailFrame(
|
|
203
|
+
encoder: TextEncoder,
|
|
204
|
+
message: string,
|
|
205
|
+
refusalCode: string,
|
|
206
|
+
): Uint8Array {
|
|
207
|
+
const payload = upstreamErrorFailedPayload(message, refusalCode);
|
|
208
|
+
return encoder.encode(
|
|
209
|
+
`\n\nevent: response.failed\ndata: ${payload}\n\n${DONE_SSE_FRAME_TEXT}`,
|
|
210
|
+
);
|
|
163
211
|
}
|
|
164
212
|
|
|
165
213
|
function boundedBareUpstreamErrorMessage(payload: unknown): string | undefined {
|
|
@@ -169,12 +217,57 @@ function boundedBareUpstreamErrorMessage(payload: unknown): string | undefined {
|
|
|
169
217
|
return message ? redactSecretString(message).slice(0, MAX_TAIL_ERROR_MESSAGE_CHARS) : undefined;
|
|
170
218
|
}
|
|
171
219
|
|
|
220
|
+
/**
|
|
221
|
+
* A bare upstream `error` event reduced to what the synthesized terminal needs.
|
|
222
|
+
*
|
|
223
|
+
* The structured code is authoritative whenever the upstream sent one: a code
|
|
224
|
+
* that is not a refusal means the upstream did not refuse, whatever its
|
|
225
|
+
* diagnostic text happens to quote. Recognized refusal copy is read only when
|
|
226
|
+
* no code was carried anywhere on the event, which is the shape #5176 reports.
|
|
227
|
+
* A refusal code with no message still yields a terminal, because Codex accepts
|
|
228
|
+
* that shape and supplies its own copy for it.
|
|
229
|
+
*
|
|
230
|
+
* The candidate topology mirrors {@link upstreamErrorMessageFromPayload}: code
|
|
231
|
+
* and message must be read from the same places, or an event whose message is
|
|
232
|
+
* nested under `response.error` would contribute text while its verdict went
|
|
233
|
+
* unseen.
|
|
234
|
+
*/
|
|
235
|
+
function boundedBareUpstreamError(payload: unknown): {
|
|
236
|
+
message: string;
|
|
237
|
+
refusalCode: string | undefined;
|
|
238
|
+
} | undefined {
|
|
239
|
+
const root = asJsonRecord(payload);
|
|
240
|
+
if (!root || root.type !== "error") return undefined;
|
|
241
|
+
const message = boundedBareUpstreamErrorMessage(payload);
|
|
242
|
+
const response = asJsonRecord(root.response);
|
|
243
|
+
// Precedence is {@link upstreamErrorMessageFromPayload}'s, so the envelope
|
|
244
|
+
// that supplied the message also supplies the verdict. Taking the FIRST code
|
|
245
|
+
// rather than searching for a refusal is what stops a refusal nested below a
|
|
246
|
+
// transient one from overruling it.
|
|
247
|
+
const code = [
|
|
248
|
+
asJsonRecord(root.error),
|
|
249
|
+
asJsonRecord(root.last_error),
|
|
250
|
+
asJsonRecord(response?.error),
|
|
251
|
+
asJsonRecord(response?.incomplete_details),
|
|
252
|
+
root,
|
|
253
|
+
]
|
|
254
|
+
.map(candidate => stringField(candidate, "code"))
|
|
255
|
+
.find(candidate => candidate !== undefined);
|
|
256
|
+
const refusalCode = code !== undefined
|
|
257
|
+
? (isTerminalRefusalCode(code) ? code : undefined)
|
|
258
|
+
: message === undefined ? undefined : safetyRefusalCodeFromMessage(message);
|
|
259
|
+
if (message !== undefined) return { message, refusalCode };
|
|
260
|
+
if (refusalCode === undefined) return undefined;
|
|
261
|
+
return { message: terminalRefusalFallbackMessage(refusalCode), refusalCode };
|
|
262
|
+
}
|
|
263
|
+
|
|
172
264
|
export type SseTerminalOutputBoundary = {
|
|
173
265
|
feed(chunk: Uint8Array): Uint8Array;
|
|
174
266
|
finish(): Uint8Array;
|
|
175
267
|
terminalSeen(): boolean;
|
|
176
268
|
doneSeen(): boolean;
|
|
177
269
|
upstreamError(): string | undefined;
|
|
270
|
+
upstreamRefusalCode(): string | undefined;
|
|
178
271
|
dispose(): void;
|
|
179
272
|
};
|
|
180
273
|
|
|
@@ -197,6 +290,7 @@ export function createSseTerminalOutputBoundary(
|
|
|
197
290
|
let pendingDone: { block: Uint8Array; delimiter: Uint8Array } | null = null;
|
|
198
291
|
let disposed = false;
|
|
199
292
|
let upstreamError: string | undefined;
|
|
293
|
+
let upstreamRefusalCode: string | undefined;
|
|
200
294
|
|
|
201
295
|
const processFrames = (
|
|
202
296
|
frames: ReturnType<BoundedSseFrameBuffer["feed"]>,
|
|
@@ -210,8 +304,11 @@ export function createSseTerminalOutputBoundary(
|
|
|
210
304
|
const parsed = payload === null ? undefined : parseSsePayload(payload);
|
|
211
305
|
// Observe on the client reader itself: a tee inspection branch may lag
|
|
212
306
|
// behind EOF, so its log context cannot determine the outgoing terminal.
|
|
213
|
-
const
|
|
214
|
-
if (
|
|
307
|
+
const bare = boundedBareUpstreamError(parsed);
|
|
308
|
+
if (bare !== undefined) {
|
|
309
|
+
upstreamError = bare.message;
|
|
310
|
+
upstreamRefusalCode = bare.refusalCode;
|
|
311
|
+
}
|
|
215
312
|
const safetyBuffering = dropSafetyBuffering && parsed !== undefined
|
|
216
313
|
? codexSafetyBufferingBlockAction(parsed) : "keep";
|
|
217
314
|
if (safetyBuffering === "drop") continue;
|
|
@@ -278,6 +375,7 @@ export function createSseTerminalOutputBoundary(
|
|
|
278
375
|
terminalSeen: () => terminal,
|
|
279
376
|
doneSeen: () => done,
|
|
280
377
|
upstreamError: () => upstreamError,
|
|
378
|
+
upstreamRefusalCode: () => upstreamRefusalCode,
|
|
281
379
|
dispose() {
|
|
282
380
|
if (disposed) return;
|
|
283
381
|
disposed = true;
|
|
@@ -349,7 +447,11 @@ export function relaySseWithFailedTail(
|
|
|
349
447
|
const upstreamError = terminalBoundary.upstreamError() ?? opts?.upstreamError;
|
|
350
448
|
controller.enqueue(upstreamError === undefined
|
|
351
449
|
? adapterEofIncompleteFrame(encoder)
|
|
352
|
-
: upstreamErrorTailFrame(
|
|
450
|
+
: upstreamErrorTailFrame(
|
|
451
|
+
encoder,
|
|
452
|
+
upstreamError,
|
|
453
|
+
terminalBoundary.upstreamRefusalCode(),
|
|
454
|
+
));
|
|
353
455
|
controller.enqueue(doneFrame(encoder));
|
|
354
456
|
}
|
|
355
457
|
terminalBoundary.dispose();
|
|
@@ -378,7 +480,11 @@ export function relaySseWithFailedTail(
|
|
|
378
480
|
if (!terminalBoundary.doneSeen()) controller.enqueue(doneFrame(encoder));
|
|
379
481
|
} else {
|
|
380
482
|
// Leading blank line terminates a partial SSE block so the failed frame parses cleanly.
|
|
381
|
-
|
|
483
|
+
const refusalCode = terminalBoundary.upstreamRefusalCode();
|
|
484
|
+
const refusalMessage = terminalBoundary.upstreamError();
|
|
485
|
+
controller.enqueue(refusalCode !== undefined && refusalMessage !== undefined
|
|
486
|
+
? refusalFailedTailFrame(encoder, refusalMessage, refusalCode)
|
|
487
|
+
: failedTailFrame(encoder, err));
|
|
382
488
|
}
|
|
383
489
|
controller.close();
|
|
384
490
|
} catch { /* client already torn down */ }
|
|
@@ -720,6 +826,9 @@ export function responseWithDeferredRequestLog(
|
|
|
720
826
|
// convention for a client cancellation or upstream read failure.
|
|
721
827
|
const status = reason === "cancel" ? 499 : reason === "read_error" ? 502 : response.status;
|
|
722
828
|
addFinalRequestLog(requestId, start, logCtx, status, {
|
|
829
|
+
...(reason === "eof" && logCtx.observedTerminalStatus
|
|
830
|
+
? { terminalStatus: logCtx.observedTerminalStatus }
|
|
831
|
+
: {}),
|
|
723
832
|
closeReason: reason === "cancel" ? "client_cancel" : "non_stream",
|
|
724
833
|
}, addLog);
|
|
725
834
|
},
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
isCyberPolicyCode,
|
|
13
13
|
isCyberPolicyMessage,
|
|
14
14
|
isRateLimitOrQuotaFailureMessage,
|
|
15
|
+
isUpstreamResetReplayRefusedMessage,
|
|
15
16
|
upstreamErrorMessageFromPayload,
|
|
16
17
|
} from "../lib/errors";
|
|
17
18
|
import { CODEX_CONFIG_PATH, readRootTomlString } from "../codex/paths";
|
|
@@ -22,6 +23,7 @@ import { normalizeRouteDecisionTrace, type RouteDecisionTraceV1 } from "../routi
|
|
|
22
23
|
import type { AdapterRequest } from "../adapters/base";
|
|
23
24
|
import type { RequestSpendSettlement } from "./responses/request-spend";
|
|
24
25
|
import type { AdapterTierMetadata } from "../providers/fastwire";
|
|
26
|
+
import { UPSTREAM_RESET_REPLAY_REFUSED_CODE } from "../lib/upstream-retry";
|
|
25
27
|
import { redactSecretString, sanitizeLogMetadataString } from "../lib/redact";
|
|
26
28
|
import {
|
|
27
29
|
appendUsageEntry,
|
|
@@ -67,10 +69,15 @@ import { inferCursorContextWindow } from "../adapters/cursor/discovery";
|
|
|
67
69
|
import { KIRO_MODEL_CONTEXT_WINDOWS, normalizeKiroModelId } from "../providers/kiro-models";
|
|
68
70
|
import { DEVIN_MODEL_CONTEXT_WINDOWS } from "../adapters/devin/live-models";
|
|
69
71
|
import { modelRecordValue } from "../reasoning-effort";
|
|
72
|
+
import type { RequestMetricsRecorder } from "./request-metrics";
|
|
70
73
|
|
|
71
74
|
export interface RequestLogContext {
|
|
72
75
|
model: string;
|
|
73
76
|
provider: string;
|
|
77
|
+
/** Optional process-lifetime aggregate sink, injected by the server composition owner. */
|
|
78
|
+
requestMetricsRecorder?: RequestMetricsRecorder;
|
|
79
|
+
/** Bounded terminal enum observed while inspecting a buffered response body. */
|
|
80
|
+
observedTerminalStatus?: ResponsesTerminalStatus;
|
|
74
81
|
/**
|
|
75
82
|
* Identity of the ONE logical request this context serves (#4546). Set from the execution
|
|
76
83
|
* budget minted at ingress; a retry leg, a repair refetch and a combo child share it.
|
|
@@ -291,6 +298,7 @@ export interface RequestLogEntry {
|
|
|
291
298
|
}
|
|
292
299
|
|
|
293
300
|
const requestLog: RequestLogEntry[] = [];
|
|
301
|
+
const requestLogObserversForTests = new Set<(entry: RequestLogEntry) => void>();
|
|
294
302
|
const MAX_LOG_SIZE = 2000;
|
|
295
303
|
const requestLogEntryBytes = new WeakMap<RequestLogEntry, number>();
|
|
296
304
|
let requestLogBytes = 0;
|
|
@@ -491,6 +499,9 @@ export function addRequestLog(entry: RequestLogEntry) {
|
|
|
491
499
|
else if (retained !== entry) delete retained.claudeCompatibility;
|
|
492
500
|
entry = retained;
|
|
493
501
|
retainRequestLogEntry(entry);
|
|
502
|
+
for (const observer of requestLogObserversForTests) {
|
|
503
|
+
try { observer(entry); } catch { /* test observation must never fail request logging */ }
|
|
504
|
+
}
|
|
494
505
|
try {
|
|
495
506
|
// Failure diagnostics survive the 200-entry ring buffer by riding the persisted
|
|
496
507
|
// usage entry (devlog/_plan/260716_claudecode_hardening/030). Success rows stay
|
|
@@ -566,6 +577,12 @@ export function addRequestLog(entry: RequestLogEntry) {
|
|
|
566
577
|
}
|
|
567
578
|
}
|
|
568
579
|
|
|
580
|
+
/** Test-only finalized-row observation without polling the management projection. */
|
|
581
|
+
export function observeRequestLogsForTests(observer: (entry: RequestLogEntry) => void): () => void {
|
|
582
|
+
requestLogObserversForTests.add(observer);
|
|
583
|
+
return () => { requestLogObserversForTests.delete(observer); };
|
|
584
|
+
}
|
|
585
|
+
|
|
569
586
|
export function nextRequestLogId(_timestamp = Date.now()): string {
|
|
570
587
|
return `ocx-${randomBytes(16).toString("hex")}`;
|
|
571
588
|
}
|
|
@@ -714,7 +731,15 @@ export function requestLogErrorCode(
|
|
|
714
731
|
}
|
|
715
732
|
return "permission_denied";
|
|
716
733
|
}
|
|
717
|
-
if (status === 429)
|
|
734
|
+
if (status === 429) {
|
|
735
|
+
// A refused ambiguous reset answers 429 by design (it must not invite a client
|
|
736
|
+
// retry that could duplicate inference); classify it by its message so the log
|
|
737
|
+
// distinguishes a proxy refusal from provider throttling.
|
|
738
|
+
if (upstreamError?.trim() && isUpstreamResetReplayRefusedMessage(upstreamError)) {
|
|
739
|
+
return UPSTREAM_RESET_REPLAY_REFUSED_CODE;
|
|
740
|
+
}
|
|
741
|
+
return "rate_limit_exceeded";
|
|
742
|
+
}
|
|
718
743
|
if (status === 503) return "server_is_overloaded";
|
|
719
744
|
if (status >= 500) return "upstream_server_error";
|
|
720
745
|
return `http_${status}`;
|
|
@@ -766,6 +791,22 @@ export function catalogModelSupportsServiceTier(modelId: string, serviceTier: st
|
|
|
766
791
|
|
|
767
792
|
export function applyResponseLogMetadata(logCtx: RequestLogContext, payload: unknown): void {
|
|
768
793
|
if (!payload || typeof payload !== "object") return;
|
|
794
|
+
if (logCtx.observedTerminalStatus === undefined) {
|
|
795
|
+
const eventType = (payload as { type?: unknown }).type;
|
|
796
|
+
const response = (payload as { response?: unknown }).response;
|
|
797
|
+
const responseStatus = response && typeof response === "object"
|
|
798
|
+
? (response as { status?: unknown }).status
|
|
799
|
+
: (payload as { status?: unknown }).status;
|
|
800
|
+
if (eventType === "response.completed") {
|
|
801
|
+
logCtx.observedTerminalStatus = "completed";
|
|
802
|
+
} else if (eventType === "response.failed") {
|
|
803
|
+
logCtx.observedTerminalStatus = "failed";
|
|
804
|
+
} else if (eventType === "response.incomplete") {
|
|
805
|
+
logCtx.observedTerminalStatus = "incomplete";
|
|
806
|
+
} else if (responseStatus === "completed" || responseStatus === "failed" || responseStatus === "incomplete") {
|
|
807
|
+
logCtx.observedTerminalStatus = responseStatus;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
769
810
|
const source = "response" in payload && typeof (payload as { response?: unknown }).response === "object"
|
|
770
811
|
? (payload as { response?: unknown }).response
|
|
771
812
|
: payload;
|
|
@@ -1317,6 +1358,17 @@ export function addFinalRequestLog(
|
|
|
1317
1358
|
const usageStatus = aggregate?.status ?? existing.status;
|
|
1318
1359
|
const totalTokens = aggregate?.totalTokens ?? existing.totalTokens;
|
|
1319
1360
|
const spend = requestSpendRecord(logCtx, attempts);
|
|
1361
|
+
const durationMs = Date.now() - start;
|
|
1362
|
+
logCtx.requestMetricsRecorder?.recordFinalRequest({
|
|
1363
|
+
...(logCtx.inboundProtocol ? { protocol: logCtx.inboundProtocol } : {}),
|
|
1364
|
+
status: effectiveStatus,
|
|
1365
|
+
durationMs,
|
|
1366
|
+
...(logCtx.firstOutputMs !== undefined ? { firstOutputMs: logCtx.firstOutputMs } : {}),
|
|
1367
|
+
...(meta?.terminalStatus ? { terminalStatus: meta.terminalStatus } : {}),
|
|
1368
|
+
...(closeReason ? { closeReason } : {}),
|
|
1369
|
+
...(attempts !== undefined ? { attempts } : {}),
|
|
1370
|
+
...(spend ? { spendSends: spend.sends } : {}),
|
|
1371
|
+
});
|
|
1320
1372
|
const cacheProvenance = classifyCacheTelemetryProvenance(loggedUsage, {
|
|
1321
1373
|
wireParsed: logCtx.usageWireParsed === true,
|
|
1322
1374
|
});
|
|
@@ -1364,7 +1416,7 @@ export function addFinalRequestLog(
|
|
|
1364
1416
|
: {}),
|
|
1365
1417
|
...(logCtx.resolvedModel ? { resolvedModel: logCtx.resolvedModel } : {}),
|
|
1366
1418
|
status: effectiveStatus,
|
|
1367
|
-
durationMs
|
|
1419
|
+
durationMs,
|
|
1368
1420
|
...(logCtx.firstOutputMs !== undefined ? { firstOutputMs: logCtx.firstOutputMs } : {}),
|
|
1369
1421
|
...(errorCode ? { errorCode } : {}),
|
|
1370
1422
|
...(meta?.terminalStatus ? { terminalStatus: meta.terminalStatus } : {}),
|
|
@@ -1809,4 +1861,5 @@ export function clearRequestLogsForTests(): void {
|
|
|
1809
1861
|
requestLog.length = 0;
|
|
1810
1862
|
requestLogBytes = 0;
|
|
1811
1863
|
requestLogsHydratedFromDisk = false;
|
|
1864
|
+
requestLogObserversForTests.clear();
|
|
1812
1865
|
}
|