diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f2210387..1e7bab17d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- **AIServicePlugin Auto-Detection** — AIServicePlugin now automatically detects and initializes + LLM providers based on environment variables, eliminating the need for manual adapter configuration + in each deployment: + - Auto-detection priority: `AI_GATEWAY_MODEL` → `OPENAI_API_KEY` → `ANTHROPIC_API_KEY` → `GOOGLE_GENERATIVE_AI_API_KEY` + - Graceful fallback to MemoryLLMAdapter when no provider is configured + - Comprehensive logging of selected provider and warnings for missing SDKs + - Supports custom model selection via `AI_MODEL` environment variable + - Consistent behavior across CLI, Vercel, Docker, and custom deployments + - Dynamic import failures are handled as soft errors with automatic fallback + ([#1067](https://github.com/objectstack-ai/framework/issues/1067)) + - **Metadata Versioning & History** — Comprehensive version history tracking and rollback capabilities for metadata items. Key features include: - `MetadataHistoryRecordSchema` defining structure for historical snapshots diff --git a/apps/studio/package.json b/apps/studio/package.json index b84c871ba..75d64523d 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -17,6 +17,10 @@ "preview": "vite preview" }, "dependencies": { + "@ai-sdk/anthropic": "^3.0.0", + "@ai-sdk/gateway": "^3.0.0", + "@ai-sdk/google": "^3.0.0", + "@ai-sdk/openai": "^3.0.0", "@ai-sdk/react": "^3.0.144", "@hono/node-server": "^1.19.11", "@objectstack/client": "workspace:*", diff --git a/apps/studio/scripts/build-vercel.sh b/apps/studio/scripts/build-vercel.sh index bd8908d79..ab516bab6 100755 --- a/apps/studio/scripts/build-vercel.sh +++ b/apps/studio/scripts/build-vercel.sh @@ -61,6 +61,17 @@ if [ -d "../../node_modules/@libsql" ]; then else echo "[build-vercel] ⚠ @libsql not found (skipped)" fi +# Copy the @ai-sdk scope (dynamically loaded provider packages) +if [ -d "../../node_modules/@ai-sdk" ]; then + mkdir -p "node_modules/@ai-sdk" + for pkg in ../../node_modules/@ai-sdk/*/; do + pkgname="$(basename "$pkg")" + cp -rL "$pkg" "node_modules/@ai-sdk/$pkgname" + done + echo "[build-vercel] ✓ Copied @ai-sdk/*" +else + echo "[build-vercel] ⚠ @ai-sdk not found (skipped)" +fi # 4. Copy Vite build output to public/ for static file serving rm -rf public diff --git a/apps/studio/scripts/bundle-api.mjs b/apps/studio/scripts/bundle-api.mjs index ec985e9d0..63c5f2b03 100644 --- a/apps/studio/scripts/bundle-api.mjs +++ b/apps/studio/scripts/bundle-api.mjs @@ -18,6 +18,11 @@ import { build } from 'esbuild'; const EXTERNAL = [ '@libsql/client', 'better-sqlite3', + // AI SDK provider packages — dynamically imported based on env vars + '@ai-sdk/anthropic', + '@ai-sdk/gateway', + '@ai-sdk/google', + '@ai-sdk/openai', // Optional knex database drivers — never used at runtime, but knex requires() them 'pg', 'pg-native', diff --git a/apps/studio/vercel.json b/apps/studio/vercel.json index 8e420ba77..20408b0a6 100644 --- a/apps/studio/vercel.json +++ b/apps/studio/vercel.json @@ -13,7 +13,7 @@ "api/**/*.js": { "memory": 1024, "maxDuration": 300, - "includeFiles": "{node_modules/@libsql,node_modules/better-sqlite3}/**" + "includeFiles": "{node_modules/@libsql,node_modules/better-sqlite3,node_modules/@ai-sdk}/**" } }, "headers": [ diff --git a/packages/cli/src/commands/serve.ts b/packages/cli/src/commands/serve.ts index 3be6cc45b..04a632ebd 100644 --- a/packages/cli/src/commands/serve.ts +++ b/packages/cli/src/commands/serve.ts @@ -344,51 +344,12 @@ export default class Serve extends Command { if (!hasAIPlugin) { try { const aiPkg = '@objectstack/service-ai'; - const { AIServicePlugin, VercelLLMAdapter } = await import(/* webpackIgnore: true */ aiPkg); - - // Auto-detect LLM provider from environment variables. - // Priority: 1) Vercel AI Gateway 2) Direct provider SDKs 3) MemoryLLMAdapter (echo) - let adapter: any = undefined; - - // 1. Vercel AI Gateway — works with any provider via gateway('provider/model') - // Uses OIDC on Vercel, VERCEL_API_KEY locally. - const gatewayModel = process.env.AI_GATEWAY_MODEL; // e.g. 'anthropic/claude-sonnet-4.6' - if (gatewayModel) { - try { - const gatewayPkg = '@ai-sdk/gateway'; - const { gateway } = await import(/* webpackIgnore: true */ gatewayPkg); - adapter = new VercelLLMAdapter({ model: gateway(gatewayModel) }); - } catch { - // @ai-sdk/gateway not installed - } - } - - // 2. Direct provider SDKs - if (!adapter) { - const providerConfigs: Array<{ envKey: string; pkg: string; factory: string; defaultModel: string }> = [ - { envKey: 'OPENAI_API_KEY', pkg: '@ai-sdk/openai', factory: 'openai', defaultModel: 'gpt-4o' }, - { envKey: 'ANTHROPIC_API_KEY', pkg: '@ai-sdk/anthropic', factory: 'anthropic', defaultModel: 'claude-sonnet-4-20250514' }, - { envKey: 'GOOGLE_GENERATIVE_AI_API_KEY', pkg: '@ai-sdk/google', factory: 'google', defaultModel: 'gemini-2.0-flash' }, - ]; - - for (const { envKey, pkg, factory, defaultModel } of providerConfigs) { - if (process.env[envKey]) { - try { - const mod = await import(/* webpackIgnore: true */ pkg); - const createModel = mod[factory] ?? mod.default; - if (typeof createModel === 'function') { - const modelId = process.env.AI_MODEL ?? defaultModel; - adapter = new VercelLLMAdapter({ model: createModel(modelId) }); - break; - } - } catch { - // Provider SDK not installed — skip - } - } - } - } + const { AIServicePlugin } = await import(/* webpackIgnore: true */ aiPkg); - await kernel.use(new AIServicePlugin(adapter ? { adapter } : undefined)); + // AIServicePlugin will auto-detect LLM provider from environment variables + // (AI_GATEWAY_MODEL, OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY) + // No need to manually construct the adapter here. + await kernel.use(new AIServicePlugin()); trackPlugin('AIService'); } catch { // @objectstack/service-ai not installed — AI features unavailable diff --git a/packages/metadata/src/loaders/database-loader.ts b/packages/metadata/src/loaders/database-loader.ts index 682ab178f..335831203 100644 --- a/packages/metadata/src/loaders/database-loader.ts +++ b/packages/metadata/src/loaders/database-loader.ts @@ -590,6 +590,27 @@ export class DatabaseLoader implements MetadataLoader { ); } } + + /** + * Delete a metadata item from the database + */ + async delete(type: string, name: string): Promise { + await this.ensureSchema(); + + // Find the existing record to get its ID + const existing = await this.driver.findOne(this.tableName, { + object: this.tableName, + where: this.baseFilter(type, name), + }); + + if (!existing) { + // Item doesn't exist, nothing to delete + return; + } + + // Delete from the main metadata table using the record's ID + await this.driver.delete(this.tableName, existing.id as string); + } } /** diff --git a/packages/metadata/src/loaders/memory-loader.ts b/packages/metadata/src/loaders/memory-loader.ts index a4e39a4c6..6a0a5e801 100644 --- a/packages/metadata/src/loaders/memory-loader.ts +++ b/packages/metadata/src/loaders/memory-loader.ts @@ -91,7 +91,7 @@ export class MemoryLoader implements MetadataLoader { if (!this.storage.has(type)) { this.storage.set(type, new Map()); } - + this.storage.get(type)!.set(name, data); return { @@ -100,4 +100,17 @@ export class MemoryLoader implements MetadataLoader { saveTime: 0, }; } + + /** + * Delete a metadata item from memory storage + */ + async delete(type: string, name: string): Promise { + const typeStore = this.storage.get(type); + if (typeStore) { + typeStore.delete(name); + if (typeStore.size === 0) { + this.storage.delete(type); + } + } + } } diff --git a/packages/metadata/src/metadata-history.test.ts b/packages/metadata/src/metadata-history.test.ts index a9702a8ec..33328f7bd 100644 --- a/packages/metadata/src/metadata-history.test.ts +++ b/packages/metadata/src/metadata-history.test.ts @@ -3,15 +3,15 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { MetadataManager } from './metadata-manager'; import { DatabaseLoader } from './loaders/database-loader'; -import { MemoryDriver } from '@objectstack/driver-memory'; +import { InMemoryDriver } from '@objectstack/driver-memory'; describe('Metadata History', () => { let manager: MetadataManager; - let driver: MemoryDriver; + let driver: InMemoryDriver; beforeEach(async () => { // Create a fresh in-memory driver and database loader - driver = new MemoryDriver({}); + driver = new InMemoryDriver({}); const dbLoader = new DatabaseLoader({ driver, diff --git a/packages/metadata/src/metadata-manager.ts b/packages/metadata/src/metadata-manager.ts index f6904880a..30d1bb6f6 100644 --- a/packages/metadata/src/metadata-manager.ts +++ b/packages/metadata/src/metadata-manager.ts @@ -153,12 +153,20 @@ export class MetadataManager implements IMetadataService { /** * Register/save a metadata item by type + * Stores in-memory registry and persists to writable loaders (if configured) */ async register(type: string, name: string, data: unknown): Promise { if (!this.registry.has(type)) { this.registry.set(type, new Map()); } this.registry.get(type)!.set(name, data); + + // Persist to writable loaders (e.g., DatabaseLoader for history tracking) + for (const loader of this.loaders.values()) { + if (loader.save) { + await loader.save(type, name, data); + } + } } /** @@ -213,6 +221,7 @@ export class MetadataManager implements IMetadataService { * Unregister/remove a metadata item by type and name */ async unregister(type: string, name: string): Promise { + // Remove from in-memory registry const typeStore = this.registry.get(type); if (typeStore) { typeStore.delete(name); @@ -220,6 +229,18 @@ export class MetadataManager implements IMetadataService { this.registry.delete(type); } } + + // Also delete from all loaders that support deletion + for (const loader of this.loaders.values()) { + // Check if the loader has a delete method + if (typeof (loader as any).delete === 'function') { + try { + await (loader as any).delete(type, name); + } catch (error) { + this.logger.warn(`Failed to delete ${type}/${name} from loader ${loader.contract.name}`, { error }); + } + } + } } /** @@ -321,20 +342,21 @@ export class MetadataManager implements IMetadataService { * Unregister all metadata items from a specific package */ async unregisterPackage(packageName: string): Promise { + // Collect all items to delete (type and name pairs) + const itemsToDelete: Array<{ type: string; name: string }> = []; + for (const [type, typeStore] of this.registry) { - const toDelete: string[] = []; for (const [name, data] of typeStore) { const meta = data as any; if (meta?.packageId === packageName || meta?.package === packageName) { - toDelete.push(name); + itemsToDelete.push({ type, name }); } } - for (const name of toDelete) { - typeStore.delete(name); - } - if (typeStore.size === 0) { - this.registry.delete(type); - } + } + + // Delete each item using unregister() to ensure deletion from both registry and loaders + for (const { type, name } of itemsToDelete) { + await this.unregister(type, name); } } @@ -1343,6 +1365,12 @@ export class MetadataManager implements IMetadataService { options?.recordedBy ); + // Update in-memory registry with the restored metadata + if (!this.registry.has(type)) { + this.registry.set(type, new Map()); + } + this.registry.get(type)!.set(name, restoredMetadata); + return restoredMetadata; } diff --git a/packages/metadata/vitest.config.ts b/packages/metadata/vitest.config.ts index 274a11e9b..c0f0b369a 100644 --- a/packages/metadata/vitest.config.ts +++ b/packages/metadata/vitest.config.ts @@ -7,6 +7,8 @@ export default defineConfig({ resolve: { alias: { '@objectstack/core': path.resolve(__dirname, '../core/src/index.ts'), + '@objectstack/driver-memory': path.resolve(__dirname, '../plugins/driver-memory/src/index.ts'), + '@objectstack/spec/api': path.resolve(__dirname, '../spec/src/api/index.ts'), '@objectstack/spec/contracts': path.resolve(__dirname, '../spec/src/contracts/index.ts'), '@objectstack/spec/data': path.resolve(__dirname, '../spec/src/data/index.ts'), '@objectstack/spec/kernel': path.resolve(__dirname, '../spec/src/kernel/index.ts'), diff --git a/packages/runtime/src/app-plugin.test.ts b/packages/runtime/src/app-plugin.test.ts index a0c86f671..54096980d 100644 --- a/packages/runtime/src/app-plugin.test.ts +++ b/packages/runtime/src/app-plugin.test.ts @@ -49,10 +49,15 @@ describe('AppPlugin', () => { objects: [] }; const plugin = new AppPlugin(bundle); - + + // Mock the manifest service + const mockManifestService = { register: vi.fn() }; + vi.mocked(mockContext.getService).mockReturnValue(mockManifestService); + await plugin.init(mockContext); - - expect(mockContext.registerService).toHaveBeenCalledWith('app.com.test.simple', bundle); + + expect(mockContext.getService).toHaveBeenCalledWith('manifest'); + expect(mockManifestService.register).toHaveBeenCalledWith(bundle); }); it('start should do nothing if no runtime hooks', async () => { diff --git a/packages/runtime/vitest.config.ts b/packages/runtime/vitest.config.ts new file mode 100644 index 000000000..750c4faaf --- /dev/null +++ b/packages/runtime/vitest.config.ts @@ -0,0 +1,25 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { defineConfig } from 'vitest/config'; +import path from 'node:path'; + +export default defineConfig({ + resolve: { + alias: { + '@objectstack/core': path.resolve(__dirname, '../core/src/index.ts'), + '@objectstack/rest': path.resolve(__dirname, '../rest/src/index.ts'), + '@objectstack/spec/api': path.resolve(__dirname, '../spec/src/api/index.ts'), + '@objectstack/spec/contracts': path.resolve(__dirname, '../spec/src/contracts/index.ts'), + '@objectstack/spec/data': path.resolve(__dirname, '../spec/src/data/index.ts'), + '@objectstack/spec/kernel': path.resolve(__dirname, '../spec/src/kernel/index.ts'), + '@objectstack/spec/system': path.resolve(__dirname, '../spec/src/system/index.ts'), + '@objectstack/spec': path.resolve(__dirname, '../spec/src/index.ts'), + '@objectstack/types': path.resolve(__dirname, '../types/src/index.ts'), + }, + }, + test: { + globals: true, + environment: 'node', + include: ['src/**/*.test.ts'], + }, +}); diff --git a/packages/services/service-ai/package.json b/packages/services/service-ai/package.json index 7d478f143..962cf5cad 100644 --- a/packages/services/service-ai/package.json +++ b/packages/services/service-ai/package.json @@ -23,6 +23,26 @@ "@objectstack/spec": "workspace:*", "ai": "^6.0.0" }, + "peerDependencies": { + "@ai-sdk/anthropic": "^3.0.0", + "@ai-sdk/gateway": "^3.0.0", + "@ai-sdk/google": "^3.0.0", + "@ai-sdk/openai": "^3.0.0" + }, + "peerDependenciesMeta": { + "@ai-sdk/anthropic": { + "optional": true + }, + "@ai-sdk/gateway": { + "optional": true + }, + "@ai-sdk/google": { + "optional": true + }, + "@ai-sdk/openai": { + "optional": true + } + }, "devDependencies": { "@types/node": "^25.5.0", "typescript": "^6.0.2", diff --git a/packages/services/service-ai/src/__tests__/ai-service.test.ts b/packages/services/service-ai/src/__tests__/ai-service.test.ts index 05e0694b8..4ba88c200 100644 --- a/packages/services/service-ai/src/__tests__/ai-service.test.ts +++ b/packages/services/service-ai/src/__tests__/ai-service.test.ts @@ -837,4 +837,116 @@ describe('AIServicePlugin', () => { expect(ctx.hook).toHaveBeenCalledWith('ai:beforeChat', expect.any(Function)); }); + + // ── LLM Provider Auto-Detection ───────────────────────────────── + + it('should use MemoryLLMAdapter when no env vars are set', async () => { + const plugin = new AIServicePlugin(); + const ctx = createMockContext(); + + // Ensure no LLM provider env vars are set + const oldEnv = { ...process.env }; + delete process.env.AI_GATEWAY_MODEL; + delete process.env.OPENAI_API_KEY; + delete process.env.ANTHROPIC_API_KEY; + delete process.env.GOOGLE_GENERATIVE_AI_API_KEY; + + try { + await plugin.init(ctx); + + const service = ctx.getService('ai'); + expect(service.adapterName).toBe('memory'); + + // Verify warning was logged + expect(silentLogger.warn).toHaveBeenCalledWith( + expect.stringContaining('No LLM provider configured') + ); + } finally { + // Restore environment + process.env = oldEnv; + } + }); + + it('should fallback to MemoryLLMAdapter when provider SDK is not installed', async () => { + const plugin = new AIServicePlugin(); + const ctx = createMockContext(); + + const oldEnv = { ...process.env }; + // Set env var, but the SDK won't be available in test environment + process.env.OPENAI_API_KEY = 'fake-openai-key'; + delete process.env.AI_GATEWAY_MODEL; + delete process.env.ANTHROPIC_API_KEY; + delete process.env.GOOGLE_GENERATIVE_AI_API_KEY; + + try { + await plugin.init(ctx); + + const service = ctx.getService('ai'); + // Should fall back to memory because @ai-sdk/openai is not installed + expect(service.adapterName).toBe('memory'); + + // Verify warning was logged about SDK load failure + expect(silentLogger.warn).toHaveBeenCalledWith( + expect.stringContaining('Failed to load @ai-sdk/openai'), + expect.objectContaining({ error: expect.any(String) }) + ); + + // Verify warning was logged about final fallback + expect(silentLogger.warn).toHaveBeenCalledWith( + expect.stringContaining('No LLM provider configured') + ); + } finally { + process.env = oldEnv; + } + }); + + it('should prefer explicit adapter over auto-detection', async () => { + const customAdapter: LLMAdapter = { + name: 'custom-explicit', + chat: async () => ({ content: 'explicit' }), + complete: async () => ({ content: '' }), + }; + + const plugin = new AIServicePlugin({ adapter: customAdapter }); + const ctx = createMockContext(); + + const oldEnv = { ...process.env }; + process.env.OPENAI_API_KEY = 'fake-openai-key'; + + try { + await plugin.init(ctx); + + const service = ctx.getService('ai'); + expect(service.adapterName).toBe('custom-explicit'); + + // Verify it logged as explicitly configured + expect(silentLogger.info).toHaveBeenCalledWith( + expect.stringContaining('explicitly configured') + ); + } finally { + process.env = oldEnv; + } + }); + + it('should log adapter selection', async () => { + const plugin = new AIServicePlugin(); + const ctx = createMockContext(); + + const oldEnv = { ...process.env }; + delete process.env.AI_GATEWAY_MODEL; + delete process.env.OPENAI_API_KEY; + delete process.env.ANTHROPIC_API_KEY; + delete process.env.GOOGLE_GENERATIVE_AI_API_KEY; + + try { + await plugin.init(ctx); + + // Verify adapter selection was logged + expect(silentLogger.info).toHaveBeenCalledWith( + expect.stringContaining('Using LLM adapter') + ); + } finally { + process.env = oldEnv; + } + }); }); diff --git a/packages/services/service-ai/src/plugin.ts b/packages/services/service-ai/src/plugin.ts index 029dffe2b..24e9bc4e6 100644 --- a/packages/services/service-ai/src/plugin.ts +++ b/packages/services/service-ai/src/plugin.ts @@ -12,6 +12,8 @@ import { registerDataTools } from './tools/data-tools.js'; import { registerMetadataTools } from './tools/metadata-tools.js'; import { AgentRuntime } from './agent-runtime.js'; import { DATA_CHAT_AGENT, METADATA_ASSISTANT_AGENT } from './agents/index.js'; +import { VercelLLMAdapter } from './adapters/vercel-adapter.js'; +import { MemoryLLMAdapter } from './adapters/memory-adapter.js'; /** * Configuration options for the AIServicePlugin. @@ -61,6 +63,90 @@ export class AIServicePlugin implements Plugin { this.options = options; } + /** + * Auto-detect LLM provider from environment variables. + * + * Priority order: + * 1. AI_GATEWAY_MODEL → Vercel AI Gateway + * 2. OPENAI_API_KEY → OpenAI + * 3. ANTHROPIC_API_KEY → Anthropic + * 4. GOOGLE_GENERATIVE_AI_API_KEY → Google + * 5. Fallback → MemoryLLMAdapter + * + * Returns the adapter and a description for logging. + */ + private async detectAdapter(ctx: PluginContext): Promise<{ adapter: LLMAdapter; description: string }> { + // 1. Vercel AI Gateway — works with any provider via gateway('provider/model') + const gatewayModel = process.env.AI_GATEWAY_MODEL; + if (gatewayModel) { + try { + const gatewayPkg = '@ai-sdk/gateway'; + const { gateway } = await import(/* webpackIgnore: true */ gatewayPkg); + const adapter = new VercelLLMAdapter({ model: gateway(gatewayModel) }); + return { adapter, description: `Vercel AI Gateway (model: ${gatewayModel})` }; + } catch (err) { + ctx.logger.warn( + `[AI] Failed to load @ai-sdk/gateway for AI_GATEWAY_MODEL=${gatewayModel}, trying next provider`, + err instanceof Error ? { error: err.message } : undefined + ); + } + } + + // 2. Direct provider SDKs + const providerConfigs: Array<{ + envKey: string; + pkg: string; + factory: string; + defaultModel: string; + displayName: string; + }> = [ + { + envKey: 'OPENAI_API_KEY', + pkg: '@ai-sdk/openai', + factory: 'openai', + defaultModel: 'gpt-4o', + displayName: 'OpenAI' + }, + { + envKey: 'ANTHROPIC_API_KEY', + pkg: '@ai-sdk/anthropic', + factory: 'anthropic', + defaultModel: 'claude-sonnet-4-20250514', + displayName: 'Anthropic' + }, + { + envKey: 'GOOGLE_GENERATIVE_AI_API_KEY', + pkg: '@ai-sdk/google', + factory: 'google', + defaultModel: 'gemini-2.0-flash', + displayName: 'Google' + }, + ]; + + for (const { envKey, pkg, factory, defaultModel, displayName } of providerConfigs) { + if (process.env[envKey]) { + try { + const mod = await import(/* webpackIgnore: true */ pkg); + const createModel = mod[factory] ?? mod.default; + if (typeof createModel === 'function') { + const modelId = process.env.AI_MODEL ?? defaultModel; + const adapter = new VercelLLMAdapter({ model: createModel(modelId) }); + return { adapter, description: `${displayName} (model: ${modelId})` }; + } + } catch (err) { + ctx.logger.warn( + `[AI] Failed to load ${pkg} for ${envKey}, trying next provider`, + err instanceof Error ? { error: err.message } : undefined + ); + } + } + } + + // 3. Fallback to MemoryLLMAdapter + ctx.logger.warn('[AI] No LLM provider configured via environment variables. Falling back to MemoryLLMAdapter (echo mode). Set AI_GATEWAY_MODEL, OPENAI_API_KEY, ANTHROPIC_API_KEY, or GOOGLE_GENERATIVE_AI_API_KEY to use a real LLM.'); + return { adapter: new MemoryLLMAdapter(), description: 'MemoryLLMAdapter (echo mode - for testing only)' }; + } + async init(ctx: PluginContext): Promise { // Check if there is an existing AI service (e.g. from dev-plugin) let hasExisting = false; @@ -88,8 +174,26 @@ export class AIServicePlugin implements Plugin { } } + // Determine LLM adapter: explicit > auto-detect from env > MemoryLLMAdapter fallback + let adapter: LLMAdapter; + let adapterDescription: string; + + if (this.options.adapter) { + // User provided an explicit adapter + adapter = this.options.adapter; + adapterDescription = `${adapter.name} (explicitly configured)`; + } else { + // Auto-detect from environment variables + const detected = await this.detectAdapter(ctx); + adapter = detected.adapter; + adapterDescription = detected.description; + } + + // Log the selected adapter + ctx.logger.info(`[AI] Using LLM adapter: ${adapterDescription}`); + const config: AIServiceConfig = { - adapter: this.options.adapter, + adapter, logger: ctx.logger, conversationService, }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 60bb3d185..55eef5429 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -127,6 +127,18 @@ importers: apps/studio: dependencies: + '@ai-sdk/anthropic': + specifier: ^3.0.0 + version: 3.0.67(zod@4.3.6) + '@ai-sdk/gateway': + specifier: ^3.0.0 + version: 3.0.84(zod@4.3.6) + '@ai-sdk/google': + specifier: ^3.0.0 + version: 3.0.59(zod@4.3.6) + '@ai-sdk/openai': + specifier: ^3.0.0 + version: 3.0.51(zod@4.3.6) '@ai-sdk/react': specifier: ^3.0.144 version: 3.0.144(react@19.2.4)(zod@4.3.6) @@ -1063,6 +1075,18 @@ importers: packages/services/service-ai: dependencies: + '@ai-sdk/anthropic': + specifier: ^3.0.0 + version: 3.0.67(zod@4.3.6) + '@ai-sdk/gateway': + specifier: ^3.0.0 + version: 3.0.84(zod@4.3.6) + '@ai-sdk/google': + specifier: ^3.0.0 + version: 3.0.59(zod@4.3.6) + '@ai-sdk/openai': + specifier: ^3.0.0 + version: 3.0.51(zod@4.3.6) '@ai-sdk/provider': specifier: ^2.0.0 version: 2.0.1 @@ -1306,18 +1330,42 @@ importers: packages: + '@ai-sdk/anthropic@3.0.67': + resolution: {integrity: sha512-FFX4P5Fd6lcQJc2OLngZQkbbJHa0IDDZi087Edb8qRZx6h90krtM61ArbMUL8us/7ZUwojCXnyJ/wQ2Eflx2jQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/gateway@3.0.84': resolution: {integrity: sha512-RnUw6UNvkaw9MEaJU9cIjA+WBP+ZR5+M/9nfbfJHcGKtTbcWXijJuYKx9nYRnm+qU+iiakb0XvQA/vvho6lTsw==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/google@3.0.59': + resolution: {integrity: sha512-N5pyd6xSIIguG45kM/hvWdTrmudOY/iZ07DZu12K5q/NSeapQQFOYg+3DRKONzS9+FESLugjjzFrzfA24sQ6lw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/openai@3.0.51': + resolution: {integrity: sha512-qBgDOC+vlXwLFbZ3UoKx3T8VFyul3K39JNyW6E4XnOnzLT4Mlhb0GeDC06RvYqwGWOQFBQNLe/vegOMVtNpl5g==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/provider-utils@4.0.21': resolution: {integrity: sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/provider-utils@4.0.23': + resolution: {integrity: sha512-z8GlDaCmRSDlqkMF2f4/RFgWxdarvIbyuk+m6WXT1LYgsnGiXRJGTD2Z1+SDl3LqtFuRtGX1aghYvQLoHL/9pg==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/provider@2.0.1': resolution: {integrity: sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng==} engines: {node: '>=18'} @@ -7320,6 +7368,12 @@ packages: snapshots: + '@ai-sdk/anthropic@3.0.67(zod@4.3.6)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.23(zod@4.3.6) + zod: 4.3.6 + '@ai-sdk/gateway@3.0.84(zod@4.3.6)': dependencies: '@ai-sdk/provider': 3.0.8 @@ -7327,6 +7381,18 @@ snapshots: '@vercel/oidc': 3.1.0 zod: 4.3.6 + '@ai-sdk/google@3.0.59(zod@4.3.6)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.23(zod@4.3.6) + zod: 4.3.6 + + '@ai-sdk/openai@3.0.51(zod@4.3.6)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.23(zod@4.3.6) + zod: 4.3.6 + '@ai-sdk/provider-utils@4.0.21(zod@4.3.6)': dependencies: '@ai-sdk/provider': 3.0.8 @@ -7334,6 +7400,13 @@ snapshots: eventsource-parser: 3.0.6 zod: 4.3.6 + '@ai-sdk/provider-utils@4.0.23(zod@4.3.6)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.0.6 + zod: 4.3.6 + '@ai-sdk/provider@2.0.1': dependencies: json-schema: 0.4.0