feat: add subscription CLI provider catalog
opus, sonnet and haiku aliases, low..max thinking, zero cost, keyless auth. Streaming is injected.
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
export const CLAUDE_CODE_PROVIDER = "claude-code";
|
||||
|
||||
export const CLAUDE_CODE_API = "claude-code-cli";
|
||||
|
||||
export enum ClaudeCodeError {
|
||||
CliNotFound = "claude CLI not found",
|
||||
NotSignedIn = "not signed in",
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { createProvider, type Api, type Model, type Provider, type SimpleStreamOptions, type StreamFunction } from "@earendil-works/pi-ai";
|
||||
|
||||
import { CLAUDE_CODE_API, CLAUDE_CODE_PROVIDER } from "./constants.ts";
|
||||
|
||||
const SHARED_MODEL_FIELDS = {
|
||||
api: CLAUDE_CODE_API,
|
||||
provider: CLAUDE_CODE_PROVIDER,
|
||||
baseUrl: "",
|
||||
input: ["text", "image"],
|
||||
// Billed to the user's Claude subscription, not per token.
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
reasoning: true,
|
||||
// The CLI has no thinking-off switch; effort low is the floor.
|
||||
thinkingLevelMap: { off: null, minimal: null, low: "low", medium: "medium", high: "high", xhigh: "xhigh", max: "max" },
|
||||
contextWindow: 1_000_000,
|
||||
maxTokens: 128_000,
|
||||
} satisfies Omit<Model<Api>, "id" | "name">;
|
||||
|
||||
export const CLAUDE_CODE_MODELS: readonly Model<Api>[] = [
|
||||
{ ...SHARED_MODEL_FIELDS, id: "opus", name: "Claude Opus" },
|
||||
{ ...SHARED_MODEL_FIELDS, id: "sonnet", name: "Claude Sonnet" },
|
||||
{ ...SHARED_MODEL_FIELDS, id: "haiku", name: "Claude Haiku" },
|
||||
];
|
||||
|
||||
export function createClaudeCodeProvider(streamSimple: StreamFunction<Api, SimpleStreamOptions>): Provider {
|
||||
return createProvider({
|
||||
id: CLAUDE_CODE_PROVIDER,
|
||||
name: "Claude Code",
|
||||
auth: { apiKey: { name: "Claude Code CLI", resolve: async () => ({ auth: {}, source: "claude CLI" }) } },
|
||||
models: CLAUDE_CODE_MODELS,
|
||||
api: { stream: streamSimple, streamSimple },
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Claude Code provider catalog (src/agent/claude-code/provider.ts).
|
||||
*
|
||||
* Registered on a fresh pi model collection with a stub stream function, so
|
||||
* only the catalog and auth surface are under test here.
|
||||
*/
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { clampThinkingLevel, createAssistantMessageEventStream, createModels, getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
||||
|
||||
import { CLAUDE_CODE_PROVIDER } from "../src/agent/claude-code/constants.ts";
|
||||
import { createClaudeCodeProvider } from "../src/agent/claude-code/provider.ts";
|
||||
|
||||
function createCollection() {
|
||||
const collection = createModels();
|
||||
collection.setProvider(createClaudeCodeProvider(() => createAssistantMessageEventStream()));
|
||||
return collection;
|
||||
}
|
||||
|
||||
test("lists exactly the opus, sonnet and haiku aliases", () => {
|
||||
const models = createCollection().getModels(CLAUDE_CODE_PROVIDER);
|
||||
assert.deepEqual(
|
||||
models.map((m) => m.id),
|
||||
["opus", "sonnet", "haiku"],
|
||||
);
|
||||
assert.deepEqual(
|
||||
models.map((m) => m.name),
|
||||
["Claude Opus", "Claude Sonnet", "Claude Haiku"],
|
||||
);
|
||||
});
|
||||
|
||||
test("supports low through max thinking and clamps off to low", () => {
|
||||
for (const model of createCollection().getModels(CLAUDE_CODE_PROVIDER)) {
|
||||
assert.deepEqual(getSupportedThinkingLevels(model), ["low", "medium", "high", "xhigh", "max"]);
|
||||
assert.equal(clampThinkingLevel(model, "off"), "low");
|
||||
assert.equal(clampThinkingLevel(model, "minimal"), "low");
|
||||
}
|
||||
});
|
||||
|
||||
test("accepts text and image input", () => {
|
||||
for (const model of createCollection().getModels(CLAUDE_CODE_PROVIDER)) {
|
||||
assert.deepEqual(model.input, ["text", "image"]);
|
||||
}
|
||||
});
|
||||
|
||||
test("every cost field is zero", () => {
|
||||
for (const model of createCollection().getModels(CLAUDE_CODE_PROVIDER)) {
|
||||
assert.deepEqual(model.cost, { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 });
|
||||
}
|
||||
});
|
||||
|
||||
test("auth resolves as configured without any key", async () => {
|
||||
const collection = createCollection();
|
||||
assert.ok(await collection.checkAuth(CLAUDE_CODE_PROVIDER));
|
||||
assert.equal((await collection.getAvailable(CLAUDE_CODE_PROVIDER)).length, 3);
|
||||
});
|
||||
Reference in New Issue
Block a user