@bitkyc08/opencodex 2.7.41 → 2.7.42
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 +4 -0
- package/gui/dist/assets/index-Bl_VBGoI.js +65 -0
- package/gui/dist/assets/index-DfVGuN88.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/base.ts +6 -0
- package/src/adapters/kiro-constants.ts +6 -2
- package/src/adapters/kiro-retry.ts +175 -10
- package/src/adapters/kiro.ts +172 -85
- package/src/adapters/mimo-free.ts +1 -0
- package/src/adapters/openai-chat.ts +30 -4
- package/src/adapters/openai-responses.ts +90 -12
- package/src/bridge.ts +91 -43
- package/src/claude/desktop-3p-paths.ts +84 -0
- package/src/claude/desktop-3p.ts +29 -2
- package/src/cli/access.ts +108 -0
- package/src/cli/account-auth.ts +223 -0
- package/src/cli/account.ts +9 -1
- package/src/cli/agent.ts +184 -0
- package/src/cli/combo.ts +119 -0
- package/src/cli/config-command.ts +145 -0
- package/src/cli/debug.ts +20 -8
- package/src/cli/doctor.ts +45 -8
- package/src/cli/help.ts +65 -13
- package/src/cli/index.ts +108 -7
- package/src/cli/integrations.ts +142 -0
- package/src/cli/models-runtime.ts +212 -0
- package/src/cli/models.ts +9 -10
- package/src/cli/observe.ts +117 -0
- package/src/cli/provider-runtime.ts +152 -0
- package/src/cli/provider.ts +23 -1
- package/src/cli/runtime-api.ts +325 -0
- package/src/cli/star-prompt.ts +3 -3
- package/src/cli/status.ts +17 -0
- package/src/cli/system-command.ts +112 -0
- package/src/codex/auth-api.ts +3 -2
- package/src/codex/catalog/aggregation.ts +113 -18
- package/src/codex/catalog/provider-fetch.ts +24 -13
- package/src/codex/catalog/sync.ts +20 -8
- package/src/codex/catalog.ts +2 -1
- package/src/codex/refresh.ts +10 -3
- package/src/codex/routing.ts +21 -32
- package/src/codex/sync.ts +17 -0
- package/src/config.ts +48 -0
- package/src/generated/jawcode-model-metadata.ts +2 -1
- package/src/grok/inject.ts +184 -4
- package/src/grok/status.ts +33 -0
- package/src/lib/retry-after.ts +55 -0
- package/src/lib/windows-elevation.ts +627 -0
- package/src/providers/openai-sidecar.ts +46 -2
- package/src/providers/registry.ts +52 -0
- package/src/server/auth-cors.ts +6 -0
- package/src/server/chat-completions.ts +6 -1
- package/src/server/claude-messages.ts +20 -1
- package/src/server/images.ts +14 -7
- package/src/server/management/agent-settings-routes.ts +10 -4
- package/src/server/management/combo-routes.ts +0 -1
- package/src/server/management/config-routes.ts +0 -1
- package/src/server/management/logs-usage-routes.ts +94 -0
- package/src/server/management/model-routes.ts +0 -1
- package/src/server/management/oauth-account-routes.ts +0 -1
- package/src/server/management/provider-routes.ts +0 -1
- package/src/server/management/shared.ts +0 -1
- package/src/server/management/system-routes.ts +27 -15
- package/src/server/management-api.ts +0 -1
- package/src/server/memory-watchdog.ts +54 -10
- package/src/server/request-log-conversation.ts +168 -0
- package/src/server/request-log.ts +122 -2
- package/src/server/responses/core.ts +76 -13
- package/src/server/responses/passthrough-error.ts +38 -13
- package/src/server/startup-action-control.ts +266 -15
- package/src/service.ts +512 -3
- package/src/storage/cleanup.ts +1538 -0
- package/src/storage/scanner.ts +4 -1
- package/src/types.ts +16 -0
- package/src/update/job.ts +229 -25
- package/src/usage/log.ts +39 -0
- package/src/web-search/loop.ts +8 -1
- package/gui/dist/assets/index-B2J4t3te.css +0 -1
- package/gui/dist/assets/index-BmvM6wRb.js +0 -65
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort chat/session correlation for Logs / usage.jsonl (#330).
|
|
3
|
+
* Opaque ids only — never persist raw emails or Claude Desktop system-hash fallbacks.
|
|
4
|
+
*/
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
|
|
7
|
+
/** Reject absurdly long client strings before hashing (DoS / JSONL bloat). */
|
|
8
|
+
export const LOG_CONVERSATION_ID_INPUT_MAX = 4096;
|
|
9
|
+
/** Persisted / filterable form is always a 32-char hex digest. */
|
|
10
|
+
export const LOG_CONVERSATION_ID_LEN = 32;
|
|
11
|
+
|
|
12
|
+
function hasControlChars(value: string): boolean {
|
|
13
|
+
for (let i = 0; i < value.length; i++) {
|
|
14
|
+
const code = value.charCodeAt(i);
|
|
15
|
+
if (code <= 0x1f || code === 0x7f) return true;
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function sanitizeConversationIdInput(raw: string | undefined | null): string | undefined {
|
|
21
|
+
if (typeof raw !== "string") return undefined;
|
|
22
|
+
const trimmed = raw.trim();
|
|
23
|
+
if (!trimmed) return undefined;
|
|
24
|
+
// Reject control characters that would break JSONL / UI paste.
|
|
25
|
+
if (hasControlChars(trimmed)) return undefined;
|
|
26
|
+
if (trimmed.length > LOG_CONVERSATION_ID_INPUT_MAX) return undefined;
|
|
27
|
+
return trimmed;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Cap / sanitize a correlation id for persistence.
|
|
32
|
+
* Always hashes so client-controlled headers cannot land emails or short secrets in usage.jsonl.
|
|
33
|
+
*/
|
|
34
|
+
export function normalizeLogConversationId(raw: string | undefined | null): string | undefined {
|
|
35
|
+
const trimmed = sanitizeConversationIdInput(raw);
|
|
36
|
+
if (!trimmed) return undefined;
|
|
37
|
+
return createHash("sha256").update(trimmed).digest("hex").slice(0, LOG_CONVERSATION_ID_LEN);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Filter match: accept either the persisted digest or the original preimage
|
|
42
|
+
* (so pasting from Logs detail or the client-facing session id both work).
|
|
43
|
+
*/
|
|
44
|
+
export function matchesLogConversationId(
|
|
45
|
+
stored: string | undefined,
|
|
46
|
+
query: string | undefined | null,
|
|
47
|
+
): boolean {
|
|
48
|
+
if (!stored) return false;
|
|
49
|
+
const trimmed = typeof query === "string" ? query.trim() : "";
|
|
50
|
+
if (!trimmed) return false;
|
|
51
|
+
if (stored === trimmed) return true;
|
|
52
|
+
const hashed = normalizeLogConversationId(trimmed);
|
|
53
|
+
return hashed !== undefined && stored === hashed;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Codex/Claude/Cursor priority for Responses-shaped requests:
|
|
58
|
+
* parent thread header > session_id / session-id > thread-id > cursor conversation id.
|
|
59
|
+
*/
|
|
60
|
+
export function sessionIdHeaderFromRequest(headers: Headers): string | null {
|
|
61
|
+
return headers.get("session_id") ?? headers.get("session-id");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function conversationIdFromResponsesRequest(input: {
|
|
65
|
+
clientThreadId?: string;
|
|
66
|
+
sessionIdHeader?: string | null;
|
|
67
|
+
threadIdHeader?: string | null;
|
|
68
|
+
cursorConversationId?: string;
|
|
69
|
+
}): string | undefined {
|
|
70
|
+
return normalizeLogConversationId(
|
|
71
|
+
input.clientThreadId
|
|
72
|
+
?? input.sessionIdHeader
|
|
73
|
+
?? input.threadIdHeader
|
|
74
|
+
?? input.cursorConversationId,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Claude Code metadata.user_id only — never the system-hash Desktop fallback.
|
|
80
|
+
* Hashes the raw user_id once (same opacity goal as inbound prompt_cache_key).
|
|
81
|
+
*/
|
|
82
|
+
export function conversationIdFromClaudeMetadata(
|
|
83
|
+
metadata: { user_id?: unknown } | null | undefined,
|
|
84
|
+
): string | undefined {
|
|
85
|
+
if (!metadata || typeof metadata.user_id !== "string") return undefined;
|
|
86
|
+
return normalizeLogConversationId(metadata.user_id);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** @deprecated Prefer conversationIdFromClaudeMetadata; kept for call-site clarity. */
|
|
90
|
+
export function conversationIdFromClaudeCacheKey(
|
|
91
|
+
cacheKeySource: "metadata" | "system" | null | undefined,
|
|
92
|
+
promptCacheKey: string | undefined,
|
|
93
|
+
): string | undefined {
|
|
94
|
+
if (cacheKeySource !== "metadata") return undefined;
|
|
95
|
+
// prompt_cache_key is already sha256(user_id)[:32] from inbound — persist as-is
|
|
96
|
+
// (do not re-hash) so native and translated paths stay aligned when callers pass
|
|
97
|
+
// the preimage via conversationIdFromClaudeMetadata instead.
|
|
98
|
+
const trimmed = sanitizeConversationIdInput(promptCacheKey);
|
|
99
|
+
if (!trimmed) return undefined;
|
|
100
|
+
if (trimmed.length === LOG_CONVERSATION_ID_LEN && /^[0-9a-f]+$/i.test(trimmed)) return trimmed.toLowerCase();
|
|
101
|
+
return normalizeLogConversationId(trimmed);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface ConversationLogTotals {
|
|
105
|
+
requests: number;
|
|
106
|
+
totalTokens: number;
|
|
107
|
+
estimatedCostUsd: number;
|
|
108
|
+
pricedRequests: number;
|
|
109
|
+
unpricedRequests: number;
|
|
110
|
+
unmeteredRequests: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
type TotalsSource = {
|
|
114
|
+
totalTokens?: number;
|
|
115
|
+
usage?: { inputTokens?: number; outputTokens?: number; totalTokens?: number };
|
|
116
|
+
usageStatus?: string;
|
|
117
|
+
displayMetrics?: {
|
|
118
|
+
cost?:
|
|
119
|
+
| { kind: "value"; estimate: { cost: { total: number } } }
|
|
120
|
+
| { kind: "unavailable"; reason: string };
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
function rowTokenTotal(entry: TotalsSource): number | undefined {
|
|
125
|
+
if (typeof entry.totalTokens === "number" && Number.isFinite(entry.totalTokens) && entry.totalTokens >= 0) {
|
|
126
|
+
return entry.totalTokens;
|
|
127
|
+
}
|
|
128
|
+
const usageTotal = entry.usage?.totalTokens;
|
|
129
|
+
if (typeof usageTotal === "number" && Number.isFinite(usageTotal) && usageTotal >= 0) return usageTotal;
|
|
130
|
+
const input = entry.usage?.inputTokens;
|
|
131
|
+
const output = entry.usage?.outputTokens;
|
|
132
|
+
if (typeof input === "number" && typeof output === "number" && Number.isFinite(input) && Number.isFinite(output)) {
|
|
133
|
+
return Math.max(0, input) + Math.max(0, output);
|
|
134
|
+
}
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Sum tokens/cost for the currently loaded log slice matching a conversation filter. */
|
|
139
|
+
export function summarizeConversationLogs(entries: readonly TotalsSource[]): ConversationLogTotals {
|
|
140
|
+
let totalTokens = 0;
|
|
141
|
+
let estimatedCostUsd = 0;
|
|
142
|
+
let pricedRequests = 0;
|
|
143
|
+
let unpricedRequests = 0;
|
|
144
|
+
let unmeteredRequests = 0;
|
|
145
|
+
for (const entry of entries) {
|
|
146
|
+
const tokens = rowTokenTotal(entry);
|
|
147
|
+
if (tokens !== undefined) totalTokens += tokens;
|
|
148
|
+
if (entry.usageStatus === "unsupported") {
|
|
149
|
+
unmeteredRequests += 1;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const cost = entry.displayMetrics?.cost;
|
|
153
|
+
if (cost?.kind === "value" && Number.isFinite(cost.estimate.cost.total) && cost.estimate.cost.total >= 0) {
|
|
154
|
+
estimatedCostUsd += cost.estimate.cost.total;
|
|
155
|
+
pricedRequests += 1;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
unpricedRequests += 1;
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
requests: entries.length,
|
|
162
|
+
totalTokens,
|
|
163
|
+
estimatedCostUsd,
|
|
164
|
+
pricedRequests,
|
|
165
|
+
unpricedRequests,
|
|
166
|
+
unmeteredRequests,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import { CODEX_CONFIG_PATH, readRootTomlString } from "../codex/paths";
|
|
9
9
|
import { readCodexCatalogPath } from "../codex/catalog";
|
|
10
10
|
import type { OcxUsage } from "../types";
|
|
11
|
+
import type { AdapterRequest } from "../adapters/base";
|
|
11
12
|
import { redactSecretString } from "../lib/redact";
|
|
12
13
|
import {
|
|
13
14
|
appendUsageEntry,
|
|
@@ -28,17 +29,23 @@ import {
|
|
|
28
29
|
USAGE_DEBUG_BODY_SAMPLE_BYTES,
|
|
29
30
|
type UsageDebugBodyKind,
|
|
30
31
|
} from "../usage/debug";
|
|
32
|
+
import { matchesLogConversationId } from "./request-log-conversation";
|
|
31
33
|
|
|
32
34
|
export interface RequestLogContext {
|
|
33
35
|
model: string;
|
|
34
36
|
provider: string;
|
|
35
37
|
/** TTFT: ms from request start to the first non-empty model output delta (WP4, devlog 040). */
|
|
36
38
|
firstOutputMs?: number;
|
|
39
|
+
/** Best-effort chat/session correlation for Logs grouping (#330). Opaque; omit when unknown. */
|
|
40
|
+
conversationId?: string;
|
|
37
41
|
surface?: "claude" | "claude-desktop" | "grok";
|
|
38
42
|
requestedModel?: string;
|
|
39
43
|
/** Internal structural combo identity; omitted from RequestLogEntry/JSONL. */
|
|
40
44
|
comboId?: string;
|
|
41
45
|
requestedEffort?: string;
|
|
46
|
+
effectiveEffort?: string;
|
|
47
|
+
reasoningWireField?: string;
|
|
48
|
+
reasoningWireValue?: string | number;
|
|
42
49
|
requestedServiceTier?: string;
|
|
43
50
|
requestedSpeedLabel?: string;
|
|
44
51
|
configuredServiceTier?: string;
|
|
@@ -70,6 +77,8 @@ export interface RequestLogContext {
|
|
|
70
77
|
upstreamError?: string;
|
|
71
78
|
/** HTTP status derived from a terminal `response.failed` SSE payload (429/401/503/etc.). */
|
|
72
79
|
terminalHttpStatus?: number;
|
|
80
|
+
/** Structured reason from `response.incomplete`; internal-only input to log classification. */
|
|
81
|
+
terminalIncompleteReason?: string;
|
|
73
82
|
affinity?: "reused" | "new_bind" | "rebound" | "cleared";
|
|
74
83
|
transportPhase?: "pre_headers" | "mid_stream" | "terminal_sse";
|
|
75
84
|
terminalSource?: "upstream" | "synthetic";
|
|
@@ -83,8 +92,13 @@ export interface RequestLogEntry {
|
|
|
83
92
|
/** TTFT: ms from request start to the first non-empty model output delta; unset for non-streaming/tool-only. */
|
|
84
93
|
firstOutputMs?: number;
|
|
85
94
|
surface?: "claude" | "claude-desktop" | "grok";
|
|
95
|
+
/** Best-effort chat/session correlation for Logs grouping (#330). */
|
|
96
|
+
conversationId?: string;
|
|
86
97
|
requestedModel?: string;
|
|
87
98
|
requestedEffort?: string;
|
|
99
|
+
effectiveEffort?: string;
|
|
100
|
+
reasoningWireField?: string;
|
|
101
|
+
reasoningWireValue?: string | number;
|
|
88
102
|
requestedServiceTier?: string;
|
|
89
103
|
requestedSpeedLabel?: string;
|
|
90
104
|
configuredServiceTier?: string;
|
|
@@ -146,8 +160,12 @@ export function requestLogEntryFromPersistedUsage(entry: PersistedUsageEntry): R
|
|
|
146
160
|
provider: entry.provider,
|
|
147
161
|
...(entry.firstOutputMs !== undefined ? { firstOutputMs: entry.firstOutputMs } : {}),
|
|
148
162
|
...(isKnownUsageSurface(entry.surface) ? { surface: entry.surface } : {}),
|
|
163
|
+
...(entry.conversationId ? { conversationId: entry.conversationId } : {}),
|
|
149
164
|
...(entry.requestedModel ? { requestedModel: entry.requestedModel } : {}),
|
|
150
165
|
...(entry.requestedEffort ? { requestedEffort: entry.requestedEffort } : {}),
|
|
166
|
+
...(entry.effectiveEffort ? { effectiveEffort: entry.effectiveEffort } : {}),
|
|
167
|
+
...(entry.reasoningWireField ? { reasoningWireField: entry.reasoningWireField } : {}),
|
|
168
|
+
...(entry.reasoningWireValue !== undefined ? { reasoningWireValue: entry.reasoningWireValue } : {}),
|
|
151
169
|
...(entry.requestedServiceTier ? { requestedServiceTier: entry.requestedServiceTier } : {}),
|
|
152
170
|
...(entry.requestedSpeedLabel ? { requestedSpeedLabel: entry.requestedSpeedLabel } : {}),
|
|
153
171
|
...(entry.configuredServiceTier ? { configuredServiceTier: entry.configuredServiceTier } : {}),
|
|
@@ -222,9 +240,13 @@ export function addRequestLog(entry: RequestLogEntry) {
|
|
|
222
240
|
provider: entry.provider,
|
|
223
241
|
model: entry.model,
|
|
224
242
|
...(isKnownUsageSurface(entry.surface) ? { surface: entry.surface } : {}),
|
|
243
|
+
...(entry.conversationId ? { conversationId: entry.conversationId } : {}),
|
|
225
244
|
...(entry.resolvedModel ? { resolvedModel: entry.resolvedModel } : {}),
|
|
226
245
|
...(entry.requestedModel ? { requestedModel: entry.requestedModel } : {}),
|
|
227
246
|
...(entry.requestedEffort ? { requestedEffort: entry.requestedEffort } : {}),
|
|
247
|
+
...(entry.effectiveEffort ? { effectiveEffort: entry.effectiveEffort } : {}),
|
|
248
|
+
...(entry.reasoningWireField ? { reasoningWireField: entry.reasoningWireField } : {}),
|
|
249
|
+
...(entry.reasoningWireValue !== undefined ? { reasoningWireValue: entry.reasoningWireValue } : {}),
|
|
228
250
|
...(entry.requestedServiceTier ? { requestedServiceTier: entry.requestedServiceTier } : {}),
|
|
229
251
|
...(entry.requestedSpeedLabel ? { requestedSpeedLabel: entry.requestedSpeedLabel } : {}),
|
|
230
252
|
...(entry.configuredServiceTier ? { configuredServiceTier: entry.configuredServiceTier } : {}),
|
|
@@ -272,6 +294,71 @@ export function recordFirstOutput(
|
|
|
272
294
|
}
|
|
273
295
|
}
|
|
274
296
|
|
|
297
|
+
/** Snapshot target-specific requested effort even for runTurn adapters with no AdapterRequest. */
|
|
298
|
+
export function recordAttemptRequestedEffort(logCtx: RequestLogContext): void {
|
|
299
|
+
const attempt = logCtx.activeAttempt;
|
|
300
|
+
if (!attempt) return;
|
|
301
|
+
delete attempt.requestedEffort;
|
|
302
|
+
try {
|
|
303
|
+
if (typeof logCtx.requestedEffort === "string" && logCtx.requestedEffort) {
|
|
304
|
+
attempt.requestedEffort = redactSecretString(logCtx.requestedEffort).slice(0, 64);
|
|
305
|
+
}
|
|
306
|
+
} catch {
|
|
307
|
+
// Request logging is best-effort and must not affect request delivery.
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Copy the adapter's exact outbound reasoning parameter into the durable request log. */
|
|
312
|
+
export function recordAdapterReasoning(
|
|
313
|
+
logCtx: RequestLogContext,
|
|
314
|
+
request: AdapterRequest,
|
|
315
|
+
): void {
|
|
316
|
+
delete logCtx.effectiveEffort;
|
|
317
|
+
delete logCtx.reasoningWireField;
|
|
318
|
+
delete logCtx.reasoningWireValue;
|
|
319
|
+
const attempt = logCtx.activeAttempt;
|
|
320
|
+
if (attempt) {
|
|
321
|
+
delete attempt.effectiveEffort;
|
|
322
|
+
delete attempt.reasoningWireField;
|
|
323
|
+
delete attempt.reasoningWireValue;
|
|
324
|
+
}
|
|
325
|
+
recordAttemptRequestedEffort(logCtx);
|
|
326
|
+
|
|
327
|
+
// Diagnostics must never make an otherwise valid upstream request fail. Config files
|
|
328
|
+
// written by older versions (or edited by hand) can contain values that violate the
|
|
329
|
+
// current TypeScript shape, so validate the runtime object before redacting strings.
|
|
330
|
+
try {
|
|
331
|
+
const raw: unknown = request.reasoningLog;
|
|
332
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
|
|
333
|
+
const reasoning = raw as Record<string, unknown>;
|
|
334
|
+
if (typeof reasoning.effectiveEffort !== "string" || !reasoning.effectiveEffort
|
|
335
|
+
|| (reasoning.wireField !== "reasoning_effort"
|
|
336
|
+
&& reasoning.wireField !== "thinking_budget"
|
|
337
|
+
&& reasoning.wireField !== "thinking.type")
|
|
338
|
+
|| (!(typeof reasoning.wireValue === "string" && reasoning.wireValue)
|
|
339
|
+
&& !(typeof reasoning.wireValue === "number"
|
|
340
|
+
&& Number.isFinite(reasoning.wireValue)
|
|
341
|
+
&& reasoning.wireValue >= 0))) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const effectiveEffort = redactSecretString(reasoning.effectiveEffort).slice(0, 64);
|
|
346
|
+
const wireValue = typeof reasoning.wireValue === "string"
|
|
347
|
+
? redactSecretString(reasoning.wireValue).slice(0, 64)
|
|
348
|
+
: reasoning.wireValue;
|
|
349
|
+
logCtx.effectiveEffort = effectiveEffort;
|
|
350
|
+
logCtx.reasoningWireField = reasoning.wireField;
|
|
351
|
+
logCtx.reasoningWireValue = wireValue;
|
|
352
|
+
if (attempt) {
|
|
353
|
+
attempt.effectiveEffort = effectiveEffort;
|
|
354
|
+
attempt.reasoningWireField = reasoning.wireField;
|
|
355
|
+
attempt.reasoningWireValue = wireValue;
|
|
356
|
+
}
|
|
357
|
+
} catch {
|
|
358
|
+
// Request logging is best-effort and must not affect request delivery.
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
275
362
|
export function requestLogErrorCode(status: number, upstreamError?: string): string | undefined {
|
|
276
363
|
if (status >= 200 && status < 400) return undefined;
|
|
277
364
|
// Defense in depth: mid-stream web-search aborts used to land as 502 with this message.
|
|
@@ -447,7 +534,7 @@ export function inspectResponseLogSsePayload(logCtx: RequestLogContext, payload:
|
|
|
447
534
|
* run it through redactSecretString so secrets never reach /api/logs. Pure; safe on any text.
|
|
448
535
|
*/
|
|
449
536
|
function captureUpstreamError(logCtx: RequestLogContext, text: string | null): void {
|
|
450
|
-
if (!text
|
|
537
|
+
if (!text) return;
|
|
451
538
|
try {
|
|
452
539
|
const json = JSON.parse(text) as {
|
|
453
540
|
type?: unknown;
|
|
@@ -459,6 +546,14 @@ function captureUpstreamError(logCtx: RequestLogContext, text: string | null): v
|
|
|
459
546
|
};
|
|
460
547
|
};
|
|
461
548
|
captureTerminalHttpStatus(logCtx, json);
|
|
549
|
+
const reason = json?.response?.incomplete_details?.reason;
|
|
550
|
+
if (json.type === "response.incomplete"
|
|
551
|
+
&& logCtx.terminalIncompleteReason === undefined
|
|
552
|
+
&& typeof reason === "string"
|
|
553
|
+
&& reason.trim()) {
|
|
554
|
+
logCtx.terminalIncompleteReason = reason.trim();
|
|
555
|
+
}
|
|
556
|
+
if (logCtx.upstreamError) return;
|
|
462
557
|
const message = json?.error?.message
|
|
463
558
|
?? json?.last_error?.message
|
|
464
559
|
?? json?.response?.error?.message;
|
|
@@ -470,11 +565,11 @@ function captureUpstreamError(logCtx: RequestLogContext, text: string | null): v
|
|
|
470
565
|
// the bridge on a stall-timeout or adapter EOF (response.incomplete). Maps the raw reason to a
|
|
471
566
|
// reader-facing label so a generic 502 in /api/logs explains WHY the turn ended, not just the
|
|
472
567
|
// mapped HTTP code.
|
|
473
|
-
const reason = json?.response?.incomplete_details?.reason;
|
|
474
568
|
if (typeof reason === "string" && reason.trim()) {
|
|
475
569
|
logCtx.upstreamError = redactSecretString(incompleteReasonLabel(reason.trim())).slice(0, 500);
|
|
476
570
|
}
|
|
477
571
|
} catch {
|
|
572
|
+
if (logCtx.upstreamError) return;
|
|
478
573
|
const trimmed = text.trim();
|
|
479
574
|
if (trimmed) {
|
|
480
575
|
logCtx.upstreamError = redactSecretString(trimmed).slice(0, 500);
|
|
@@ -485,6 +580,8 @@ function captureUpstreamError(logCtx: RequestLogContext, text: string | null): v
|
|
|
485
580
|
/** Map a raw `incomplete_details.reason` (emitted by the bridge) to a reader-facing label. */
|
|
486
581
|
function incompleteReasonLabel(reason: string): string {
|
|
487
582
|
switch (reason) {
|
|
583
|
+
case "max_output_tokens":
|
|
584
|
+
return `Output reached the requested token limit (${reason})`;
|
|
488
585
|
case "upstream_stall_timeout":
|
|
489
586
|
return `Upstream stalled: no data for the stall-timeout window (${reason})`;
|
|
490
587
|
case "adapter_eof":
|
|
@@ -529,6 +626,21 @@ export function httpStatusForRequestLogTerminal(
|
|
|
529
626
|
status: ResponsesTerminalStatus,
|
|
530
627
|
logCtx?: RequestLogContext,
|
|
531
628
|
): number {
|
|
629
|
+
/**
|
|
630
|
+
* [Decision Log]
|
|
631
|
+
* - 목적과 의도: Keep request logs aligned with the successful HTTP/SSE contract.
|
|
632
|
+
* - 기존 구현 및 제약 조건: All incomplete terminals were recorded as 502 even when the
|
|
633
|
+
* client-requested output limit was reached normally.
|
|
634
|
+
* - 검토한 주요 대안: Treat every incomplete as success, or infer the reason from display text.
|
|
635
|
+
* - 선택한 방식: Only structured max_output_tokens incompletes map to 200.
|
|
636
|
+
* - 다른 대안 대신 이 방식을 선택한 이유: Stall, EOF, and unknown incompletes must remain
|
|
637
|
+
* visible failures, and display text is not a stable classification contract.
|
|
638
|
+
* - 장점, 단점 및 영향: Logs stop reporting false upstream errors while retaining the
|
|
639
|
+
* incomplete terminal detail; native callers without a structured reason keep old behavior.
|
|
640
|
+
*/
|
|
641
|
+
if (status === "incomplete" && logCtx?.terminalIncompleteReason === "max_output_tokens") {
|
|
642
|
+
return 200;
|
|
643
|
+
}
|
|
532
644
|
if (status === "failed" && logCtx?.terminalHttpStatus !== undefined) {
|
|
533
645
|
return logCtx.terminalHttpStatus;
|
|
534
646
|
}
|
|
@@ -583,8 +695,12 @@ export function addFinalRequestLog(
|
|
|
583
695
|
model: isCombo ? logCtx.requestedModel! : logCtx.model,
|
|
584
696
|
provider: isCombo ? "combo" : logCtx.provider,
|
|
585
697
|
...(logCtx.surface ? { surface: logCtx.surface } : {}),
|
|
698
|
+
...(logCtx.conversationId ? { conversationId: logCtx.conversationId } : {}),
|
|
586
699
|
...(logCtx.requestedModel ? { requestedModel: logCtx.requestedModel } : {}),
|
|
587
700
|
...(logCtx.requestedEffort ? { requestedEffort: logCtx.requestedEffort } : {}),
|
|
701
|
+
...(logCtx.effectiveEffort ? { effectiveEffort: logCtx.effectiveEffort } : {}),
|
|
702
|
+
...(logCtx.reasoningWireField ? { reasoningWireField: logCtx.reasoningWireField } : {}),
|
|
703
|
+
...(logCtx.reasoningWireValue !== undefined ? { reasoningWireValue: logCtx.reasoningWireValue } : {}),
|
|
588
704
|
...(logCtx.requestedServiceTier ? { requestedServiceTier: logCtx.requestedServiceTier } : {}),
|
|
589
705
|
...(logCtx.requestedSpeedLabel ? { requestedSpeedLabel: logCtx.requestedSpeedLabel } : {}),
|
|
590
706
|
...(logCtx.configuredServiceTier ? { configuredServiceTier: logCtx.configuredServiceTier } : {}),
|
|
@@ -629,6 +745,10 @@ export function filterRequestLogs(logs: RequestLogEntry[], params: URLSearchPara
|
|
|
629
745
|
filtered = filtered.filter(entry => entry.provider === provider
|
|
630
746
|
|| entry.attempts?.some(attempt => attempt.provider === provider));
|
|
631
747
|
}
|
|
748
|
+
const conversationId = params.get("conversationId")?.trim() || params.get("conversation")?.trim();
|
|
749
|
+
if (conversationId) {
|
|
750
|
+
filtered = filtered.filter(entry => matchesLogConversationId(entry.conversationId, conversationId));
|
|
751
|
+
}
|
|
632
752
|
const status = params.get("status")?.trim().toLowerCase();
|
|
633
753
|
if (status) {
|
|
634
754
|
filtered = /^[1-5]xx$/.test(status)
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
} from "../../combos";
|
|
28
28
|
import { isInjectionDebugEnabled } from "../../lib/debug-settings";
|
|
29
29
|
import { injectionDebugLog } from "../../lib/injection-debug-log";
|
|
30
|
+
import { resolveClientRetryAfter } from "../../lib/retry-after";
|
|
30
31
|
import { modelInList, namespacedToolName } from "../../types";
|
|
31
32
|
import type { AdapterEvent, OcxConfig, OcxParsedRequest, OcxProviderConfig, OcxProviderContinuationState, OcxUsage } from "../../types";
|
|
32
33
|
import {
|
|
@@ -92,11 +93,18 @@ import {
|
|
|
92
93
|
inspectResponseLogJson,
|
|
93
94
|
noteAttemptSend,
|
|
94
95
|
readConfiguredCodexServiceTier,
|
|
96
|
+
recordAdapterReasoning,
|
|
97
|
+
recordAttemptRequestedEffort,
|
|
95
98
|
requestLogSpeedLabel,
|
|
96
99
|
sealRequestAttemptIdentity,
|
|
97
100
|
usageFromResponsesPayload,
|
|
98
101
|
type RequestLogContext,
|
|
99
102
|
} from "../request-log";
|
|
103
|
+
import {
|
|
104
|
+
conversationIdFromResponsesRequest,
|
|
105
|
+
normalizeLogConversationId,
|
|
106
|
+
sessionIdHeaderFromRequest,
|
|
107
|
+
} from "../request-log-conversation";
|
|
100
108
|
import type { AttemptRecoveryKind } from "../../usage/log";
|
|
101
109
|
import {
|
|
102
110
|
consumeForInspection,
|
|
@@ -347,14 +355,30 @@ export async function consumeComboFailure(
|
|
|
347
355
|
const message = classificationText === fallback
|
|
348
356
|
? fallback
|
|
349
357
|
: `${fallback}: ${classificationText}`;
|
|
350
|
-
const
|
|
358
|
+
const upstreamRetryAfter = response.headers.get("retry-after");
|
|
359
|
+
// Client response may get the synthetic "2" fallback; cooldown metadata must not —
|
|
360
|
+
// otherwise coolComboTarget treats it as a 2s cooldown instead of the 60s default.
|
|
361
|
+
const clientRetryAfter = resolveClientRetryAfter({
|
|
362
|
+
status: response.status,
|
|
363
|
+
message,
|
|
364
|
+
upstreamRetryAfter,
|
|
365
|
+
now,
|
|
366
|
+
});
|
|
367
|
+
const cooldownRetryAfter = resolveClientRetryAfter({
|
|
368
|
+
status: response.status,
|
|
369
|
+
message,
|
|
370
|
+
upstreamRetryAfter,
|
|
371
|
+
now,
|
|
372
|
+
includeDefault: false,
|
|
373
|
+
});
|
|
351
374
|
return {
|
|
352
375
|
response: formatErrorResponse(response.status, "upstream_error", message, {
|
|
353
376
|
...(upstreamCode !== undefined ? { code: upstreamCode } : {}),
|
|
377
|
+
...(clientRetryAfter !== undefined ? { retryAfter: clientRetryAfter } : {}),
|
|
354
378
|
}),
|
|
355
379
|
classificationText,
|
|
356
380
|
...(upstreamCode !== undefined ? { upstreamCode } : {}),
|
|
357
|
-
...(
|
|
381
|
+
...(cooldownRetryAfter !== undefined ? { retryAfter: cooldownRetryAfter } : {}),
|
|
358
382
|
...(usage ? { usage } : {}),
|
|
359
383
|
};
|
|
360
384
|
}
|
|
@@ -593,6 +617,7 @@ async function applyFinalRouteRequestNormalization(args: {
|
|
|
593
617
|
logCtx.requestedEffort = `${logCtx.requestedEffort ?? "max"}->${clamped}`;
|
|
594
618
|
}
|
|
595
619
|
}
|
|
620
|
+
recordAttemptRequestedEffort(logCtx);
|
|
596
621
|
logCtx.modelSupportsServiceTier = catalogModelSupportsServiceTier(
|
|
597
622
|
route.modelId,
|
|
598
623
|
logCtx.requestedServiceTier ?? logCtx.configuredServiceTier,
|
|
@@ -622,6 +647,19 @@ export async function handleComboResponses(
|
|
|
622
647
|
if (!combo) {
|
|
623
648
|
return formatErrorResponse(404, "invalid_request_error", `Unknown combo: ${comboId}`);
|
|
624
649
|
}
|
|
650
|
+
const adoptFailedChildLog = (childLog: RequestLogContext): void => {
|
|
651
|
+
// Attempts remain the complete physical history; the logical row mirrors the most recent
|
|
652
|
+
// failed target so an exhausted combo still has useful top-level reasoning diagnostics.
|
|
653
|
+
Object.assign(logCtx, childLog, {
|
|
654
|
+
requestedModel,
|
|
655
|
+
model: requestedModel,
|
|
656
|
+
provider: "combo",
|
|
657
|
+
comboId,
|
|
658
|
+
attempts: logCtx.attempts,
|
|
659
|
+
activeAttempt: undefined,
|
|
660
|
+
activeAttemptStartedAt: undefined,
|
|
661
|
+
});
|
|
662
|
+
};
|
|
625
663
|
|
|
626
664
|
const unreadableEncryptedAgentTask = hasUnreadableEncryptedAgentTask(
|
|
627
665
|
(rawBody as { input?: unknown } | undefined)?.input,
|
|
@@ -658,6 +696,8 @@ export async function handleComboResponses(
|
|
|
658
696
|
const childLog: RequestLogContext = {
|
|
659
697
|
model: pick.target.model,
|
|
660
698
|
provider: pick.target.provider,
|
|
699
|
+
...(logCtx.conversationId ? { conversationId: logCtx.conversationId } : {}),
|
|
700
|
+
...(logCtx.surface ? { surface: logCtx.surface } : {}),
|
|
661
701
|
};
|
|
662
702
|
const targetRoute = routeModel(config, `${pick.target.provider}/${pick.target.model}`);
|
|
663
703
|
const childBody = concreteComboRequestBody(
|
|
@@ -792,25 +832,19 @@ export async function handleComboResponses(
|
|
|
792
832
|
if (comboFailureDecision(failure.response.status, failure.classificationText, {
|
|
793
833
|
code: failure.upstreamCode,
|
|
794
834
|
}) === "stop") {
|
|
795
|
-
|
|
796
|
-
requestedModel,
|
|
797
|
-
model: requestedModel,
|
|
798
|
-
provider: "combo",
|
|
799
|
-
comboId,
|
|
800
|
-
attempts: logCtx.attempts,
|
|
801
|
-
activeAttempt: undefined,
|
|
802
|
-
activeAttemptStartedAt: undefined,
|
|
803
|
-
});
|
|
835
|
+
adoptFailedChildLog(childLog);
|
|
804
836
|
return lastFailure;
|
|
805
837
|
}
|
|
806
838
|
console.warn(
|
|
807
839
|
`[combo] ${comboId}: ${targetKey(pick.target)} failed with ${response.status} after ${Date.now() - started}ms`,
|
|
808
840
|
);
|
|
809
|
-
|
|
841
|
+
const nextPick = advanceComboAfterFailure(config, pick, {
|
|
810
842
|
retryAfter: failure.retryAfter,
|
|
811
843
|
now: Date.now(),
|
|
812
844
|
eligible: payloadEligible,
|
|
813
845
|
});
|
|
846
|
+
if (!nextPick) adoptFailedChildLog(childLog);
|
|
847
|
+
pick = nextPick;
|
|
814
848
|
}
|
|
815
849
|
return lastFailure!;
|
|
816
850
|
}
|
|
@@ -866,6 +900,16 @@ export async function handleResponses(
|
|
|
866
900
|
} catch (err) {
|
|
867
901
|
return formatErrorResponse(400, "invalid_request_error", err instanceof Error ? err.message : String(err));
|
|
868
902
|
}
|
|
903
|
+
// Prefer a pre-populated id (routed Claude) over Responses headers that may be
|
|
904
|
+
// absent or synthetically injected (session_id from prompt_cache_key).
|
|
905
|
+
if (!logCtx.conversationId) {
|
|
906
|
+
logCtx.conversationId = conversationIdFromResponsesRequest({
|
|
907
|
+
clientThreadId: parsed._clientThreadId,
|
|
908
|
+
sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
|
|
909
|
+
threadIdHeader: req.headers.get("thread-id"),
|
|
910
|
+
cursorConversationId: parsed._cursorConversationId,
|
|
911
|
+
});
|
|
912
|
+
}
|
|
869
913
|
logCtx.requestedModel = parsed.modelId;
|
|
870
914
|
logCtx.requestedEffort = parsed.options.reasoning;
|
|
871
915
|
logCtx.requestedServiceTier = parsed.options.serviceTier;
|
|
@@ -1124,6 +1168,7 @@ export async function handleResponses(
|
|
|
1124
1168
|
);
|
|
1125
1169
|
}
|
|
1126
1170
|
let request = await adapter.buildRequest(parsed, { headers: selectedForwardHeaders });
|
|
1171
|
+
recordAdapterReasoning(logCtx, request);
|
|
1127
1172
|
const passthroughEstimate = typeof request.usageLog?.inputTokens === "number"
|
|
1128
1173
|
? request.usageLog.inputTokens
|
|
1129
1174
|
: undefined;
|
|
@@ -1213,6 +1258,7 @@ export async function handleResponses(
|
|
|
1213
1258
|
config.cacheRetention,
|
|
1214
1259
|
);
|
|
1215
1260
|
request = await retryAdapter.buildRequest(parsed, { headers: retryHeaders });
|
|
1261
|
+
recordAdapterReasoning(logCtx, request);
|
|
1216
1262
|
|
|
1217
1263
|
await upstreamResponse.body?.cancel().catch(() => undefined);
|
|
1218
1264
|
authCtx = retryAuthCtx;
|
|
@@ -1500,6 +1546,11 @@ export async function handleResponses(
|
|
|
1500
1546
|
message: err instanceof Error ? err.message : String(err),
|
|
1501
1547
|
});
|
|
1502
1548
|
} finally {
|
|
1549
|
+
// Cursor assigns a stable conversation id inside runTurn on the first headerless
|
|
1550
|
+
// turn; backfill so Logs can filter/total that opening request (#330 / #522).
|
|
1551
|
+
if (!logCtx.conversationId && parsed._cursorConversationId) {
|
|
1552
|
+
logCtx.conversationId = normalizeLogConversationId(parsed._cursorConversationId);
|
|
1553
|
+
}
|
|
1503
1554
|
queue.close();
|
|
1504
1555
|
}
|
|
1505
1556
|
};
|
|
@@ -1614,6 +1665,7 @@ export async function handleResponses(
|
|
|
1614
1665
|
forceEmptyResponseId: true,
|
|
1615
1666
|
abortSignal: options.abortSignal,
|
|
1616
1667
|
...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
|
|
1668
|
+
onRequestBuilt: request => recordAdapterReasoning(logCtx, request),
|
|
1617
1669
|
onUsage: usage => {
|
|
1618
1670
|
logCtx.usageFromBridge = true;
|
|
1619
1671
|
if (usage) {
|
|
@@ -1658,6 +1710,7 @@ export async function handleResponses(
|
|
|
1658
1710
|
let activeAdapter = adapter;
|
|
1659
1711
|
|
|
1660
1712
|
const request = await activeAdapter.buildRequest(parsed, { headers: selectedForwardHeaders });
|
|
1713
|
+
recordAdapterReasoning(logCtx, request);
|
|
1661
1714
|
const inputTokenEstimate = typeof request.usageLog?.inputTokens === "number"
|
|
1662
1715
|
? request.usageLog.inputTokens
|
|
1663
1716
|
: undefined;
|
|
@@ -1710,6 +1763,7 @@ export async function handleResponses(
|
|
|
1710
1763
|
headers: selectedForwardHeaders,
|
|
1711
1764
|
...(imageTierBias > 0 ? { imageTierBias } : {}),
|
|
1712
1765
|
});
|
|
1766
|
+
recordAdapterReasoning(logCtx, retryRequest);
|
|
1713
1767
|
const retryEstimate = typeof retryRequest.usageLog?.inputTokens === "number"
|
|
1714
1768
|
? retryRequest.usageLog.inputTokens
|
|
1715
1769
|
: undefined;
|
|
@@ -1830,7 +1884,15 @@ export async function handleResponses(
|
|
|
1830
1884
|
);
|
|
1831
1885
|
// Upstreams occasionally echo request details in error bodies — scrub token-shaped
|
|
1832
1886
|
// material before it reaches the client-facing error surface.
|
|
1833
|
-
|
|
1887
|
+
const message = `Provider error ${upstreamResponse.status}: ${redactSecretString(errorText.slice(0, 500))}`;
|
|
1888
|
+
const retryAfter = resolveClientRetryAfter({
|
|
1889
|
+
status: upstreamResponse.status,
|
|
1890
|
+
message,
|
|
1891
|
+
upstreamRetryAfter: upstreamResponse.headers.get("retry-after"),
|
|
1892
|
+
});
|
|
1893
|
+
return formatErrorResponse(upstreamResponse.status, "upstream_error", message, {
|
|
1894
|
+
...(retryAfter !== undefined ? { retryAfter } : {}),
|
|
1895
|
+
});
|
|
1834
1896
|
}
|
|
1835
1897
|
}
|
|
1836
1898
|
|
|
@@ -1850,6 +1912,7 @@ export async function handleResponses(
|
|
|
1850
1912
|
headers: selectedForwardHeaders,
|
|
1851
1913
|
...(imageTierBias > 0 ? { imageTierBias } : {}),
|
|
1852
1914
|
});
|
|
1915
|
+
recordAdapterReasoning(logCtx, continuationRequest);
|
|
1853
1916
|
const continuationEstimate = typeof continuationRequest.usageLog?.inputTokens === "number"
|
|
1854
1917
|
? continuationRequest.usageLog.inputTokens
|
|
1855
1918
|
: undefined;
|