From d48dc994173fde6530892573d891081435a77847 Mon Sep 17 00:00:00 2001 From: t Date: Sun, 6 Sep 2026 19:01:17 +0900 Subject: [PATCH 1/4] feat(usage): record resolved xAI credential source per attempt Carry #3642 and rederive provenance after transport rebuilds while preserving finalized combo child attribution. Co-authored-by: olddonkey --- .../src/content/docs/reference/adapters.md | 6 +++ .../content/docs/reference/management-api.md | 8 ++++ src/server/chat-native.ts | 20 +++++---- src/server/request-log.ts | 30 ++++++++++++- src/server/responses/core.ts | 12 ++++++ src/usage/log.ts | 9 ++++ .../server-xai-oauth-401-replay.test.ts | 42 +++++++++++++++++- tests/usage/request-log.test.ts | 43 +++++++++++++++++++ tests/usage/usage-log.test.ts | 22 ++++++++++ 9 files changed, 182 insertions(+), 10 deletions(-) diff --git a/docs-site/src/content/docs/reference/adapters.md b/docs-site/src/content/docs/reference/adapters.md index e69875ed92..03c56f329b 100644 --- a/docs-site/src/content/docs/reference/adapters.md +++ b/docs-site/src/content/docs/reference/adapters.md @@ -42,6 +42,12 @@ additional permissions. Native passthrough and compaction retain their raw-body provider — xAI, Kimi, DeepSeek, GLM, Groq, OpenRouter, Ollama (local), and more. **Auth:** `key` (Bearer). +For xAI, the resolved upstream adapter can be `openai-chat` or `openai-responses`, +depending on model defaults and explicit `modelAdapters` overrides. Both support +public xAI API-key authentication and Grok CLI OAuth. The usage log's +[`attempts[].credentialSource`](/reference/management-api/) follows that resolved +transport; it does not infer subscription attribution from the inbound protocol. + - Converts internal messages to OpenAI roles; maps tools to `{type:"function", function:{…}}` and `tool_choice` (`auto`/`none`/`required` or a named function). - **Tool-result images** ride in a follow-up user vision message (`image_url` parts) released once diff --git a/docs-site/src/content/docs/reference/management-api.md b/docs-site/src/content/docs/reference/management-api.md index 2a3ddd2787..a12303cfd7 100644 --- a/docs-site/src/content/docs/reference/management-api.md +++ b/docs-site/src/content/docs/reference/management-api.md @@ -185,6 +185,14 @@ See [Combos](/guides/combos/) for target strategies, cooldowns, aliases, and rou | `POST /api/storage/cleanup-policy/run` | Start a manual cleanup-policy run | 409 `already_running`; 500 `cleanup_failed` | | `GET /api/storage/cleanup-policy/test-stream` | Test-only policy stream hook | 404 `not_found` when unavailable | +New xAI attempts in `usage.jsonl` include a request-time `credentialSource`: `grok-oauth` +for the resolved Grok CLI OAuth transport, or `xai-api-key` for the public xAI API key +transport. This fixed label contains no credential or account identifier. It belongs to +each item in `attempts`, so a combo's aggregate token total must not be attributed to its +final provider. Custom destinations and historic rows omit the field; consumers must not +infer subscription usage from the current configuration, model name, or inbound API key. +The log reports usage, not subscription invoice amounts. + `GET /api/usage` reads `~/.opencodex/usage.jsonl` from the beginning through the current ledger snapshot on a cold start. It processes fixed 1 MiB chunks and retains compact aggregate state rather than every normalized request row. Later refreshes validate the previous line boundary and fold only diff --git a/src/server/chat-native.ts b/src/server/chat-native.ts index 32f3daea8a..b99267bdde 100644 --- a/src/server/chat-native.ts +++ b/src/server/chat-native.ts @@ -46,6 +46,7 @@ import { beginRequestAttempt, noteAttemptSend, recordFirstOutput, + recordAttemptCredentialSource, sealRequestAttemptIdentity, type RequestLogContext, } from "./request-log"; @@ -183,14 +184,17 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio translatorBudget.chargeRetained(bytes, { kind: "request_copies" }); retainedRequestBytes = bytes; }; - const buildActiveRequest = () => buildOpenAIChatPassthroughRequest( - activeProvider, - options.chatBody, - route.modelId, - requestedStream, - fastPolicyForModel(activeProvider, route.modelId, route.providerName, "chat"), - config.fastMode, - ); + const buildActiveRequest = () => { + recordAttemptCredentialSource(attempt, route.providerName, activeProvider); + return buildOpenAIChatPassthroughRequest( + activeProvider, + options.chatBody, + route.modelId, + requestedStream, + fastPolicyForModel(activeProvider, route.modelId, route.providerName, "chat"), + config.fastMode, + ); + }; try { activeRequest = buildActiveRequest(); retainRequest(activeRequest); diff --git a/src/server/request-log.ts b/src/server/request-log.ts index 431ea6ccdd..a4c942bd88 100644 --- a/src/server/request-log.ts +++ b/src/server/request-log.ts @@ -12,7 +12,7 @@ import { } from "../lib/errors"; import { CODEX_CONFIG_PATH, readRootTomlString } from "../codex/paths"; import { readCodexCatalogPath } from "../codex/catalog"; -import type { AttemptTierOutcome, OcxUsage } from "../types"; +import type { AttemptTierOutcome, OcxProviderConfig, OcxUsage } from "../types"; import { normalizeRouteDecisionTrace, type RouteDecisionTraceV1 } from "../routing/trace"; import type { AdapterRequest } from "../adapters/base"; import type { AdapterTierMetadata } from "../providers/fastwire"; @@ -1211,11 +1211,39 @@ export function sealRequestAttemptIdentity( accountLogLabel?: string, ): void { if (!attempt) return; + if (attempt.provider !== provider || attempt.adapter !== adapter) delete attempt.credentialSource; attempt.provider = provider; attempt.adapter = adapter; if (isCodexUsageAccountLogLabel(accountLogLabel)) attempt.accountLogLabel = accountLogLabel; } +/** Capture only the resolved upstream route; inbound auth and today's config cannot label old usage. */ +export function recordAttemptCredentialSource( + attempt: PersistedUsageAttempt | undefined, + providerName: string, + provider: Pick, + adapterName: string = provider.adapter, +): void { + if (!attempt) return; + // Rebinding an attempt to an unrecognized route must not retain its previous attribution. + delete attempt.credentialSource; + if (providerName !== "xai" + || !["openai-chat", "openai-responses"].includes(adapterName)) return; + try { + const url = new URL(provider.baseUrl ?? ""); + if (url.protocol !== "https:" || url.port || url.username || url.password + || url.search || url.hash || !["/v1", "/v1/"].includes(url.pathname)) return; + if (provider.authMode === "oauth" && url.hostname === "cli-chat-proxy.grok.com") { + attempt.credentialSource = "grok-oauth"; + } else if ((provider.authMode === "key" || provider.authMode === undefined) + && url.hostname === "api.x.ai") { + attempt.credentialSource = "xai-api-key"; + } + } catch { + // Invalid/custom destinations have no known subscription provenance. + } +} + export function noteAttemptSend( attempt: PersistedUsageAttempt | undefined, inputTokenEstimate: number | undefined, diff --git a/src/server/responses/core.ts b/src/server/responses/core.ts index 833bc75478..2fc7cf678c 100644 --- a/src/server/responses/core.ts +++ b/src/server/responses/core.ts @@ -292,6 +292,7 @@ import { recordAttemptRequestedEffort, requestLogSpeedLabel, sealRequestAttemptIdentity, + recordAttemptCredentialSource, usageFromResponsesPayload, type RequestLogContext, } from "../request-log"; @@ -1422,6 +1423,7 @@ async function retryCodexPoolOnAlternateAccount( retryAdapter.name, logCtx.accountLogLabel, ); + recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, retryAdapter.name); const retrySameConfirmedAccount = outcomeStatus === 400 && ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(route.modelId) @@ -3925,6 +3927,7 @@ async function handleResponsesInner( (logCtx.attempts ??= []).push(attempt); } sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, adapter.name, logCtx.accountLogLabel); + recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, adapterProvider); let runTurnAdapter = adapter; if (adapter.runTurn) { recordAdapterTierMetadata(logCtx, adapter.tierLogForRunTurn?.(parsed)); @@ -4590,6 +4593,7 @@ async function handleResponsesInner( retryAdapter.name, logCtx.accountLogLabel, ); + recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, retryAdapter.name); const rebuiltBodyRefusal = refuseOversizedOutboundBody(request); if (rebuiltBodyRefusal) return { failed: rebuiltBodyRefusal }; try { @@ -4678,6 +4682,7 @@ async function handleResponsesInner( }); logCtx.providerAdapter = replayAdapter.name; sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, replayAdapter.name, logCtx.accountLogLabel); + recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, replayAdapter.name); try { request = await replayAdapter.buildRequest(parsed, { headers: selectedForwardHeaders, @@ -4797,6 +4802,7 @@ async function handleResponsesInner( refreshedAdapter.name, logCtx.accountLogLabel, ); + recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, refreshedAdapter.name); try { request = await refreshedAdapter.buildRequest(parsed, { headers: selectedForwardHeaders, @@ -6123,6 +6129,7 @@ async function handleResponsesInner( forwardHeaders: selectedForwardHeaders, }); sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, rotatedAdapter.name, logCtx.accountLogLabel); + recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, rotatedAdapter.name); return true; } catch { return false; @@ -6542,6 +6549,7 @@ async function handleResponsesInner( if (retryEstimate !== undefined) logCtx.usageLogInputTokens = retryEstimate; logCtx.providerAdapter = activeAdapter.name; sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel); + recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, activeAdapter.name); noteAttemptSend(logCtx.activeAttempt, retryEstimate, recovery); try { try { @@ -6789,6 +6797,7 @@ async function handleResponsesInner( config.cacheRetention, ); sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel); + recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, activeAdapter.name); const result = await rebuildAndRefetch("anthropic-oauth-429"); if ("failed" in result) return result.failed; upstreamResponse = result; @@ -6832,6 +6841,7 @@ async function handleResponsesInner( config.cacheRetention, ); sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel); + recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, activeAdapter.name); const result = await rebuildAndRefetch("oauth-account-429"); if ("failed" in result) return result.failed; upstreamResponse = result; @@ -7199,6 +7209,7 @@ async function handleResponsesInner( config.cacheRetention, ); sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel); + recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, activeAdapter.name); nextContinuationRecoveryKind = "anthropic-oauth-429"; continue; } catch { @@ -7240,6 +7251,7 @@ async function handleResponsesInner( config.cacheRetention, ); sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel); + recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, activeAdapter.name); nextContinuationRecoveryKind = "oauth-account-429"; continue; } diff --git a/src/usage/log.ts b/src/usage/log.ts index acbe8a00db..257cb86d3e 100644 --- a/src/usage/log.ts +++ b/src/usage/log.ts @@ -55,9 +55,14 @@ export type AttemptRecoveryKind = | "opaque-blob-rejection" | "empty-completion"; +/** Request-time upstream credential class, never a credential or account identifier. */ +export type UsageCredentialSource = "grok-oauth" | "xai-api-key"; + export interface PersistedUsageAttempt { ordinal: number; provider: string; + /** Absent on historic attempts and routes whose subscription attribution is unknown. */ + credentialSource?: UsageCredentialSource; model: string; adapter: string; status: number; @@ -400,6 +405,10 @@ function normalizeUsageAttempt(raw: unknown): PersistedUsageAttempt | null { return { ordinal: attempt.ordinal as number, provider: attempt.provider, + ...(attempt.provider === "xai" + && (attempt.credentialSource === "grok-oauth" || attempt.credentialSource === "xai-api-key") + ? { credentialSource: attempt.credentialSource } + : {}), model: attempt.model, adapter: attempt.adapter, status: attempt.status, diff --git a/tests/server/server-xai-oauth-401-replay.test.ts b/tests/server/server-xai-oauth-401-replay.test.ts index 6ad96b59d2..811dfbb6e5 100644 --- a/tests/server/server-xai-oauth-401-replay.test.ts +++ b/tests/server/server-xai-oauth-401-replay.test.ts @@ -1,11 +1,12 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; -import { mkdtempSync} from "node:fs"; +import { mkdtempSync, readFileSync} from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { saveConfig } from "../../src/config"; import { XAI_OAUTH_DISCOVERY_URL } from "../../src/oauth/xai"; import { saveCredential } from "../../src/oauth/store"; import { XAI_GROK_CLI_BASE_URL } from "../../src/providers/xai-transport"; +import { readUsageEntries, usageLogPath } from "../../src/usage/log"; import { startServer } from "../../src/server"; import type { OcxConfig } from "../../src/types"; import { installIsolatedCodexHome, type IsolatedCodexHome } from "../helpers/isolated-codex-home"; @@ -209,6 +210,14 @@ describe("xAI OAuth Responses opt-in upstream 401 replay", () => { expect(json.output?.find(item => item.type === "message")?.content?.[0]?.text).toBe("ok after refresh"); expect(observed.counts.refresh).toBe(1); expect(observed.chatAuth).toEqual(["Bearer rejected-access", "Bearer fresh-access"]); + const attempt = readUsageEntries().at(-1)?.attempts?.[0]; + expect(attempt?.credentialSource).toBe("grok-oauth"); + expect(attempt?.sendCount).toBe(2); + expect(attempt?.totalTokens).toBe(5); + const persisted = readFileSync(usageLogPath(), "utf8"); + expect(persisted).not.toContain("rejected-access"); + expect(persisted).not.toContain("fresh-access"); + expect(persisted).not.toContain("xai-test-account"); } finally { await server.stop(true); } @@ -231,6 +240,36 @@ describe("xAI OAuth Responses opt-in upstream 401 replay", () => { } }); + test("native Chat records canonical API-key provenance", async () => { + saveConfig(xaiConfig("key")); + globalThis.fetch = (async (input, init) => { + const url = input instanceof Request ? input.url : String(input); + expect(url).toBe("https://api.x.ai/v1/chat/completions"); + expect(new Headers(init?.headers).get("authorization")).toBe("Bearer xai-api-key"); + return Response.json({ + id: "chat-native-xai", object: "chat.completion", model: "grok-4.5", + choices: [{ index: 0, message: { role: "assistant", content: "ok" }, finish_reason: "stop" }], + usage: { prompt_tokens: 3, completion_tokens: 2, total_tokens: 5 }, + }); + }) as typeof fetch; + const server = startServer(0); + try { + const response = await originalFetch(new URL("/v1/chat/completions", server.url), { + method: "POST", headers: { "content-type": "application/json" }, + body: JSON.stringify({ model: "xai/grok-4.5", messages: [{ role: "user", content: "hello" }], stream: false }), + }); + expect(response.status).toBe(200); + await response.json(); + const entry = readUsageEntries().at(-1); + expect(entry?.inboundProtocol).toBe("chat"); + expect(entry?.attempts?.[0]?.credentialSource).toBe("xai-api-key"); + expect(entry?.attempts?.[0]?.totalTokens).toBe(5); + expect(entry?.attempts?.[0]?.sendCount).toBe(1); + } finally { + await server.stop(true); + } + }); + test("API-key xAI path never attempts OAuth refresh", async () => { saveConfig(xaiConfig("key")); let refreshCalls = 0; @@ -257,6 +296,7 @@ describe("xAI OAuth Responses opt-in upstream 401 replay", () => { expect(response.status).toBe(401); expect(chatCalls).toBe(1); expect(refreshCalls).toBe(0); + expect(readUsageEntries().at(-1)?.attempts?.[0]?.credentialSource).toBe("xai-api-key"); } finally { await server.stop(true); } diff --git a/tests/usage/request-log.test.ts b/tests/usage/request-log.test.ts index 5b8d30c58c..cf17a96a26 100644 --- a/tests/usage/request-log.test.ts +++ b/tests/usage/request-log.test.ts @@ -22,6 +22,7 @@ import { recordFirstOutput, requestLogEntryFromPersistedUsage, sealRequestAttemptIdentity, + recordAttemptCredentialSource, type RequestLogContext, } from "../../src/server/request-log"; import { handleResponses } from "../../src/server/responses"; @@ -56,6 +57,48 @@ function log(overrides: Partial): RequestLogEntry { } describe("request log metadata", () => { + test("upstream credential attribution requires the resolved canonical xAI transport", () => { + const attempt = beginRequestAttempt(1, "xai", "grok-test", "openai-chat"); + const oauth = { adapter: "openai-chat", authMode: "oauth" as const, baseUrl: "https://cli-chat-proxy.grok.com/v1" }; + recordAttemptCredentialSource(attempt, "xai", oauth); + expect(attempt.credentialSource).toBe("grok-oauth"); + for (const baseUrl of ["https://api.x.ai/v1", "https://proxy.example/v1", "http://cli-chat-proxy.grok.com/v1", + "https://cli-chat-proxy.grok.com:8443/v1", Object.assign(new URL(oauth.baseUrl), { username: "test" }).href, + "https://cli-chat-proxy.grok.com/v1?credential=canary", "https://cli-chat-proxy.grok.com/v2", "invalid"]) { + recordAttemptCredentialSource(attempt, "xai", { ...oauth, baseUrl }); + expect(attempt.credentialSource).toBeUndefined(); + } + recordAttemptCredentialSource(attempt, "xai", oauth); + recordAttemptCredentialSource(attempt, "custom", oauth); + expect(attempt.credentialSource).toBeUndefined(); + recordAttemptCredentialSource(attempt, "xai", { ...oauth, authMode: "key", baseUrl: "https://api.x.ai/v1" }); + expect(attempt.credentialSource).toBe("xai-api-key"); + recordAttemptCredentialSource(attempt, "xai", { ...oauth, authMode: "key" }); + expect(attempt.credentialSource).toBeUndefined(); + }); + + test("combo logging keeps credential provenance on physical attempts only", () => { + const a = beginRequestAttempt(1, "xai", "grok-test", "openai-chat"); + const b = beginRequestAttempt(2, "openai", "gpt-test", "openai-responses"); + recordAttemptCredentialSource(a, "xai", { + adapter: "openai-chat", authMode: "oauth", baseUrl: "https://cli-chat-proxy.grok.com/v1", + }); + noteAttemptSend(a, undefined); + finishRequestAttempt(a, 503, 1, { inputTokens: 4, outputTokens: 1 }); + noteAttemptSend(b, undefined); + const entries: RequestLogEntry[] = []; + addFinalRequestLog("mixed-combo", Date.now(), { + provider: "openai", model: "gpt-test", requestedModel: "combo/test", comboId: "test", + providerAdapter: "openai-responses", attempts: [a, b], activeAttempt: b, + usage: { inputTokens: 10, outputTokens: 2 }, + }, 200, undefined, entry => entries.push(entry)); + expect(entries[0]?.totalTokens).toBe(17); + expect(entries[0]?.attempts?.[0]?.credentialSource).toBe("grok-oauth"); + expect(entries[0]?.attempts?.[0]?.totalTokens).toBe(5); + expect(entries[0]?.attempts?.[1]?.credentialSource).toBeUndefined(); + expect(entries[0]).not.toHaveProperty("credentialSource"); + }); + test("creates one ordinary attempt after the final adapter is resolved", async () => { const originalFetch = globalThis.fetch; globalThis.fetch = (async () => Response.json({ diff --git a/tests/usage/usage-log.test.ts b/tests/usage/usage-log.test.ts index 42648e45bc..e3c490535e 100644 --- a/tests/usage/usage-log.test.ts +++ b/tests/usage/usage-log.test.ts @@ -39,6 +39,28 @@ afterEach(() => { }); describe("usage log", () => { + test("round trips only recognized per-attempt xAI credential sources", () => { + const attempt = { + ordinal: 1, provider: "xai", model: "grok-test", adapter: "openai-chat", status: 200, + durationMs: 1, sendCount: 1, recoveryKinds: [], usageStatus: "reported" as const, + usage: { inputTokens: 3, outputTokens: 2, totalTokens: 5 }, totalTokens: 5, + }; + appendUsageEntry({ + requestId: "credential-source", timestamp: Date.now(), provider: "combo", model: "combo/test", + status: 200, durationMs: 1, usageStatus: "reported", attempts: [ + { ...attempt, credentialSource: "grok-oauth" }, + { ...attempt, ordinal: 2, credentialSource: "xai-api-key" }, + { ...attempt, ordinal: 3, credentialSource: "secret-canary" as never }, + { ...attempt, ordinal: 4, provider: "custom", credentialSource: "grok-oauth" }, + { ...attempt, ordinal: 5 }, + ], + }); + resetUsageReadCacheForTests(); + const sources = readUsageEntries()[0]?.attempts?.map(row => row.credentialSource); + expect(sources).toEqual(["grok-oauth", "xai-api-key", undefined, undefined, undefined]); + expect(readFileSync(usageLogPath(), "utf8")).not.toContain("secret-canary"); + }); + test("preserves explicitly empty attempts through normalization", () => { const normalized = normalizeUsageEntryForTest({ requestId: "ocx-empty-attempts", From e73eb318c819faa87dcf9128fce8bc127dbd1803 Mon Sep 17 00:00:00 2001 From: t Date: Sun, 6 Sep 2026 19:01:43 +0900 Subject: [PATCH 2/4] fix(usage): derive native Chat attribution from its active adapter --- src/server/chat-native.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/chat-native.ts b/src/server/chat-native.ts index b99267bdde..513a9d0913 100644 --- a/src/server/chat-native.ts +++ b/src/server/chat-native.ts @@ -185,7 +185,7 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio retainedRequestBytes = bytes; }; const buildActiveRequest = () => { - recordAttemptCredentialSource(attempt, route.providerName, activeProvider); + recordAttemptCredentialSource(attempt, route.providerName, activeProvider, activeAdapter.name); return buildOpenAIChatPassthroughRequest( activeProvider, options.chatBody, From a634d341e3a770588bc04084d0bc4eb2e48648c1 Mon Sep 17 00:00:00 2001 From: t Date: Sun, 6 Sep 2026 19:04:07 +0900 Subject: [PATCH 3/4] test(usage): cover attempt resealing and native Chat key rotation --- .../server-xai-oauth-401-replay.test.ts | 60 +++++++++++++++++++ tests/usage/request-log.test.ts | 33 ++++++++++ 2 files changed, 93 insertions(+) diff --git a/tests/server/server-xai-oauth-401-replay.test.ts b/tests/server/server-xai-oauth-401-replay.test.ts index 811dfbb6e5..aab488ccb5 100644 --- a/tests/server/server-xai-oauth-401-replay.test.ts +++ b/tests/server/server-xai-oauth-401-replay.test.ts @@ -270,6 +270,66 @@ describe("xAI OAuth Responses opt-in upstream 401 replay", () => { } }); + test("native Chat 429 rotates configured apiKeyPool and keeps canonical xAI source", async () => { + const firstKey = "xai-pool-key-alpha-000111222333"; + const secondKey = "xai-pool-key-beta-444555666777"; + saveConfig({ + ...xaiConfig("key"), + providers: { + xai: { + adapter: "openai-chat", + baseUrl: "https://api.x.ai/v1", + authMode: "key", + apiKey: firstKey, + apiKeyPool: [ + { id: "k1", key: firstKey, addedAt: 1 }, + { id: "k2", key: secondKey, addedAt: 2 }, + ], + models: ["grok-4.5"], + }, + }, + } as OcxConfig); + const seenAuth: string[] = []; + globalThis.fetch = (async (input, init) => { + const url = input instanceof Request ? input.url : String(input); + expect(url).toBe("https://api.x.ai/v1/chat/completions"); + seenAuth.push(new Headers(init?.headers).get("authorization") ?? ""); + if (seenAuth.length === 1) { + return new Response(JSON.stringify({ error: { message: "rate limited" } }), { + status: 429, + headers: { "retry-after": "30", "content-type": "application/json" }, + }); + } + return Response.json({ + id: "chat-native-xai-rotate", object: "chat.completion", model: "grok-4.5", + choices: [{ index: 0, message: { role: "assistant", content: "ok after rotate" }, finish_reason: "stop" }], + usage: { prompt_tokens: 3, completion_tokens: 2, total_tokens: 5 }, + }); + }) as typeof fetch; + const server = startServer(0); + try { + const response = await originalFetch(new URL("/v1/chat/completions", server.url), { + method: "POST", headers: { "content-type": "application/json" }, + body: JSON.stringify({ model: "xai/grok-4.5", messages: [{ role: "user", content: "hello" }], stream: false }), + }); + expect(response.status).toBe(200); + await response.json(); + expect(seenAuth).toEqual([`Bearer ${firstKey}`, `Bearer ${secondKey}`]); + const entries = readUsageEntries(); + expect(entries).toHaveLength(1); + const attempt = entries[0]?.attempts?.[0]; + expect(entries[0]?.attempts).toHaveLength(1); + expect(attempt?.credentialSource).toBe("xai-api-key"); + expect(attempt?.sendCount).toBe(2); + expect(attempt?.adapter).toBe("openai-chat"); + const persisted = readFileSync(usageLogPath(), "utf8"); + expect(persisted).not.toContain(firstKey); + expect(persisted).not.toContain(secondKey); + } finally { + await server.stop(true); + } + }); + test("API-key xAI path never attempts OAuth refresh", async () => { saveConfig(xaiConfig("key")); let refreshCalls = 0; diff --git a/tests/usage/request-log.test.ts b/tests/usage/request-log.test.ts index cf17a96a26..02e26e764b 100644 --- a/tests/usage/request-log.test.ts +++ b/tests/usage/request-log.test.ts @@ -77,6 +77,39 @@ describe("request log metadata", () => { expect(attempt.credentialSource).toBeUndefined(); }); + test("seal same identity preserves credentialSource; provider or adapter change clears it", () => { + const attempt = beginRequestAttempt(1, "xai", "grok-test", "openai-chat"); + recordAttemptCredentialSource(attempt, "xai", { + adapter: "openai-chat", authMode: "key", baseUrl: "https://api.x.ai/v1", + }); + expect(attempt.credentialSource).toBe("xai-api-key"); + sealRequestAttemptIdentity(attempt, "xai", "openai-chat"); + expect(attempt.credentialSource).toBe("xai-api-key"); + expect(attempt.provider).toBe("xai"); + expect(attempt.adapter).toBe("openai-chat"); + + sealRequestAttemptIdentity(attempt, "custom", "openai-chat"); + expect(attempt.credentialSource).toBeUndefined(); + expect(attempt.provider).toBe("custom"); + + recordAttemptCredentialSource(attempt, "xai", { + adapter: "openai-chat", authMode: "key", baseUrl: "https://api.x.ai/v1", + }); + attempt.provider = "xai"; + expect(attempt.credentialSource).toBe("xai-api-key"); + sealRequestAttemptIdentity(attempt, "xai", "openai-responses"); + expect(attempt.credentialSource).toBeUndefined(); + expect(attempt.adapter).toBe("openai-responses"); + }); + + test("recordAttemptCredentialSource fourth adapterName rejects unsupported even when config adapter is openai-chat", () => { + const attempt = beginRequestAttempt(1, "xai", "grok-test", "openai-chat"); + recordAttemptCredentialSource(attempt, "xai", { + adapter: "openai-chat", authMode: "key", baseUrl: "https://api.x.ai/v1", + }, "anthropic"); + expect(attempt.credentialSource).toBeUndefined(); + }); + test("combo logging keeps credential provenance on physical attempts only", () => { const a = beginRequestAttempt(1, "xai", "grok-test", "openai-chat"); const b = beginRequestAttempt(2, "openai", "gpt-test", "openai-responses"); From 63282e49cf961c91cc7819e96bc8384e6de4a041 Mon Sep 17 00:00:00 2001 From: t Date: Sun, 6 Sep 2026 19:05:46 +0900 Subject: [PATCH 4/4] fix(usage): pass the initial resolved adapter to source recording --- src/server/responses/core.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/responses/core.ts b/src/server/responses/core.ts index 2fc7cf678c..b48438deb5 100644 --- a/src/server/responses/core.ts +++ b/src/server/responses/core.ts @@ -3927,7 +3927,7 @@ async function handleResponsesInner( (logCtx.attempts ??= []).push(attempt); } sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, adapter.name, logCtx.accountLogLabel); - recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, adapterProvider); + recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, adapterProvider, adapter.name); let runTurnAdapter = adapter; if (adapter.runTurn) { recordAdapterTierMetadata(logCtx, adapter.tierLogForRunTurn?.(parsed));