From 913b29efe596e0edaee7aec60de3779aa9ed03c9 Mon Sep 17 00:00:00 2001 From: Ivanko1608 <29154527+Ivanko1608@users.noreply.github.com> Date: Sat, 24 Oct 2026 20:24:57 +0400 Subject: [PATCH] feat: add subscription CLI provider catalog opus, sonnet and haiku aliases, low..max thinking, zero cost, keyless auth. Streaming is injected. --- src/agent/claude-code/constants.ts | 8 +++++ src/agent/claude-code/provider.ts | 33 +++++++++++++++++ tests/claude-code-provider.test.ts | 57 ++++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+) create mode 100644 src/agent/claude-code/constants.ts create mode 100644 src/agent/claude-code/provider.ts create mode 100644 tests/claude-code-provider.test.ts diff --git a/src/agent/claude-code/constants.ts b/src/agent/claude-code/constants.ts new file mode 100644 index 0000000..18ac8d7 --- /dev/null +++ b/src/agent/claude-code/constants.ts @@ -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", +} diff --git a/src/agent/claude-code/provider.ts b/src/agent/claude-code/provider.ts new file mode 100644 index 0000000..c190230 --- /dev/null +++ b/src/agent/claude-code/provider.ts @@ -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, "id" | "name">; + +export const CLAUDE_CODE_MODELS: readonly Model[] = [ + { ...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): 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 }, + }); +} diff --git a/tests/claude-code-provider.test.ts b/tests/claude-code-provider.test.ts new file mode 100644 index 0000000..a8ef24d --- /dev/null +++ b/tests/claude-code-provider.test.ts @@ -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); +});