From 46e1dc38c66603f0873adddac452e93099581e99 Mon Sep 17 00:00:00 2001 From: Ricardo Torres Date: Sat, 25 Apr 2026 19:38:54 -0600 Subject: [PATCH 1/3] feat(pi-continuous-learning): configure analyzer provider --- packages/pi-continuous-learning/README.md | 10 +-- .../pi-continuous-learning/docs/internals.md | 14 ++-- .../docs/specification.md | 5 +- .../src/cli/analyze-model.test.ts | 68 +++++++++++++++++++ .../src/cli/analyze-model.ts | 38 +++++++++++ .../pi-continuous-learning/src/cli/analyze.ts | 34 +++------- .../pi-continuous-learning/src/config.test.ts | 11 ++- packages/pi-continuous-learning/src/config.ts | 2 + .../src/instinct-injector.test.ts | 1 + .../src/instinct-loader.test.ts | 1 + .../pi-continuous-learning/src/types.test.ts | 2 + packages/pi-continuous-learning/src/types.ts | 1 + 12 files changed, 147 insertions(+), 40 deletions(-) create mode 100644 packages/pi-continuous-learning/src/cli/analyze-model.test.ts create mode 100644 packages/pi-continuous-learning/src/cli/analyze-model.ts diff --git a/packages/pi-continuous-learning/README.md b/packages/pi-continuous-learning/README.md index 5feaac7..b0579a6 100644 --- a/packages/pi-continuous-learning/README.md +++ b/packages/pi-continuous-learning/README.md @@ -48,7 +48,7 @@ This installs the extension globally and makes the `pi-cl-analyze` CLI available |---|---| | [Pi](https://github.com/nicholasgasior/pi-coding-agent) | >= 0.62.0 | | Node.js | >= 18 | -| LLM provider | configured in Pi (analyzer defaults to Haiku) | +| LLM provider | configured in Pi (analyzer defaults to Anthropic Haiku; provider/model are configurable) | --- @@ -62,7 +62,7 @@ Every hook event in your session — tool calls, prompts, errors, corrections, m ### 2. Analysis -The background analyzer (`pi-cl-analyze`) processes new observations using Haiku. It applies three tiers of quality filtering: +The background analyzer (`pi-cl-analyze`) processes new observations using the configured provider/model (Anthropic Haiku by default). It applies three tiers of quality filtering: | Tier | What it captures | How it's stored | |---|---|---| @@ -216,7 +216,7 @@ Ask Pi things like _"show me my instincts"_, _"merge these two"_, or _"remember ## Background analyzer -The analyzer is a standalone CLI that processes all your projects in a single pass and creates/updates instincts using Haiku. It runs outside of Pi sessions so it never causes lag or interference. +The analyzer is a standalone CLI that processes all your projects in a single pass and creates/updates instincts using the configured provider/model. It runs outside of Pi sessions so it never causes lag or interference. ### Running manually @@ -233,7 +233,7 @@ pi-cl-analyze 5. Applies passive confidence decay to all instincts 6. Runs cleanup (expired/contradicted instincts) 7. Scores observation batches by signal strength — low-signal batches are skipped to save cost -8. Calls Haiku to analyse patterns and write instinct files +8. Calls the configured model to analyse patterns and write instinct files 9. Saves a cursor so only new observations are processed next time **Safety features:** @@ -378,6 +378,7 @@ All defaults work out of the box. Override at `~/.pi/continuous-learning/config. "max_instincts": 20, "max_injection_chars": 4000, "model": "claude-haiku-4-5", + "provider": "anthropic", "timeout_seconds": 120, "active_hours_start": 8, "active_hours_end": 23, @@ -402,6 +403,7 @@ All defaults work out of the box. Override at `~/.pi/continuous-learning/config. | `max_instincts` | 20 | Maximum instincts injected per turn | | `max_injection_chars` | 4000 | Character budget for the injection block (~1,000 tokens) | | `model` | `claude-haiku-4-5` | Model for the background analyzer | +| `provider` | `anthropic` | Pi provider for the background analyzer model | | `timeout_seconds` | 120 | Per-project LLM session timeout | | `active_hours_start` | 8 | Hour (0–23) at which the active observation window starts | | `active_hours_end` | 23 | Hour (0–23) at which the active observation window ends | diff --git a/packages/pi-continuous-learning/docs/internals.md b/packages/pi-continuous-learning/docs/internals.md index 106b99e..8625424 100644 --- a/packages/pi-continuous-learning/docs/internals.md +++ b/packages/pi-continuous-learning/docs/internals.md @@ -9,7 +9,7 @@ How pi-continuous-learning works under the hood. Covers the data flow, file layo The system has two separate runtimes: 1. **Pi Extension** (runs inside Pi sessions): Observes events, records observations, injects instincts into prompts, registers LLM tools, and provides slash commands. -2. **Standalone Analyzer** (`src/cli/analyze.ts`): Runs outside Pi via cron/launchd. Iterates all projects, analyzes observations using Haiku + the Pi SDK, and writes instinct files. +2. **Standalone Analyzer** (`src/cli/analyze.ts`): Runs outside Pi via cron/launchd. Iterates all projects, analyzes observations using the configured provider/model via the Pi SDK, and writes instinct files. --- @@ -79,6 +79,7 @@ Defined in `config.ts`. The extension reads `~/.pi/continuous-learning/config.js min_confidence: 0.5, // Instincts below this are not injected max_instincts: 20, // Cap on instincts injected per turn model: "claude-haiku-4-5", // Model for the analyzer + provider: "anthropic", // Pi provider for the analyzer model timeout_seconds: 120, // Per-project timeout for analyzer LLM session active_hours_start: 8, // (legacy, unused by standalone analyzer) active_hours_end: 23, // (legacy, unused by standalone analyzer) @@ -166,17 +167,16 @@ instinct-cleanup.ts -- auto-cleanup: delete flagged/TTL/over-cap instincts instinct-decay.ts -- apply passive confidence decay (-0.05/week) after cleanup | v -Create AgentSession (Pi SDK) with: +Resolve analyzer provider/model from config: + - provider: anthropic (configurable) - model: claude-haiku-4-5 (configurable) - - customTools: instinct_list, instinct_read, instinct_write, instinct_delete - - systemPrompt: analyzer instructions (pattern detection, scoring rules, conservativeness) - - sessionManager: in-memory (no persistence) + - credentials: existing Pi auth for that provider | v -session.prompt(userPrompt) -- sends observations + project context to Haiku +runSingleShot(context, model, apiKey) -- sends observations + project context to the configured model | v -Haiku analyzes patterns, calls instinct_write/instinct_read tools +Model analyzes patterns and returns structured instinct changes | v session.dispose(), update last_analyzed_at in project.json diff --git a/packages/pi-continuous-learning/docs/specification.md b/packages/pi-continuous-learning/docs/specification.md index ecbc42e..88ce140 100644 --- a/packages/pi-continuous-learning/docs/specification.md +++ b/packages/pi-continuous-learning/docs/specification.md @@ -10,7 +10,7 @@ Inspired by [everything-claude-code/continuous-learning-v2](https://github.com/n 1. **Observe** - capture tool calls, user prompts, and outcomes via Pi extension events 2. **Record** - write observations to project-scoped JSONL files -3. **Analyze** - run a background job every 5 minutes using Haiku to detect patterns +3. **Analyze** - run a background job every 5 minutes using the configured analyzer provider/model to detect patterns 4. **Learn** - create/update instinct files (YAML-frontmatter markdown) with confidence scoring 5. **Apply** - inject relevant instincts into Pi's system prompt via `before_agent_start` 6. **Validate** - closed-loop feedback: track whether injected instincts align with actual session behavior, adjusting confidence based on real outcomes rather than observation count alone @@ -612,6 +612,7 @@ Stored at `~/.pi/continuous-learning/config.json`: }, "analyzer": { "model": "claude-haiku-4-5", + "provider": "anthropic", "timeout_seconds": 120, "max_observations_per_analysis": 500, "max_turns": 10 @@ -623,7 +624,7 @@ Stored at `~/.pi/continuous-learning/config.json`: } ``` -Defaults are used when config file is absent. The extension reads config on `session_start` and caches it. +Defaults are used when config file is absent. The extension reads config on `session_start` and caches it. The analyzer defaults to Anthropic Haiku, but `provider` and `model` can be overridden to any Pi-registered provider/model pair. --- diff --git a/packages/pi-continuous-learning/src/cli/analyze-model.test.ts b/packages/pi-continuous-learning/src/cli/analyze-model.test.ts new file mode 100644 index 0000000..0cf3189 --- /dev/null +++ b/packages/pi-continuous-learning/src/cli/analyze-model.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, it, vi } from "vitest"; +import { DEFAULT_CONFIG } from "../config.js"; +import type { Config } from "../types.js"; +import { resolveAnalyzerModel } from "./analyze-model.js"; + +function config(overrides: Partial = {}): Config { + return { ...DEFAULT_CONFIG, ...overrides }; +} + +describe("resolveAnalyzerModel", () => { + it("uses the configured provider and model", async () => { + const authStorage = { + getApiKey: vi.fn().mockResolvedValue("codex-token"), + }; + + const result = await resolveAnalyzerModel( + config({ provider: "openai-codex", model: "gpt-5.4-mini" }), + authStorage, + ); + + expect(result.providerId).toBe("openai-codex"); + expect(result.modelId).toBe("gpt-5.4-mini"); + expect(result.model.provider).toBe("openai-codex"); + expect(result.model.id).toBe("gpt-5.4-mini"); + expect(result.apiKey).toBe("codex-token"); + expect(authStorage.getApiKey).toHaveBeenCalledWith("openai-codex"); + }); + + it("keeps Anthropic Haiku as the backwards-compatible default", async () => { + const authStorage = { + getApiKey: vi.fn().mockResolvedValue("anthropic-token"), + }; + + const result = await resolveAnalyzerModel(config(), authStorage); + + expect(result.providerId).toBe("anthropic"); + expect(result.modelId).toBe("claude-haiku-4-5"); + expect(result.model.provider).toBe("anthropic"); + expect(authStorage.getApiKey).toHaveBeenCalledWith("anthropic"); + }); + + it("throws a provider-specific error when credentials are missing", async () => { + const authStorage = { + getApiKey: vi.fn().mockResolvedValue(undefined), + }; + + await expect( + resolveAnalyzerModel( + config({ provider: "openai-codex", model: "gpt-5.4-mini" }), + authStorage, + ), + ).rejects.toThrow("No API key configured for provider: openai-codex"); + }); + + it("throws a provider/model-specific error for unknown model ids", async () => { + const authStorage = { + getApiKey: vi.fn().mockResolvedValue("token"), + }; + + await expect( + resolveAnalyzerModel( + config({ provider: "openai-codex", model: "not-a-real-model" }), + authStorage, + ), + ).rejects.toThrow("Unknown analyzer model: openai-codex/not-a-real-model"); + expect(authStorage.getApiKey).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/pi-continuous-learning/src/cli/analyze-model.ts b/packages/pi-continuous-learning/src/cli/analyze-model.ts new file mode 100644 index 0000000..4e900a5 --- /dev/null +++ b/packages/pi-continuous-learning/src/cli/analyze-model.ts @@ -0,0 +1,38 @@ +import type { AuthStorage } from "@mariozechner/pi-coding-agent"; +import { getModel, type Api, type Model } from "@mariozechner/pi-ai"; +import { DEFAULT_CONFIG } from "../config.js"; +import type { Config } from "../types.js"; + +type AnalyzerAuthStorage = Pick; + +export interface AnalyzerModelResolution { + readonly apiKey: string; + readonly model: Model; + readonly modelId: string; + readonly providerId: string; +} + +export async function resolveAnalyzerModel( + config: Config, + authStorage: AnalyzerAuthStorage, +): Promise { + const providerId = config.provider || DEFAULT_CONFIG.provider; + const modelId = config.model || DEFAULT_CONFIG.model; + const model = getModel(providerId as never, modelId as never) as + | Model + | undefined; + + if (!model) { + throw new Error(`Unknown analyzer model: ${providerId}/${modelId}`); + } + + const apiKey = await authStorage.getApiKey(providerId); + if (!apiKey) { + throw new Error( + `No API key configured for provider: ${providerId}. ` + + "Set credentials via Pi auth.json, /login, or the provider's API key environment variable.", + ); + } + + return { apiKey, model, modelId, providerId }; +} diff --git a/packages/pi-continuous-learning/src/cli/analyze.ts b/packages/pi-continuous-learning/src/cli/analyze.ts index 4d209cf..f49ab00 100644 --- a/packages/pi-continuous-learning/src/cli/analyze.ts +++ b/packages/pi-continuous-learning/src/cli/analyze.ts @@ -9,7 +9,6 @@ import { import { createHash } from "node:crypto"; import { join } from "node:path"; import { AuthStorage } from "@mariozechner/pi-coding-agent"; -import { getModel } from "@mariozechner/pi-ai"; import { loadConfig, DEFAULT_CONFIG } from "../config.js"; import type { InstalledSkill, ProjectEntry } from "../types.js"; @@ -74,6 +73,7 @@ import { type ProjectRunStats, type RunSummary, } from "./analyze-logger.js"; +import { resolveAnalyzerModel } from "./analyze-model.js"; // --------------------------------------------------------------------------- // Lockfile guard - ensures only one instance runs at a time @@ -400,18 +400,10 @@ async function analyzeProject( }, ); - const authStorage = AuthStorage.create(); - const modelId = (config.model || DEFAULT_CONFIG.model) as Parameters< - typeof getModel - >[1]; - const model = getModel("anthropic", modelId); - const apiKey = await authStorage.getApiKey("anthropic"); - - if (!apiKey) { - throw new Error( - "No Anthropic API key configured. Set via auth.json or ANTHROPIC_API_KEY.", - ); - } + const { apiKey, model, modelId } = await resolveAnalyzerModel( + config, + AuthStorage.create(), + ); const context = { systemPrompt: buildSingleShotSystemPrompt(), @@ -672,18 +664,10 @@ async function consolidateProject( projectId: project.id, }); - const authStorage = AuthStorage.create(); - const modelId = (config.model || DEFAULT_CONFIG.model) as Parameters< - typeof getModel - >[1]; - const model = getModel("anthropic", modelId); - const apiKey = await authStorage.getApiKey("anthropic"); - - if (!apiKey) { - throw new Error( - "No Anthropic API key configured. Set via auth.json or ANTHROPIC_API_KEY.", - ); - } + const { apiKey, model, modelId } = await resolveAnalyzerModel( + config, + AuthStorage.create(), + ); const context = { systemPrompt, diff --git a/packages/pi-continuous-learning/src/config.test.ts b/packages/pi-continuous-learning/src/config.test.ts index 64dade7..c1ccdaa 100644 --- a/packages/pi-continuous-learning/src/config.test.ts +++ b/packages/pi-continuous-learning/src/config.test.ts @@ -35,6 +35,7 @@ describe("loadConfig", () => { max_instincts: 30, max_injection_chars: 6000, model: "claude-opus-4-5", + provider: "anthropic", timeout_seconds: 240, active_hours_start: 9, active_hours_end: 18, @@ -67,7 +68,11 @@ describe("loadConfig", () => { }); it("merges partial overrides with defaults (overrides win)", () => { - const partial = { run_interval_minutes: 15, model: "claude-opus-4-5" }; + const partial = { + run_interval_minutes: 15, + provider: "openai-codex", + model: "gpt-5.4-mini", + }; mockedFs.existsSync.mockReturnValue(true); mockedFs.readFileSync.mockReturnValue( JSON.stringify(partial) as unknown as ReturnType, @@ -76,7 +81,8 @@ describe("loadConfig", () => { const config = loadConfig(); expect(config.run_interval_minutes).toBe(15); - expect(config.model).toBe("claude-opus-4-5"); + expect(config.model).toBe("gpt-5.4-mini"); + expect(config.provider).toBe("openai-codex"); // Remaining fields come from defaults expect(config.min_observations_to_analyze).toBe( DEFAULT_CONFIG.min_observations_to_analyze, @@ -109,6 +115,7 @@ describe("loadConfig", () => { expect(DEFAULT_CONFIG.min_confidence).toBe(0.5); expect(DEFAULT_CONFIG.max_instincts).toBe(20); expect(DEFAULT_CONFIG.model).toBe("claude-haiku-4-5"); + expect(DEFAULT_CONFIG.provider).toBe("anthropic"); expect(DEFAULT_CONFIG.timeout_seconds).toBe(120); }); diff --git a/packages/pi-continuous-learning/src/config.ts b/packages/pi-continuous-learning/src/config.ts index d301da4..c1e41ac 100644 --- a/packages/pi-continuous-learning/src/config.ts +++ b/packages/pi-continuous-learning/src/config.ts @@ -77,6 +77,7 @@ export const DEFAULT_CONFIG: Config = { max_instincts: 20, max_injection_chars: 4000, model: "claude-haiku-4-5", + provider: "anthropic", timeout_seconds: 120, active_hours_start: 8, active_hours_end: 23, @@ -110,6 +111,7 @@ const PartialConfigSchema = Type.Partial( max_instincts: Type.Number(), max_injection_chars: Type.Number(), model: Type.String(), + provider: Type.String(), timeout_seconds: Type.Number(), active_hours_start: Type.Number(), active_hours_end: Type.Number(), diff --git a/packages/pi-continuous-learning/src/instinct-injector.test.ts b/packages/pi-continuous-learning/src/instinct-injector.test.ts index f65a589..f538412 100644 --- a/packages/pi-continuous-learning/src/instinct-injector.test.ts +++ b/packages/pi-continuous-learning/src/instinct-injector.test.ts @@ -52,6 +52,7 @@ const BASE_CONFIG: Config = { max_instincts: 20, max_injection_chars: 4000, model: "claude-haiku-4-5", + provider: "anthropic", timeout_seconds: 120, active_hours_start: 8, active_hours_end: 23, diff --git a/packages/pi-continuous-learning/src/instinct-loader.test.ts b/packages/pi-continuous-learning/src/instinct-loader.test.ts index 2423c15..a138d68 100644 --- a/packages/pi-continuous-learning/src/instinct-loader.test.ts +++ b/packages/pi-continuous-learning/src/instinct-loader.test.ts @@ -49,6 +49,7 @@ const BASE_CONFIG: Config = { max_instincts: 20, max_injection_chars: 4000, model: "claude-haiku-4-5", + provider: "anthropic", timeout_seconds: 120, active_hours_start: 8, active_hours_end: 23, diff --git a/packages/pi-continuous-learning/src/types.test.ts b/packages/pi-continuous-learning/src/types.test.ts index c8e6224..d5d1c95 100644 --- a/packages/pi-continuous-learning/src/types.test.ts +++ b/packages/pi-continuous-learning/src/types.test.ts @@ -152,6 +152,7 @@ describe("types exports", () => { max_instincts: 20, max_injection_chars: 4000, model: "claude-haiku-4-5", + provider: "anthropic", timeout_seconds: 120, active_hours_start: 8, active_hours_end: 23, @@ -172,6 +173,7 @@ describe("types exports", () => { }; expect(config.run_interval_minutes).toBe(5); expect(config.model).toBe("claude-haiku-4-5"); + expect(config.provider).toBe("anthropic"); expect(config.active_hours_start).toBe(8); expect(config.max_idle_seconds).toBe(1800); expect(config.max_total_instincts_per_project).toBe(30); diff --git a/packages/pi-continuous-learning/src/types.ts b/packages/pi-continuous-learning/src/types.ts index 1668f6c..2253d99 100644 --- a/packages/pi-continuous-learning/src/types.ts +++ b/packages/pi-continuous-learning/src/types.ts @@ -130,6 +130,7 @@ export interface Config { max_instincts: number; max_injection_chars: number; model: string; + provider: string; timeout_seconds: number; active_hours_start: number; // 0-23 active_hours_end: number; // 0-23 From 6ca5bd1a8b3964f35896036a950e6cad4f189075 Mon Sep 17 00:00:00 2001 From: MattDevy Date: Mon, 27 Apr 2026 10:01:53 +0100 Subject: [PATCH 2/3] fix(pi-continuous-learning): replace as-never casts with type guard and deduplicate AuthStorage Use isKnownProvider() guard (via getProviders()) to validate the configured provider before calling getModel, giving a clear error on unknown providers without needing unsafe casts. Remove redundant DEFAULT_CONFIG fallbacks since loadConfig always merges defaults. Create a single AuthStorage instance per run and thread it through analyzeProject/consolidateProject instead of constructing one per project. --- .../src/cli/analyze-model.ts | 29 ++++++++++++++----- .../pi-continuous-learning/src/cli/analyze.ts | 11 +++++-- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/packages/pi-continuous-learning/src/cli/analyze-model.ts b/packages/pi-continuous-learning/src/cli/analyze-model.ts index 4e900a5..a56dcf9 100644 --- a/packages/pi-continuous-learning/src/cli/analyze-model.ts +++ b/packages/pi-continuous-learning/src/cli/analyze-model.ts @@ -1,6 +1,11 @@ import type { AuthStorage } from "@mariozechner/pi-coding-agent"; -import { getModel, type Api, type Model } from "@mariozechner/pi-ai"; -import { DEFAULT_CONFIG } from "../config.js"; +import { + getModel, + getProviders, + type Api, + type KnownProvider, + type Model, +} from "@mariozechner/pi-ai"; import type { Config } from "../types.js"; type AnalyzerAuthStorage = Pick; @@ -12,15 +17,25 @@ export interface AnalyzerModelResolution { readonly providerId: string; } +function isKnownProvider(value: string): value is KnownProvider { + return (getProviders() as string[]).includes(value); +} + export async function resolveAnalyzerModel( config: Config, authStorage: AnalyzerAuthStorage, ): Promise { - const providerId = config.provider || DEFAULT_CONFIG.provider; - const modelId = config.model || DEFAULT_CONFIG.model; - const model = getModel(providerId as never, modelId as never) as - | Model - | undefined; + const providerId = config.provider; + const modelId = config.model; + + if (!isKnownProvider(providerId)) { + throw new Error(`Unknown analyzer model: ${providerId}/${modelId}`); + } + + // getModel returns undefined for unknown model IDs but its overload signature + // only accepts known model IDs — cast the result to include undefined so the + // runtime guard below is reachable for arbitrary config values. + const model = getModel(providerId, modelId as never) as Model | undefined; if (!model) { throw new Error(`Unknown analyzer model: ${providerId}/${modelId}`); diff --git a/packages/pi-continuous-learning/src/cli/analyze.ts b/packages/pi-continuous-learning/src/cli/analyze.ts index f49ab00..e46b025 100644 --- a/packages/pi-continuous-learning/src/cli/analyze.ts +++ b/packages/pi-continuous-learning/src/cli/analyze.ts @@ -230,6 +230,7 @@ async function analyzeProject( config: ReturnType, baseDir: string, logger: AnalyzeLogger, + authStorage: AuthStorage, ): Promise { const meta = loadProjectMeta(project.id, baseDir); @@ -402,7 +403,7 @@ async function analyzeProject( const { apiKey, model, modelId } = await resolveAnalyzerModel( config, - AuthStorage.create(), + authStorage, ); const context = { @@ -587,6 +588,7 @@ async function consolidateProject( baseDir: string, logger: AnalyzeLogger, force: boolean, + authStorage: AuthStorage, ): Promise { const obsPath = getObservationsPath(project.id, baseDir); const sessionCount = countDistinctSessions(obsPath); @@ -666,7 +668,7 @@ async function consolidateProject( const { apiKey, model, modelId } = await resolveAnalyzerModel( config, - AuthStorage.create(), + authStorage, ); const context = { @@ -864,6 +866,7 @@ async function main(): Promise { let skipped = 0; let errored = 0; const allProjectStats: ProjectRunStats[] = []; + const authStorage = AuthStorage.create(); if (isConsolidateOnly) { // --consolidate: manual trigger, consolidation only, skip gates @@ -875,6 +878,7 @@ async function main(): Promise { baseDir, logger, true, + authStorage, ); if (result.ran && result.stats) { processed++; @@ -898,7 +902,7 @@ async function main(): Promise { // Normal mode: analyze observations, then opportunistic consolidation for (const project of projects) { try { - const result = await analyzeProject(project, config, baseDir, logger); + const result = await analyzeProject(project, config, baseDir, logger, authStorage); if (result.ran && result.stats) { processed++; allProjectStats.push(result.stats); @@ -928,6 +932,7 @@ async function main(): Promise { baseDir, logger, false, + authStorage, ); if (result.ran && result.stats) { processed++; From e09925c53d9f92b9cd4fd125c06354da4916e6ab Mon Sep 17 00:00:00 2001 From: MattDevy Date: Mon, 27 Apr 2026 10:06:11 +0100 Subject: [PATCH 3/3] fix(pi-continuous-learning): distinguish unknown provider vs model errors and add missing test Throws 'Unknown analyzer provider: {id}' when the provider string is not registered, instead of conflating it with the unknown-model message. Adds a test covering the !isKnownProvider branch. --- .../src/cli/analyze-model.test.ts | 14 ++++++++++++++ .../src/cli/analyze-model.ts | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/pi-continuous-learning/src/cli/analyze-model.test.ts b/packages/pi-continuous-learning/src/cli/analyze-model.test.ts index 0cf3189..744cc05 100644 --- a/packages/pi-continuous-learning/src/cli/analyze-model.test.ts +++ b/packages/pi-continuous-learning/src/cli/analyze-model.test.ts @@ -65,4 +65,18 @@ describe("resolveAnalyzerModel", () => { ).rejects.toThrow("Unknown analyzer model: openai-codex/not-a-real-model"); expect(authStorage.getApiKey).not.toHaveBeenCalled(); }); + + it("throws a provider-specific error for unknown provider strings", async () => { + const authStorage = { + getApiKey: vi.fn().mockResolvedValue("token"), + }; + + await expect( + resolveAnalyzerModel( + config({ provider: "not-a-real-provider", model: "claude-haiku-4-5" }), + authStorage, + ), + ).rejects.toThrow("Unknown analyzer provider: not-a-real-provider"); + expect(authStorage.getApiKey).not.toHaveBeenCalled(); + }); }); diff --git a/packages/pi-continuous-learning/src/cli/analyze-model.ts b/packages/pi-continuous-learning/src/cli/analyze-model.ts index a56dcf9..46ad090 100644 --- a/packages/pi-continuous-learning/src/cli/analyze-model.ts +++ b/packages/pi-continuous-learning/src/cli/analyze-model.ts @@ -29,7 +29,7 @@ export async function resolveAnalyzerModel( const modelId = config.model; if (!isKnownProvider(providerId)) { - throw new Error(`Unknown analyzer model: ${providerId}/${modelId}`); + throw new Error(`Unknown analyzer provider: ${providerId}`); } // getModel returns undefined for unknown model IDs but its overload signature