feat(FN-1474): add Kimi endpoint fallback for 404 url.not_found

This commit is contained in:
gsxdsm
2026-04-09 21:36:53 -07:00
parent 2ef27cf749
commit b7005001d2
3 changed files with 305 additions and 15 deletions

View File

@@ -3037,6 +3037,238 @@ describe("usage", () => {
});
});
describe("endpoint fallback", () => {
it("falls back to underscore endpoint when hyphen endpoint returns 404 url.not_found", async () => {
const requestedPaths: string[] = [];
mockReadFileSync.mockImplementation((filePath: string) => {
if (filePath.includes(".pi/agent/auth.json")) {
return JSON.stringify({
"kimi-coding": { type: "api_key", key: "test-api-key" },
});
}
throw new Error("File not found");
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
const mockResponse = {
data: {
total: 100,
used: 25,
remaining: 75,
reset_time: Date.now() + 3600000,
},
};
mockRequest.mockImplementation((options: any, callback: any) => {
const pathname = new URL(`https://${options.hostname}${options.path}`).pathname;
requestedPaths.push(pathname);
let statusCode = 200;
let responseBody = mockResponse;
if (pathname === "/v1/coding-plan/usage") {
// First endpoint returns 404 with url.not_found
statusCode = 404;
responseBody = { error: "url.not_found" };
}
const mockRes = {
statusCode,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from(JSON.stringify(responseBody)));
if (event === "end") handler();
}),
};
callback(mockRes);
return { on: vi.fn(), write: vi.fn(), end: vi.fn() };
});
const providers = await fetchAllProviderUsage();
const kimi = providers.find((p) => p.name === "Kimi")!;
expect(kimi.status).toBe("ok");
expect(kimi.windows).toHaveLength(1);
expect(kimi.windows[0].percentUsed).toBe(25);
// Should have tried both endpoints
expect(requestedPaths).toEqual(["/v1/coding-plan/usage", "/v1/coding_plan/usage"]);
});
it("does not attempt fallback on 401 auth error", async () => {
const requestedPaths: string[] = [];
mockReadFileSync.mockImplementation((filePath: string) => {
if (filePath.includes(".pi/agent/auth.json")) {
return JSON.stringify({
"kimi-coding": { type: "api_key", key: "test-api-key" },
});
}
throw new Error("File not found");
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
mockRequest.mockImplementation((options: any, callback: any) => {
const pathname = new URL(`https://${options.hostname}${options.path}`).pathname;
requestedPaths.push(pathname);
const mockRes = {
statusCode: 401,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from(JSON.stringify({})));
if (event === "end") handler();
}),
};
callback(mockRes);
return { on: vi.fn(), write: vi.fn(), end: vi.fn() };
});
const providers = await fetchAllProviderUsage();
const kimi = providers.find((p) => p.name === "Kimi")!;
expect(kimi.status).toBe("error");
expect(kimi.error).toContain("Auth expired");
// Only first endpoint should be attempted (no fallback for auth errors)
expect(requestedPaths).toEqual(["/v1/coding-plan/usage"]);
});
it("does not attempt fallback on 403 auth error", async () => {
const requestedPaths: string[] = [];
mockReadFileSync.mockImplementation((filePath: string) => {
if (filePath.includes(".pi/agent/auth.json")) {
return JSON.stringify({
"kimi-coding": { type: "api_key", key: "test-api-key" },
});
}
throw new Error("File not found");
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
mockRequest.mockImplementation((options: any, callback: any) => {
const pathname = new URL(`https://${options.hostname}${options.path}`).pathname;
requestedPaths.push(pathname);
const mockRes = {
statusCode: 403,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from(JSON.stringify({})));
if (event === "end") handler();
}),
};
callback(mockRes);
return { on: vi.fn(), write: vi.fn(), end: vi.fn() };
});
const providers = await fetchAllProviderUsage();
const kimi = providers.find((p) => p.name === "Kimi")!;
expect(kimi.status).toBe("error");
expect(kimi.error).toContain("Auth expired");
// Only first endpoint should be attempted (no fallback for auth errors)
expect(requestedPaths).toEqual(["/v1/coding-plan/usage"]);
});
it("returns error without fallback when first endpoint returns 404 without url.not_found", async () => {
const requestedPaths: string[] = [];
mockReadFileSync.mockImplementation((filePath: string) => {
if (filePath.includes(".pi/agent/auth.json")) {
return JSON.stringify({
"kimi-coding": { type: "api_key", key: "test-api-key" },
});
}
throw new Error("File not found");
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
mockRequest.mockImplementation((options: any, callback: any) => {
const pathname = new URL(`https://${options.hostname}${options.path}`).pathname;
requestedPaths.push(pathname);
const mockRes = {
statusCode: 404,
headers: {},
on: vi.fn((event: string, handler: any) => {
// Return a 404 without url.not_found
if (event === "data") handler(Buffer.from(JSON.stringify({ error: "something_else" })));
if (event === "end") handler();
}),
};
callback(mockRes);
return { on: vi.fn(), write: vi.fn(), end: vi.fn() };
});
const providers = await fetchAllProviderUsage();
const kimi = providers.find((p) => p.name === "Kimi")!;
expect(kimi.status).toBe("error");
expect(kimi.error).toContain("HTTP 404");
// Should NOT attempt fallback because error is not url.not_found
expect(requestedPaths).toEqual(["/v1/coding-plan/usage"]);
});
it("succeeds with single endpoint when first endpoint works", async () => {
const requestedPaths: string[] = [];
mockReadFileSync.mockImplementation((filePath: string) => {
if (filePath.includes(".pi/agent/auth.json")) {
return JSON.stringify({
"kimi-coding": { type: "api_key", key: "test-api-key" },
});
}
throw new Error("File not found");
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
const mockResponse = {
data: {
total: 100,
used: 50,
remaining: 50,
reset_time: Date.now() + 3600000,
},
};
mockRequest.mockImplementation((options: any, callback: any) => {
const pathname = new URL(`https://${options.hostname}${options.path}`).pathname;
requestedPaths.push(pathname);
const mockRes = {
statusCode: 200,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from(JSON.stringify(mockResponse)));
if (event === "end") handler();
}),
};
callback(mockRes);
return { on: vi.fn(), write: vi.fn(), end: vi.fn() };
});
const providers = await fetchAllProviderUsage();
const kimi = providers.find((p) => p.name === "Kimi")!;
expect(kimi.status).toBe("ok");
expect(kimi.windows).toHaveLength(1);
expect(kimi.windows[0].percentUsed).toBe(50);
// Should only request first endpoint
expect(requestedPaths).toEqual(["/v1/coding-plan/usage"]);
});
});
describe("Claude CLI fallback parsing", () => {
describe("_stripClaudeAnsi", () => {
it("strips basic ANSI color codes", () => {