- Added a 4-slot concurrency limiter for Anthropic multi-image resize work. - Prioritized client-owned request fields and `claudeCodeVersion`-based `User-Agent` headers. - Ignored terminal `message_delta` envelopes and treated missing `usage`/`delta` payloads as non-fatal. - Stopped ping-only streams from consuming first-event watchdog logic and preserved timeout retries.
121 lines
3.7 KiB
TypeScript
121 lines
3.7 KiB
TypeScript
import { describe, expect, it } from "bun:test";
|
|
import { claudeCodeVersion } from "@oh-my-pi/pi-ai/providers/anthropic";
|
|
import type { UsageFetchContext } from "@oh-my-pi/pi-ai/usage";
|
|
import { claudeUsageProvider } from "@oh-my-pi/pi-ai/usage/claude";
|
|
|
|
function getHeaderCaseInsensitive(
|
|
headers: Headers | Record<string, string | ReadonlyArray<string>> | string[][] | undefined,
|
|
name: string,
|
|
): string | undefined {
|
|
if (!headers) return undefined;
|
|
const target = name.toLowerCase();
|
|
|
|
if (headers instanceof Headers) {
|
|
for (const [key, value] of headers.entries()) {
|
|
if (key.toLowerCase() === target) return value;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
if (Array.isArray(headers)) {
|
|
const match = headers.find(([key]) => key.toLowerCase() === target);
|
|
return match?.[1];
|
|
}
|
|
|
|
for (const [key, value] of Object.entries(headers)) {
|
|
if (key.toLowerCase() === target) return String(value);
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
describe("claude usage request headers", () => {
|
|
it("sends aligned anthropic fingerprint and bearer auth headers", async () => {
|
|
const now = Date.now();
|
|
const token = "oat-test-access-token";
|
|
const calls: Array<{ input: string; init?: RequestInit }> = [];
|
|
const fetchMock = (async (input: string | URL, init?: RequestInit) => {
|
|
calls.push({ input: String(input), init });
|
|
return new Response(
|
|
JSON.stringify({
|
|
five_hour: {
|
|
utilization: 42,
|
|
resets_at: new Date(now + 10 * 60 * 1000).toISOString(),
|
|
},
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"anthropic-organization-id": "org_test",
|
|
},
|
|
},
|
|
);
|
|
}) as unknown as typeof fetch;
|
|
|
|
const ctx: UsageFetchContext = {
|
|
fetch: fetchMock,
|
|
};
|
|
|
|
const report = await claudeUsageProvider.fetchUsage(
|
|
{
|
|
provider: "anthropic",
|
|
credential: {
|
|
type: "oauth",
|
|
accessToken: token,
|
|
accountId: "org_test",
|
|
email: "user@example.com",
|
|
expiresAt: now + 60_000,
|
|
},
|
|
},
|
|
ctx,
|
|
);
|
|
|
|
expect(report).not.toBeNull();
|
|
expect(calls).toHaveLength(1);
|
|
expect(calls[0]?.input).toBe("https://api.anthropic.com/api/oauth/usage");
|
|
|
|
const headers = calls[0]?.init?.headers;
|
|
expect(getHeaderCaseInsensitive(headers, "authorization")).toBe(`Bearer ${token}`);
|
|
expect(getHeaderCaseInsensitive(headers, "user-agent")).toBe(`claude-cli/${claudeCodeVersion} (external, cli)`);
|
|
|
|
const beta = getHeaderCaseInsensitive(headers, "anthropic-beta");
|
|
expect(beta).toBeDefined();
|
|
const betaTokens = beta?.split(",").map(tokenValue => tokenValue.trim()) ?? [];
|
|
expect(betaTokens).toContain("claude-code-20250219");
|
|
expect(betaTokens).toContain("oauth-2025-04-20");
|
|
expect(betaTokens).toContain("interleaved-thinking-2025-05-14");
|
|
expect(betaTokens).toContain("redact-thinking-2026-02-12");
|
|
expect(betaTokens).toContain("context-management-2025-06-27");
|
|
expect(betaTokens).toContain("prompt-caching-scope-2026-01-05");
|
|
expect(betaTokens).toContain("mid-conversation-system-2026-04-07");
|
|
expect(betaTokens).toContain("advanced-tool-use-2025-11-20");
|
|
expect(betaTokens).toContain("effort-2025-11-24");
|
|
expect(betaTokens).toContain("extended-cache-ttl-2025-04-11");
|
|
});
|
|
|
|
it("does not invent reset timestamps when Claude omits them", async () => {
|
|
const fetchMock = (async () => {
|
|
return new Response(
|
|
JSON.stringify({
|
|
five_hour: { utilization: 42 },
|
|
}),
|
|
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
);
|
|
}) as unknown as typeof fetch;
|
|
|
|
const report = await claudeUsageProvider.fetchUsage(
|
|
{
|
|
provider: "anthropic",
|
|
credential: {
|
|
type: "oauth",
|
|
accessToken: "oat-test-access-token",
|
|
expiresAt: Date.now() + 60_000,
|
|
},
|
|
},
|
|
{ fetch: fetchMock },
|
|
);
|
|
|
|
expect(report?.limits[0]?.window?.resetsAt).toBeUndefined();
|
|
});
|
|
});
|