diff --git a/src/agent/models.ts b/src/agent/models.ts index 80bc11e..034a7ea 100644 --- a/src/agent/models.ts +++ b/src/agent/models.ts @@ -12,6 +12,8 @@ import type { ThinkingLevel } from "@earendil-works/pi-agent-core"; import { type ModelRole } from "../../config.ts"; import { DEFAULT_THINKING_ORDER } from "../core/settings.ts"; import packageMetadata from "../../package.json" with { type: "json" }; +import { createClaudeCodeProvider } from "./claude-code/provider.ts"; +import { streamClaudeCode } from "./claude-code/stream.ts"; /** App identity is immutable and shared by Models and every native Agent. * Agent's default Pi attribution otherwise overrides the Models identity. */ @@ -57,6 +59,7 @@ export function piModels() { if (!collection) { collection = createModels({ identity: APP_IDENTITY }); for (const provider of builtinProviders()) collection.setProvider(provider); + collection.setProvider(createClaudeCodeProvider(streamClaudeCode)); } return collection; } diff --git a/src/server/model-catalog.ts b/src/server/model-catalog.ts index 58b5cfc..129a754 100644 --- a/src/server/model-catalog.ts +++ b/src/server/model-catalog.ts @@ -34,6 +34,7 @@ import type { } from "../core/contracts/models.ts"; import type { EffectiveModelSpec, EffectiveSlot } from "../core/settings-resolution.ts"; import { AUTH_PROVIDER_KEYS } from "../core/auth-keys.ts"; +import { CLAUDE_CODE_PROVIDER } from "../agent/claude-code/constants.ts"; import { BACKGROUND_MODEL_ROLES, MODEL_SLOTS, type BackgroundModelRole, type ModelSlotName, type ModelsSettings } from "../core/settings.ts"; /** The picker's client-safe model entry (contract: core/contracts/models.ts). */ @@ -130,9 +131,10 @@ export function catalogProviders(): CatalogProvider[] { /** The providers whose key is present — the picker federates exactly * these (a keyless provider's models are unreachable by design; the - * Keys page is the discovery surface, same discipline as search). */ + * Keys page is the discovery surface, same discipline as search), plus + * claude-code, which runs on the local CLI's subscription and takes no key. */ function keyedProviderIds(): Set { - return new Set(catalogProviders().filter((p) => p.keyPresent).map((p) => p.id)); + return new Set([...catalogProviders().filter((p) => p.keyPresent).map((p) => p.id), CLAUDE_CODE_PROVIDER]); } /** Project one catalog model to the client DTO. */ diff --git a/tests/model-catalog.test.ts b/tests/model-catalog.test.ts index d28f4d4..5d93a8e 100644 --- a/tests/model-catalog.test.ts +++ b/tests/model-catalog.test.ts @@ -1,6 +1,7 @@ import { describe, it } from "node:test"; import assert from "node:assert/strict"; import { + catalogContextLimits, catalogDefaultsFor, catalogProviders, catalogRolePins, @@ -22,6 +23,7 @@ import { BACKGROUND_MODEL_ROLES } from "../src/core/settings.ts"; import { resolveSettings } from "../src/core/settings-resolution.ts"; import { readSettings } from "../src/store/settings.ts"; import { effectiveSettings } from "../src/server/stores.ts"; +import { providerKeyPresent } from "../src/server/keys.ts"; // The model-control wave (2026-09-12): pi-ai owns the registry — these pins // hold our projection + validation honest against the shipped catalog, and @@ -326,4 +328,26 @@ describe("pi thinking truth (pinned against pi-ai upgrades)", () => { const { piEngineFor } = await import("../src/agent/models.ts"); assert.throws(() => piEngineFor("prober"), /no configured model for role "prober"/); }); -}); \ No newline at end of file +}); +describe("the claude-code provider (subscription CLI, no key)", () => { + it("modelCatalog() lists claude-code opus/sonnet/haiku with no keys saved", () => { + const ids = modelCatalog() + .models.filter((m) => m.provider === "claude-code") + .map((m) => m.id); + assert.deepEqual(ids, ["haiku", "opus", "sonnet"]); + }); + + it("catalogContextLimits() has claude-code:opus", () => { + assert.ok((catalogContextLimits().get("claude-code:opus") ?? 0) > 0); + }); + + it('piEngineForSpec({ provider: "claude-code", id: "sonnet" }) resolves', () => { + const engine = piEngineForSpec({ provider: "claude-code", id: "sonnet" }); + assert.equal(engine.model.provider, "claude-code"); + assert.equal(engine.model.id, "sonnet"); + }); + + it('providerKeyPresent("claude-code") is true', () => { + assert.equal(providerKeyPresent("claude-code"), true); + }); +});