Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs-site/src/content/docs/reference/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ then turns the events into Responses SSE.
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
Expand Down
8 changes: 8 additions & 0 deletions docs-site/src/content/docs/reference/management-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,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.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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
Expand Down
20 changes: 12 additions & 8 deletions src/server/chat-native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
beginRequestAttempt,
noteAttemptSend,
recordFirstOutput,
recordAttemptCredentialSource,
sealRequestAttemptIdentity,
type RequestLogContext,
} from "./request-log";
Expand Down Expand Up @@ -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);
Expand Down
28 changes: 27 additions & 1 deletion src/server/request-log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -1216,6 +1216,32 @@ export function sealRequestAttemptIdentity(
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<OcxProviderConfig, "authMode" | "baseUrl" | "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(provider.adapter)) 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,
Expand Down
2 changes: 2 additions & 0 deletions src/server/responses/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ import {
recordAttemptRequestedEffort,
requestLogSpeedLabel,
sealRequestAttemptIdentity,
recordAttemptCredentialSource,
usageFromResponsesPayload,
type RequestLogContext,
} from "../request-log";
Expand Down Expand Up @@ -3701,6 +3702,7 @@ async function handleResponsesInner(
(logCtx.attempts ??= []).push(attempt);
}
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, adapter.name, logCtx.accountLogLabel);
recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, adapterProvider);
Comment thread
olddonkey marked this conversation as resolved.
let runTurnAdapter = adapter;
if (adapter.runTurn) {
recordAdapterTierMetadata(logCtx, adapter.tierLogForRunTurn?.(parsed));
Expand Down
9 changes: 9 additions & 0 deletions src/usage/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
42 changes: 41 additions & 1 deletion tests/server/server-xai-oauth-401-replay.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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);
}
Expand All @@ -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;
Expand All @@ -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);
}
Expand Down
43 changes: 43 additions & 0 deletions tests/usage/request-log.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
recordFirstOutput,
requestLogEntryFromPersistedUsage,
sealRequestAttemptIdentity,
recordAttemptCredentialSource,
type RequestLogContext,
} from "../../src/server/request-log";
import { handleResponses } from "../../src/server/responses";
Expand Down Expand Up @@ -56,6 +57,48 @@ function log(overrides: Partial<RequestLogEntry>): 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({
Expand Down
22 changes: 22 additions & 0 deletions tests/usage/usage-log.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading