Skip to content
Merged
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
2 changes: 2 additions & 0 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ Where `agent-shim check` resolves one directory+identity's cascade in detail, `a

What it draws comes from the payload and from files the client already keeps. The model, the context window's use and the prompt cache's hit ratio come from the payload; the 5h and weekly quota bars come from the payload's own `rate_limits` (claude.ai Pro and Max accounts, after the session's first API response); and the login account with its plan, organisation and seat comes from the `oauthAccount` block of `$CLAUDE_CONFIG_DIR/.claude.json`, or `~/.claude.json` when the configuration directory is not pinned. A session launched through agent-shim adds the identity, the provider, the provider's account and plan, and the provider's own usage windows from the door's usage snapshot; a provider session never shows the payload's windows, which describe the login account rather than the quota it spends.

A session whose model something chose for it shows the choice beside the client's own model name: `auto -> glm-5.3` when a virtual model or a tier alias picked the model that answered, read from the session's last usage record, with the serving provider folded into the arrow (`auto -> z/glm-5.3`) when the choice could have landed on more than one provider or left the launch's own. A session that named a provider-qualified model itself shows `via <provider>/<model>` instead, when that name sent it off the launch's provider.

The context bar and the quota windows are laid out on one grid, one or more bars to a row as the terminal's width allows. Bars start in the same column and end in the same column on every row, with each label and set of figures padded to match, and the width left over is shared between the bars; a row with fewer bars than the grid has columns gives its last bar the empty columns' width, so its end still lines up. On a terminal too narrow for the groups to share columns, each group aligns within itself.

A session routed through headroom also shows what the proxy saved it (tokens, percentage and dollars) at the end of the first line. The statusline asks the proxy for `GET /stats/sessions/<session id>`, keyed by the payload's `session_id`, over the supervised daemon's unix socket (authenticated the way the door's own hop authenticates it) or, when `HEADROOM_PROXY_URL` names a proxy run on its own, over http. A lookup is bounded by a short timeout, and a proxy that is unreachable, slow or does not know the session simply leaves the segment out.
Expand Down
9 changes: 9 additions & 0 deletions schema/UsageRecord.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@
"model": {
"type": "string"
},
"requestedModel": {
"type": "string"
},
"candidateProviders": {
"type": "array",
"items": {
"type": "string"
}
},
"status": {
"type": "integer",
"minimum": -9007199254740991,
Expand Down
4 changes: 2 additions & 2 deletions src/antigravity/relay.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ describe("createAntigravityRelay", () => {
it("relays text streaming chunks to Anthropic SSE frames", () => {
let idCounter = 1;
const relay = createAntigravityRelay({
requestedModel: "claude-sonnet-4-5",
servedModel: "claude-sonnet-4-5",
toolNames: new Map(),
fallbackId: () => `msg_${String(idCounter++)}`,
});
Expand Down Expand Up @@ -95,7 +95,7 @@ describe("createAntigravityRelay", () => {
let idCounter = 1;
const toolNames = new Map<string, string>([["bash_exec", "bash-exec"]]);
const relay = createAntigravityRelay({
requestedModel: "claude-sonnet-4-5",
servedModel: "claude-sonnet-4-5",
toolNames,
fallbackId: () => `id_${String(idCounter++)}`,
});
Expand Down
5 changes: 3 additions & 2 deletions src/antigravity/relay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ interface AnthropicUsage {
}

export interface RelayParams {
readonly requestedModel: string;
/** The model that actually serves the request (a tier alias already translated to its concrete backend model), echoed in `message_start` exactly as the real API echoes the concrete model behind an alias, so a usage record read from the stream attributes quota to the backend model. */
readonly servedModel: string;
readonly toolNames: ReadonlyMap<string, string>;
readonly fallbackId: () => string;
}
Expand Down Expand Up @@ -63,7 +64,7 @@ export function createAntigravityRelay(params: RelayParams): {
id: messageId,
type: "message",
role: "assistant",
model: params.requestedModel,
model: params.servedModel,
content: [],
stop_reason: null,
stop_sequence: null,
Expand Down
5 changes: 4 additions & 1 deletion src/antigravity/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ export function createAntigravityRoute(ports: AntigravityRoutePorts): (request:
}
}
const relay = createAntigravityRelay({
requestedModel,
// The frame carries the model that actually served the request, as the real API echoes the concrete model behind an alias: a usage record read from the response then attributes quota to the backend model, not the tier alias the session typed.
servedModel: translated.model,
toolNames: translated.toolNames,
fallbackId,
});
Expand Down Expand Up @@ -238,6 +239,8 @@ export function createAntigravityRoute(ports: AntigravityRoutePorts): (request:
...quotaHeaders,
},
body: stream(),
// The tier alias the session asked for, whenever the translation resolved it to a different model: the front door's mount turns it into the model choice the usage record keeps.
...(requestedModel !== translated.model ? { requestedModel } : {}),
};
};
}
2 changes: 2 additions & 0 deletions src/codex/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export interface RouteResponse {
readonly status: number;
readonly headers: Readonly<Record<string, string>>;
readonly body: string | AsyncIterable<string>;
/** The model name the session asked for, when this response answers a request whose model a translation layer chose among alternatives for (a tier alias that resolved to a concrete model) and served under a different name. Read by the front door's mount, never sent to the client. */
readonly requestedModel?: string;
}

/** The outcome of looking up the codex provider a request's path names. */
Expand Down
72 changes: 72 additions & 0 deletions src/frontdoor/antigravityMount.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ function createMockResponse(): {
getHeaders: () => Record<string, string> | undefined;
getChunks: () => string[];
isEnded: () => boolean;
getNotedModelChoices: () => { requested: string; providers: string[] }[];
} {
let status: number | undefined;
let headers: Record<string, string> | undefined;
const chunks: string[] = [];
const notedChoices: { requested: string; providers: string[] }[] = [];
let ended = false;
let headersSent = false;

Expand All @@ -34,6 +36,9 @@ function createMockResponse(): {
headers = h;
headersSent = true;
},
noteModelChoice: (choice) => {
notedChoices.push({ requested: choice.requested, providers: [...choice.providers] });
},
flush: () => undefined,
write: async (chunk) => {
await Promise.resolve();
Expand All @@ -51,6 +56,7 @@ function createMockResponse(): {
getHeaders: () => headers,
getChunks: () => chunks,
isEnded: () => ended,
getNotedModelChoices: () => notedChoices,
};
}

Expand Down Expand Up @@ -163,3 +169,69 @@ describe("createAntigravityRouteMount", () => {
expect(mockRes.getChunks().join("")).toContain("request body chunk was not a Buffer");
});
});

describe("a tier alias the translator resolved", () => {
it("is noted as a choice among this provider's models, and the frame names the model that served", async () => {
const geminiSse = [
'data: {"candidates":[{"content":{"parts":[{"text":"Greetings!"}]}}]}\n\n',
'data: {"candidates":[{"finishReason":"STOP"}]}\n\n',
].join("");

const mockFetch: typeof fetch = async () => {
return await Promise.resolve(
new Response(geminiSse, {
status: 200,
headers: { "Content-Type": "text/event-stream" },
}),
);
};

const mount = createAntigravityRouteMount(
{
loadProvider: () => ({
ok: true,
config: DEFAULT_CONFIG,
token: "dummy-key",
credentialTarget: "apiKey",
}),
fetchFn: mockFetch,
},
"http://127.0.0.1:4100",
"my-ag",
);

const mockRes = createMockResponse();
const reqBody = JSON.stringify({
model: "auto",
max_tokens: 100,
messages: [{ role: "user", content: "Hi" }],
});

const request: RoutedRequest = {
method: "POST",
url: "/providers/my-ag/v1/messages",
headers: {},
body: Readable.from([Buffer.from(reqBody)]),
signal: new AbortController().signal,
session: {
identity: undefined,
sessionId: undefined,
provider: undefined,
headroom: false, routeByName: false,
projectId: undefined,
},
};

await mount.serve(request, mockRes.response);

expect(mockRes.getStatus()).toBe(HTTP_STATUS.ok);
// The alias the session typed, marked as a choice that never left this provider.
expect(mockRes.getNotedModelChoices()).toEqual([{ requested: "auto", providers: ["my-ag"] }]);
// The frame names the model that actually served, not the alias, so a usage record read from the stream attributes quota to the backend model.
const output = mockRes.getChunks().join("");
const start = output.indexOf("event: message_start");
expect(start).toBeGreaterThanOrEqual(0);
expect(output.slice(start, output.indexOf("event:", start + 1))).not.toContain('"model": "auto"');
expect(output).toContain("Greetings!");
});
});
4 changes: 4 additions & 0 deletions src/frontdoor/antigravityMount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ export function createAntigravityRouteMount(ports: AntigravityRoutePorts, upstre
if (request.signal.aborted) {
return;
}
// A tier alias the translator resolved to a concrete model is a choice among this provider's lineup: recorded before the head is written so the usage middleware sees it. The candidates never leave this provider, so the provider list is exactly it.
if (routed.requestedModel !== undefined) {
response.noteModelChoice?.({ requested: routed.requestedModel, providers: [provider] });
}
await writeRouteResponse(routed, response, request.signal);
},
};
Expand Down
41 changes: 38 additions & 3 deletions src/frontdoor/pipeline.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ function deps(resolution: RouteResolution, observers: readonly ResponseObserver[
describe("createRoutedResponse", () => {
it("applies backpressure: a write that the socket refuses waits for its drain", async () => {
const response = new FakeServerResponse(true);
const routed = createRoutedResponse(response as unknown as PipelineRequest["response"], { deps: deps({ ok: true, route: recordingRoute() }).deps, servedPath: "/v1/messages", session: { identity: "work", sessionId: "s", provider: undefined, headroom: false, routeByName: false, projectId: undefined }, route: "test", request: { method: "POST", path: "/v1/messages", receivedAt: 0, viaHeadroom: false, thread: { lineage: undefined } } });
const routed = createRoutedResponse(response as unknown as PipelineRequest["response"], { deps: deps({ ok: true, route: recordingRoute() }).deps, servedPath: "/v1/messages", session: { identity: "work", sessionId: "s", provider: undefined, headroom: false, routeByName: false, projectId: undefined }, route: "test", request: { method: "POST", path: "/v1/messages", receivedAt: 0, viaHeadroom: false, thread: { lineage: undefined }, modelChoice: { value: undefined } } });
routed.start(HTTP_STATUS.ok, {});
let resolved = false;
const write = routed.write("chunk").then(() => {
Expand All @@ -116,7 +116,7 @@ describe("createRoutedResponse", () => {

it("settles a backpressured write when the client goes away, so a route is never left waiting on a dead socket", async () => {
const response = new FakeServerResponse(true);
const routed = createRoutedResponse(response as unknown as PipelineRequest["response"], { deps: deps({ ok: true, route: recordingRoute() }).deps, servedPath: "/v1/messages", session: { identity: "work", sessionId: "s", provider: undefined, headroom: false, routeByName: false, projectId: undefined }, route: "test", request: { method: "POST", path: "/v1/messages", receivedAt: 0, viaHeadroom: false, thread: { lineage: undefined } } });
const routed = createRoutedResponse(response as unknown as PipelineRequest["response"], { deps: deps({ ok: true, route: recordingRoute() }).deps, servedPath: "/v1/messages", session: { identity: "work", sessionId: "s", provider: undefined, headroom: false, routeByName: false, projectId: undefined }, route: "test", request: { method: "POST", path: "/v1/messages", receivedAt: 0, viaHeadroom: false, thread: { lineage: undefined }, modelChoice: { value: undefined } } });
routed.start(HTTP_STATUS.ok, {});
let resolved = false;
const write = routed.write("chunk").then(() => {
Expand Down Expand Up @@ -338,7 +338,7 @@ function routedWith(response: FakeServerResponse, observers: readonly ResponseOb
deps: d,
session: { identity: "work", sessionId: "s", provider: undefined, headroom: false, routeByName: false, projectId: undefined },
route: "test",
request: { method: "POST", path: "/v1/messages", receivedAt: 0, viaHeadroom: false, thread: { lineage: undefined } },
request: { method: "POST", path: "/v1/messages", receivedAt: 0, viaHeadroom: false, thread: { lineage: undefined }, modelChoice: { value: undefined } },
servedPath: "/v1/messages",
});
return { routed, logs };
Expand Down Expand Up @@ -416,3 +416,38 @@ describe("response body observers", () => {
expect(logs.join("\n")).toContain("scanner bug");
});
});

describe("a model choice reaching the response observers", () => {
it("carries the resolution's choice on the event the observers see at the head", async () => {
const choices: (RoutedResponseEvent["modelChoice"])[] = [];
const { request, response } = pipelineRequest();
await serveRouted(request, deps({ ok: true, route: recordingRoute(), modelChoice: { requested: "auto", providers: ["z", "antigravity"] } }, [(event) => {
choices.push(event.modelChoice);
return undefined;
}]).deps);
expect(response.writtenHead?.status).toBe(HTTP_STATUS.ok);
expect(choices).toEqual([{ requested: "auto", providers: ["z", "antigravity"] }]);
});

it("carries a choice the route notes before its head, as the tier translator does", async () => {
const choices: (RoutedResponseEvent["modelChoice"])[] = [];
const notingRoute: FrontDoorRoute = {
name: "noting",
headroomEligible: false,
headroomUpstream: undefined,
serve: async (_request, response) => {
response.noteModelChoice?.({ requested: "sonnet", providers: ["antigravity"] });
response.start(HTTP_STATUS.ok, {});
await Promise.resolve();
response.end();
},
};
const { request, response } = pipelineRequest();
await serveRouted(request, deps({ ok: true, route: notingRoute }, [(event) => {
choices.push(event.modelChoice);
return undefined;
}]).deps);
expect(response.writtenHead?.status).toBe(HTTP_STATUS.ok);
expect(choices).toEqual([{ requested: "sonnet", providers: ["antigravity"] }]);
});
});
Loading
Loading