8a0903ebf2
The root barrel is now core-only and side-effect free: types, createModels/createProvider, auth substrate, lazyStream/lazyApi, faux, utils. Generated catalogs, api-registry, env-api-keys, images, global stream functions, and per-API lazy wrappers leave the root. New @earendil-works/pi-ai/compat preserves the old surface verbatim as a strict superset of the root: api-dispatch stream/complete with env key injection, the builtin registration side effect (skip-if-present so it cannot clobber earlier overrides), deprecated getModel/getModels/ getProviders aliases of the new getBuiltin* reads in providers/all, lazy api wrappers + setBedrockProviderModule, and image generation. Compat dies with the coding-agent ModelManager migration. Packaging: exports map gains ./compat, ./providers/*, ./api/*; sideEffects array lists only the effectful modules. Old-global imports across agent/coding-agent/examples and pi-ai tests switch to /compat (path-only; compat is a superset). The coding-agent extension loader resolves the pi-ai ROOT specifier to compat, so existing user extensions using the old global API keep working at runtime until compat is removed. vitest configs alias /compat to src; browser smoke imports old globals from /compat.
126 lines
4.8 KiB
TypeScript
126 lines
4.8 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { InMemoryCredentialStore } from "../src/auth/credential-store.ts";
|
|
import { createModels } from "../src/models.ts";
|
|
import { anthropicProvider } from "../src/providers/anthropic.ts";
|
|
import { githubCopilotProvider } from "../src/providers/github-copilot.ts";
|
|
import { anthropicOAuth } from "../src/utils/oauth/anthropic.ts";
|
|
import { githubCopilotOAuth } from "../src/utils/oauth/github-copilot.ts";
|
|
import { openaiCodexOAuth } from "../src/utils/oauth/openai-codex.ts";
|
|
|
|
function jsonResponse(body: unknown, status = 200): Response {
|
|
return new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" } });
|
|
}
|
|
|
|
describe.sequential("OAuthAuth adapters", () => {
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
it("anthropic toAuth derives the api key from the access token", async () => {
|
|
const auth = await anthropicOAuth.toAuth({ type: "oauth", access: "token", refresh: "r", expires: 0 });
|
|
expect(auth).toEqual({ apiKey: "token" });
|
|
});
|
|
|
|
it("openai-codex toAuth derives the api key from the access token", async () => {
|
|
const auth = await openaiCodexOAuth.toAuth({ type: "oauth", access: "token", refresh: "r", expires: 0 });
|
|
expect(auth).toEqual({ apiKey: "token" });
|
|
});
|
|
|
|
it("github-copilot toAuth derives baseUrl from the token proxy endpoint", async () => {
|
|
const access = "tid=abc;exp=123;proxy-ep=proxy.enterprise.example;rest";
|
|
const auth = await githubCopilotOAuth.toAuth({ type: "oauth", access, refresh: "r", expires: 0 });
|
|
expect(auth).toEqual({ apiKey: access, baseUrl: "https://api.enterprise.example" });
|
|
});
|
|
|
|
it("github-copilot toAuth falls back to the enterprise domain, then the individual endpoint", async () => {
|
|
const enterprise = await githubCopilotOAuth.toAuth({
|
|
type: "oauth",
|
|
access: "no-proxy-ep",
|
|
refresh: "r",
|
|
expires: 0,
|
|
enterpriseUrl: "https://company.ghe.com",
|
|
});
|
|
expect(enterprise.baseUrl).toBe("https://copilot-api.company.ghe.com");
|
|
|
|
const individual = await githubCopilotOAuth.toAuth({
|
|
type: "oauth",
|
|
access: "no-proxy-ep",
|
|
refresh: "r",
|
|
expires: 0,
|
|
});
|
|
expect(individual.baseUrl).toBe("https://api.individual.githubcopilot.com");
|
|
});
|
|
|
|
it("anthropic refresh exchanges the refresh token and returns a typed credential", async () => {
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn(async () =>
|
|
jsonResponse({ access_token: "new-access", refresh_token: "new-refresh", expires_in: 3600 }),
|
|
),
|
|
);
|
|
|
|
const refreshed = await anthropicOAuth.refresh({ type: "oauth", access: "old", refresh: "old-r", expires: 0 });
|
|
expect(refreshed.type).toBe("oauth");
|
|
expect(refreshed.access).toBe("new-access");
|
|
expect(refreshed.refresh).toBe("new-refresh");
|
|
expect(refreshed.expires).toBeGreaterThan(Date.now());
|
|
});
|
|
|
|
it("github-copilot refresh preserves the enterprise domain", async () => {
|
|
const fetchedUrls: string[] = [];
|
|
const fetchMock = vi.fn(async (input: unknown) => {
|
|
fetchedUrls.push(typeof input === "string" ? input : String(input));
|
|
return jsonResponse({ token: "new-token", expires_at: 9999999999 });
|
|
});
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
const refreshed = await githubCopilotOAuth.refresh({
|
|
type: "oauth",
|
|
access: "old",
|
|
refresh: "gh-token",
|
|
expires: 0,
|
|
enterpriseUrl: "company.ghe.com",
|
|
});
|
|
expect(refreshed.access).toBe("new-token");
|
|
expect(refreshed.enterpriseUrl).toBe("company.ghe.com");
|
|
expect(fetchedUrls[0]).toContain("api.company.ghe.com");
|
|
});
|
|
});
|
|
|
|
describe("OAuth through Models.getAuth (lazy load chain)", () => {
|
|
it("resolves stored anthropic oauth credentials via the lazy flow import", async () => {
|
|
const credentials = new InMemoryCredentialStore();
|
|
await credentials.modify("anthropic", async () => ({
|
|
type: "oauth",
|
|
access: "oauth-access-token",
|
|
refresh: "r",
|
|
expires: Date.now() + 60_000,
|
|
}));
|
|
const models = createModels({ credentials });
|
|
models.setProvider(anthropicProvider());
|
|
|
|
const model = (await models.getModels("anthropic"))[0];
|
|
const result = await models.getAuth(model);
|
|
expect(result?.auth.apiKey).toBe("oauth-access-token");
|
|
expect(result?.source).toBe("OAuth");
|
|
});
|
|
|
|
it("resolves stored github-copilot oauth credentials including per-credential baseUrl", async () => {
|
|
const access = "tid=abc;exp=123;proxy-ep=proxy.business.githubcopilot.com;rest";
|
|
const credentials = new InMemoryCredentialStore();
|
|
await credentials.modify("github-copilot", async () => ({
|
|
type: "oauth",
|
|
access,
|
|
refresh: "r",
|
|
expires: Date.now() + 60_000,
|
|
}));
|
|
const models = createModels({ credentials });
|
|
models.setProvider(githubCopilotProvider());
|
|
|
|
const model = (await models.getModels("github-copilot"))[0];
|
|
const result = await models.getAuth(model);
|
|
expect(result?.auth.apiKey).toBe(access);
|
|
expect(result?.auth.baseUrl).toBe("https://api.business.githubcopilot.com");
|
|
});
|
|
});
|