diff --git a/.changeset/ai-sdk-drop-doctor-subpath.md b/.changeset/ai-sdk-drop-doctor-subpath.md new file mode 100644 index 0000000..7a736f2 --- /dev/null +++ b/.changeset/ai-sdk-drop-doctor-subpath.md @@ -0,0 +1,9 @@ +--- +"@gemstack/ai-sdk": minor +--- + +Remove the `@gemstack/ai-sdk/doctor` subpath (epic: framework-agnostic engine). + +The AI doctor check registered into `@rudderjs/console`'s doctor registry, coupling the agnostic engine to the Rudder CLI. It has moved to the Rudder binding `@rudderjs/ai/doctor` (same import path on that package). The `./doctor` export is removed here. + +**Breaking (0.x):** importing `@gemstack/ai-sdk/doctor` no longer resolves; use `@rudderjs/ai/doctor`. (The `@rudderjs/console` peer stays for now — `make:agent` and the `/server` provider still use it until they relocate too.) diff --git a/packages/ai-sdk/package.json b/packages/ai-sdk/package.json index 5a11a28..ca2de40 100644 --- a/packages/ai-sdk/package.json +++ b/packages/ai-sdk/package.json @@ -63,10 +63,6 @@ "import": "./dist/commands/ai-eval.js", "types": "./dist/commands/ai-eval.d.ts" }, - "./doctor": { - "import": "./dist/doctor.js", - "types": "./dist/doctor.d.ts" - }, "./observers": { "import": "./dist/observers.js", "types": "./dist/observers.d.ts" diff --git a/packages/ai-sdk/src/doctor.test.ts b/packages/ai-sdk/src/doctor.test.ts deleted file mode 100644 index 4cf723b..0000000 --- a/packages/ai-sdk/src/doctor.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { describe, it, before, after, beforeEach, afterEach } from 'node:test' -import assert from 'node:assert/strict' -import fs from 'node:fs' -import path from 'node:path' -import os from 'node:os' - -// Side-effect import: registers `ai:provider-keys`. -import './doctor.js' -import { getRegisteredChecks, type DoctorResult } from '@rudderjs/console' - -const CHECK_ID = 'ai:provider-keys' -const PROVIDER_ENV_KEYS = [ - 'ANTHROPIC_API_KEY', - 'OPENAI_API_KEY', - 'GOOGLE_AI_API_KEY', - 'AWS_ACCESS_KEY_ID', - 'GROQ_API_KEY', - 'OPENROUTER_API_KEY', -] - -let tmpDir: string -let originalCwd: string -const savedEnv: Record = {} - -before(() => { - originalCwd = process.cwd() - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-doctor-')) -}) -after(() => { - process.chdir(originalCwd) - fs.rmSync(tmpDir, { recursive: true, force: true }) -}) - -beforeEach(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }) - fs.mkdirSync(tmpDir, { recursive: true }) - fs.mkdirSync(path.join(tmpDir, 'config'), { recursive: true }) - process.chdir(tmpDir) - for (const k of PROVIDER_ENV_KEYS) { - savedEnv[k] = process.env[k] - delete process.env[k] - } -}) -afterEach(() => { - process.chdir(originalCwd) - for (const k of PROVIDER_ENV_KEYS) { - if (savedEnv[k] === undefined) delete process.env[k] - else process.env[k] = savedEnv[k] - } -}) - -function writeConfig(body: string): void { - fs.writeFileSync(path.join(tmpDir, 'config/ai.ts'), body, 'utf-8') -} - -async function runCheck(): Promise { - const check = getRegisteredChecks().find(c => c.id === CHECK_ID) - assert.ok(check, `expected ${CHECK_ID} to be registered`) - return check.run() -} - -describe('ai:provider-keys doctor check', () => { - it('warns (not errors) when a single cloud provider is declared with no key set', async () => { - writeConfig(`export default { default: 'anthropic', providers: { anthropic: { driver: 'anthropic' } } }`) - const result = await runCheck() - assert.strictEqual(result.status, 'warn') - assert.match(result.message, /1 cloud provider/) - assert.match(result.fix ?? '', /ANTHROPIC_API_KEY/) - }) - - it('warns when 3 cloud providers are declared with no keys set, listing all 3 env vars', async () => { - writeConfig(`export default { - providers: { - a: { driver: 'anthropic' }, - b: { driver: 'openai' }, - c: { driver: 'google' }, - } - }`) - const result = await runCheck() - assert.strictEqual(result.status, 'warn') - assert.match(result.fix ?? '', /ANTHROPIC_API_KEY/) - assert.match(result.fix ?? '', /OPENAI_API_KEY/) - assert.match(result.fix ?? '', /GOOGLE_AI_API_KEY/) - // Parenthetical mirrors the "some missing" branch for consistency. - assert.match(result.fix ?? '', /remove the providers from config\/ai\.ts if unused/) - }) - - it('warns when one of multiple cloud providers has a key (unchanged "partial" branch)', async () => { - writeConfig(`export default { - providers: { - a: { driver: 'anthropic' }, - b: { driver: 'openai' }, - } - }`) - process.env.ANTHROPIC_API_KEY = 'sk-test' - const result = await runCheck() - assert.strictEqual(result.status, 'warn') - assert.match(result.message, /1\/2/) - assert.match(result.fix ?? '', /OPENAI_API_KEY/) - }) - - it('is ok when all declared cloud providers have keys set', async () => { - writeConfig(`export default { - providers: { - a: { driver: 'anthropic' }, - b: { driver: 'openai' }, - } - }`) - process.env.ANTHROPIC_API_KEY = 'sk-test' - process.env.OPENAI_API_KEY = 'sk-test' - const result = await runCheck() - assert.strictEqual(result.status, 'ok') - }) - - it('is ok when there is no config/ai.ts at all', async () => { - const result = await runCheck() - assert.strictEqual(result.status, 'ok') - }) - - it('is ok when only local providers (ollama) are declared', async () => { - writeConfig(`export default { providers: { local: { driver: 'ollama' } } }`) - const result = await runCheck() - assert.strictEqual(result.status, 'ok') - }) -}) diff --git a/packages/ai-sdk/src/doctor.ts b/packages/ai-sdk/src/doctor.ts deleted file mode 100644 index d5df232..0000000 --- a/packages/ai-sdk/src/doctor.ts +++ /dev/null @@ -1,66 +0,0 @@ -// Doctor checks contributed by @gemstack/ai-sdk. - -import fs from 'node:fs' -import path from 'node:path' -import { registerDoctorCheck, type DoctorResult } from '@rudderjs/console' - -function readFileSafe(rel: string): string | null { - try { return fs.readFileSync(path.join(process.cwd(), rel), 'utf-8') } catch { return null } -} - -// Maps provider driver name → env var the user must set. Mirrors the -// driver names listed in @gemstack/ai-sdk's provider implementations. -const PROVIDER_ENV: Record = { - anthropic: 'ANTHROPIC_API_KEY', - openai: 'OPENAI_API_KEY', - google: 'GOOGLE_AI_API_KEY', - bedrock: 'AWS_ACCESS_KEY_ID', - groq: 'GROQ_API_KEY', - openrouter:'OPENROUTER_API_KEY', - // ollama, lmstudio: local — no key needed. -} - -// Extracts driver names referenced by config/ai.ts WITHOUT importing the -// module. We grep for `driver: ''` literals — covers the scaffolded -// shape. -function declaredProviders(): string[] { - const text = - readFileSafe('config/ai.ts') ?? - readFileSafe('config/ai.js') ?? - readFileSafe('config/ai.mjs') ?? '' - const matches = [...text.matchAll(/driver\s*:\s*['"]([^'"]+)['"]/g)] - return [...new Set(matches.map(m => m[1]!).filter(Boolean))] -} - -registerDoctorCheck({ - id: 'ai:provider-keys', - category: 'ai', - title: 'AI provider API keys', - run(): DoctorResult { - const providers = declaredProviders() - if (providers.length === 0) { - return { status: 'ok', message: 'no config/ai.ts or no providers declared — skip' } - } - const needsKey = providers.filter(p => p in PROVIDER_ENV) - if (needsKey.length === 0) { - return { status: 'ok', message: `${providers.length} provider(s) — all local (no keys required)` } - } - const missing = needsKey.filter(p => !process.env[PROVIDER_ENV[p]!]) - if (missing.length === needsKey.length) { - return { - status: 'warn', - message: `none of ${needsKey.length} cloud provider(s) have an API key set`, - fix: `Set at least one of: ${needsKey.map(p => PROVIDER_ENV[p]).join(', ')} (or remove the providers from config/ai.ts if unused)`, - detail: `Declared providers: ${needsKey.join(', ')}`, - } - } - if (missing.length > 0) { - return { - status: 'warn', - message: `${needsKey.length - missing.length}/${needsKey.length} cloud provider(s) have keys`, - fix: `Set missing keys: ${missing.map(p => PROVIDER_ENV[p]).join(', ')} (or remove the providers from config/ai.ts if unused)`, - } - } - return { status: 'ok', message: `${needsKey.length} cloud provider(s), all keys present` } - }, -})