mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-08 10:02:56 +08:00
* feat(providers): add admin cost history * fix(ui): sync cron raw-copy baseline * fix(usage): harden provider cost credentials * fix(ui): refresh raw-copy baseline * docs: refresh documentation map * fix(ci): sync provider usage baselines * fix(ui): hide zero-cost history bars * docs(changelog): defer provider cost note
147 lines
4.4 KiB
TypeScript
147 lines
4.4 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import { fetchOpenAIAdminUsage, resolveOpenAIUsageAuth } from "./usage.js";
|
|
|
|
function requestUrl(input: string | URL | Request): URL {
|
|
return new URL(input instanceof Request ? input.url : input);
|
|
}
|
|
|
|
describe("OpenAI provider usage", () => {
|
|
it("aggregates provider-reported costs, tokens, models, and categories", async () => {
|
|
const fetchFn = vi.fn(async (input: string | URL | Request, _init?: RequestInit) => {
|
|
const url = requestUrl(input);
|
|
if (url.pathname.endsWith("/organization/costs")) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
data: [
|
|
{
|
|
start_time: 1_783_296_000,
|
|
end_time: 1_783_382_400,
|
|
results: [{ amount: { value: "12.34", currency: "usd" }, line_item: "Responses" }],
|
|
},
|
|
],
|
|
has_more: false,
|
|
}),
|
|
{ status: 200 },
|
|
);
|
|
}
|
|
return new Response(
|
|
JSON.stringify({
|
|
data: [
|
|
{
|
|
start_time: 1_783_296_000,
|
|
end_time: 1_783_382_400,
|
|
results: [
|
|
{
|
|
input_tokens: 1_000,
|
|
input_cached_tokens: 400,
|
|
output_tokens: 250,
|
|
num_model_requests: 8,
|
|
model: "gpt-5.5",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
has_more: false,
|
|
}),
|
|
{ status: 200 },
|
|
);
|
|
});
|
|
|
|
const result = await fetchOpenAIAdminUsage({
|
|
apiKey: "sk-admin-test",
|
|
projectId: "proj_test",
|
|
timeoutMs: 5_000,
|
|
fetchFn: fetchFn as typeof fetch,
|
|
now: Date.parse("2026-07-06T12:00:00Z"),
|
|
periodDays: 2,
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
provider: "openai",
|
|
plan: "Admin API · proj_test",
|
|
billing: [{ type: "spend", amount: 12.34, unit: "USD", period: "2d" }],
|
|
costHistory: {
|
|
unit: "USD",
|
|
periodDays: 2,
|
|
scope: "Project proj_test",
|
|
daily: [
|
|
{
|
|
date: "2026-07-06",
|
|
amount: 12.34,
|
|
requests: 8,
|
|
inputTokens: 600,
|
|
cacheReadTokens: 400,
|
|
outputTokens: 250,
|
|
totalTokens: 1_250,
|
|
},
|
|
],
|
|
models: [
|
|
{
|
|
name: "gpt-5.5",
|
|
requests: 8,
|
|
inputTokens: 600,
|
|
cacheReadTokens: 400,
|
|
totalTokens: 1_250,
|
|
},
|
|
],
|
|
categories: [{ name: "Responses", amount: 12.34 }],
|
|
},
|
|
});
|
|
expect(fetchFn).toHaveBeenCalledTimes(2);
|
|
for (const [input, init] of fetchFn.mock.calls) {
|
|
const url = requestUrl(input);
|
|
expect(url.searchParams.get("project_ids")).toBe("proj_test");
|
|
expect(url.searchParams.get("bucket_width")).toBe("1d");
|
|
expect((init as RequestInit).headers).toMatchObject({
|
|
Authorization: "Bearer sk-admin-test",
|
|
});
|
|
}
|
|
});
|
|
|
|
it("reports when organization usage rejects a non-admin key", async () => {
|
|
const result = await fetchOpenAIAdminUsage({
|
|
apiKey: "sk-proj-test",
|
|
timeoutMs: 5_000,
|
|
fetchFn: vi.fn(async () => new Response("", { status: 403 })) as typeof fetch,
|
|
});
|
|
expect(result.error).toBe("Admin API key required");
|
|
});
|
|
|
|
it("prefers an explicit admin key over ChatGPT OAuth", async () => {
|
|
const result = await resolveOpenAIUsageAuth({
|
|
config: {
|
|
models: {
|
|
providers: {
|
|
openai: {
|
|
baseUrl: "https://proxy.example.test/v1",
|
|
models: [],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
env: { OPENAI_ADMIN_KEY: "sk-admin-explicit" },
|
|
provider: "openai",
|
|
resolveApiKeyFromConfigAndStore: () => "sk-proj-fallback",
|
|
resolveOAuthToken: async () => ({ token: "oauth-token" }),
|
|
});
|
|
expect(result).toEqual({
|
|
token: 'openclaw:openai-admin:v1:{"token":"sk-admin-explicit"}',
|
|
});
|
|
});
|
|
|
|
it("does not repurpose inference credentials for organization usage", async () => {
|
|
const resolveCandidates = vi.fn(async () => ["sk-admin-secretref"]);
|
|
const result = await resolveOpenAIUsageAuth({
|
|
config: {},
|
|
env: {},
|
|
provider: "openai",
|
|
resolveApiKeyFromConfigAndStore: () => "sk-proj-inference",
|
|
resolveApiKeyCandidatesFromConfigAndStore: resolveCandidates,
|
|
resolveOAuthToken: async () => null,
|
|
});
|
|
|
|
expect(result).toEqual({ handled: true });
|
|
expect(resolveCandidates).not.toHaveBeenCalled();
|
|
});
|
|
});
|