From 435b2e62214b5d79ea45293b21a5cbf3b58a62fd Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Tue, 7 Apr 2026 01:46:49 +0000 Subject: [PATCH 01/12] Initial plan From a2f684b6186defbf2e3f4d13165857ea51a17720 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Tue, 7 Apr 2026 01:55:29 +0000 Subject: [PATCH 02/12] Add LLM provider auto-detection to AIServicePlugin - Add detectAdapter() private method to auto-detect LLM providers from env vars - Support AI_GATEWAY_MODEL, OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY - Add comprehensive logging of selected adapter and warnings for missing SDKs - Handle dynamic import failures as soft errors with automatic fallback - Remove redundant detection logic from CLI serve.ts - Add unit tests for auto-detection behavior - Update CHANGELOG.md with new feature details Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/7436aca6-b645-4fd8-9459-bef40b5867ec Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- CHANGELOG.md | 11 ++ packages/cli/src/commands/serve.ts | 49 +------- .../src/__tests__/ai-service.test.ts | 112 ++++++++++++++++++ packages/services/service-ai/src/plugin.ts | 106 ++++++++++++++++- 4 files changed, 233 insertions(+), 45 deletions(-) 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/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/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, }; From 6b1af48a4ff639336dd7b3a4fd08f1142acde3cb Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Tue, 7 Apr 2026 02:20:07 +0000 Subject: [PATCH 03/12] Fix metadata package test failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix vitest config to add missing @objectstack/spec/api alias The spec package exports subpaths like /api, /data, etc. but vitest wasn't resolving them correctly, causing ENOTDIR errors when trying to load index.ts/api - Fix vitest config to add @objectstack/driver-memory alias Tests need access to driver-memory package source files - Fix MemoryDriver import in metadata-history.test.ts The class was renamed from MemoryDriver to InMemoryDriver but the test wasn't updated These changes fix the CI test failures in @objectstack/metadata#test. Note: Some test assertions are still failing because the metadata history feature is not fully implemented, but those are pre-existing issues unrelated to this PR. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/metadata/src/metadata-history.test.ts | 6 +++--- packages/metadata/vitest.config.ts | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) 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/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'), From 3c82b74a3796b5ca4ce6c67e02f2e7a9dc0e7e10 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Tue, 7 Apr 2026 02:31:21 +0000 Subject: [PATCH 04/12] Add vitest config to runtime package to fix CI test failures Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/e8aaabe5-7763-43f9-b644-5480a42df8a8 Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/runtime/vitest.config.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 packages/runtime/vitest.config.ts 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'], + }, +}); From 0d7d18f24acf3cb2b1ea4c6d71a9cfa59e75ada2 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Tue, 7 Apr 2026 02:41:29 +0000 Subject: [PATCH 05/12] Fix metadata history tracking by persisting registered metadata to loaders The register() method was only storing metadata in the in-memory registry but never persisting it to writable loaders like DatabaseLoader. This caused history tracking tests to fail because no history records were being created. Changes: - Updated MetadataManager.register() to call save() on all loaders that implement the save() method - This ensures metadata is persisted to DatabaseLoader which triggers history record creation via createHistoryRecord() - Wrapped save() calls in try-catch to prevent registration failures if persistence fails Fixes #metadata-history-tests Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/metadata/src/metadata-manager.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/metadata/src/metadata-manager.ts b/packages/metadata/src/metadata-manager.ts index f6904880a..b86946785 100644 --- a/packages/metadata/src/metadata-manager.ts +++ b/packages/metadata/src/metadata-manager.ts @@ -153,12 +153,25 @@ 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) { + if (loader.save) { + try { + await loader.save(type, name, data); + } catch (error) { + // Log but don't fail registration if persistence fails + console.error(`Failed to persist metadata ${type}/${name} to loader:`, error); + } + } + } } /** From f83eaede07d280f0f4cbb15e87abf9308272883b Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Tue, 7 Apr 2026 02:45:38 +0000 Subject: [PATCH 06/12] Fix TypeScript error: iterate over loader Map values, not entries The previous fix had a type error because it was iterating over the Map directly (which yields entries as tuples) instead of using .values() to iterate over just the MetadataLoader instances. packages/metadata/src/metadata-manager.ts:166 Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/metadata/src/metadata-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/metadata/src/metadata-manager.ts b/packages/metadata/src/metadata-manager.ts index b86946785..c1959ab32 100644 --- a/packages/metadata/src/metadata-manager.ts +++ b/packages/metadata/src/metadata-manager.ts @@ -162,7 +162,7 @@ export class MetadataManager implements IMetadataService { this.registry.get(type)!.set(name, data); // Persist to writable loaders (e.g., DatabaseLoader for history tracking) - for (const loader of this.loaders) { + for (const loader of this.loaders.values()) { if (loader.save) { try { await loader.save(type, name, data); From b7d64ae7eb40a7e3828ae627f88507dc0168d686 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Tue, 7 Apr 2026 02:54:23 +0000 Subject: [PATCH 07/12] Remove error swallowing in metadata register to surface real failures The try-catch block was hiding errors during metadata persistence which prevented proper diagnosis of test failures. Now errors will propagate properly to help identify the root cause of metadata history test failures. packages/metadata/src/metadata-manager.ts:165-169 Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/metadata/src/metadata-manager.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/metadata/src/metadata-manager.ts b/packages/metadata/src/metadata-manager.ts index c1959ab32..78633d5c6 100644 --- a/packages/metadata/src/metadata-manager.ts +++ b/packages/metadata/src/metadata-manager.ts @@ -164,12 +164,7 @@ export class MetadataManager implements IMetadataService { // Persist to writable loaders (e.g., DatabaseLoader for history tracking) for (const loader of this.loaders.values()) { if (loader.save) { - try { - await loader.save(type, name, data); - } catch (error) { - // Log but don't fail registration if persistence fails - console.error(`Failed to persist metadata ${type}/${name} to loader:`, error); - } + await loader.save(type, name, data); } } } From cf50b78cd30f2f3d641a15a7f2f7994ab94ceef0 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Tue, 7 Apr 2026 03:00:47 +0000 Subject: [PATCH 08/12] Fix app-plugin test to match new manifest service registration The AppPlugin.init() method now uses the manifest service's register() method instead of ctx.registerService(). Updated the test to properly mock the manifest service and verify the correct behavior. packages/runtime/src/app-plugin.test.ts:46-61 Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/runtime/src/app-plugin.test.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) 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 () => { From f482e305db2f47419e852c40642bb0566ff0c82c Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Tue, 7 Apr 2026 03:08:07 +0000 Subject: [PATCH 09/12] Fix metadata unregister and rollback functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed multiple issues with metadata management: 1. rollback(): Now updates in-memory registry after restoring from history 2. unregister(): Now deletes from both in-memory registry AND all loaders 3. unregisterPackage(): Now uses unregister() to ensure proper cleanup 4. Added delete() methods to MemoryLoader and DatabaseLoader These changes ensure that unregister operations properly remove metadata from all storage locations, and rollback operations update the in-memory state correctly. Fixes: - packages/metadata/src/metadata-manager.ts:1354-1358 (rollback registry update) - packages/metadata/src/metadata-manager.ts:223-244 (unregister loader cleanup) - packages/metadata/src/metadata-manager.ts:344-361 (unregisterPackage refactor) - packages/metadata/src/loaders/memory-loader.ts:104-115 (delete method) - packages/metadata/src/loaders/database-loader.ts:594-616 (delete method) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- .../metadata/src/loaders/database-loader.ts | 24 +++++++++++++ .../metadata/src/loaders/memory-loader.ts | 15 +++++++- packages/metadata/src/metadata-manager.ts | 36 ++++++++++++++----- 3 files changed, 66 insertions(+), 9 deletions(-) diff --git a/packages/metadata/src/loaders/database-loader.ts b/packages/metadata/src/loaders/database-loader.ts index 682ab178f..dc6ca98f6 100644 --- a/packages/metadata/src/loaders/database-loader.ts +++ b/packages/metadata/src/loaders/database-loader.ts @@ -590,6 +590,30 @@ 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 + await this.driver.delete(this.tableName, { + object: this.tableName, + where: this.baseFilter(type, name), + }); + } } /** 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-manager.ts b/packages/metadata/src/metadata-manager.ts index 78633d5c6..30d1bb6f6 100644 --- a/packages/metadata/src/metadata-manager.ts +++ b/packages/metadata/src/metadata-manager.ts @@ -221,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); @@ -228,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 }); + } + } + } } /** @@ -329,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); } } @@ -1351,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; } From 200ef60d81a83b41f9e11714af1b784c7561a79d Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Tue, 7 Apr 2026 03:14:27 +0000 Subject: [PATCH 10/12] Fix AI SDK package resolution in Vercel deployment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves the "Cannot find package '@ai-sdk/gateway'" error by: 1. Added @ai-sdk/* packages as optional peer dependencies in service-ai - These are only needed when specific env vars are set - Marked as optional to avoid installation errors when not needed 2. Added @ai-sdk/* packages as dependencies in studio app - Required for LLM auto-detection feature in production 3. Updated esbuild external list to exclude @ai-sdk packages - Prevents bundling so dynamic imports work correctly - Allows runtime resolution from node_modules 4. Updated build-vercel.sh to copy @ai-sdk packages - Copies packages from monorepo to local node_modules - Required for Vercel's pnpm workspace resolution 5. Updated vercel.json includeFiles to bundle @ai-sdk packages - Ensures packages are included in serverless function This allows the AI service to gracefully detect and load LLM providers based on environment variables without build-time bundling issues. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- apps/studio/package.json | 4 ++++ apps/studio/scripts/build-vercel.sh | 11 +++++++++++ apps/studio/scripts/bundle-api.mjs | 5 +++++ apps/studio/vercel.json | 2 +- packages/services/service-ai/package.json | 20 ++++++++++++++++++++ 5 files changed, 41 insertions(+), 1 deletion(-) diff --git a/apps/studio/package.json b/apps/studio/package.json index b84c871ba..09d00440a 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -17,6 +17,10 @@ "preview": "vite preview" }, "dependencies": { + "@ai-sdk/anthropic": "^3.0.144", + "@ai-sdk/gateway": "^3.0.144", + "@ai-sdk/google": "^3.0.144", + "@ai-sdk/openai": "^3.0.144", "@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/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", From 3a87d67fb7ad20c8c3d429d9f1e09adddb875dfc Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Tue, 7 Apr 2026 03:17:18 +0000 Subject: [PATCH 11/12] Fix database-loader delete() to use correct driver signature The driver.delete() method expects (object, id, options) but was being called with {object, where} which caused TypeScript error TS2345. Now correctly passes existing.id as the second parameter. Fixes: error TS2345: Argument of type '{ object: string; where: Record...' Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/c97d6820-b99b-498b-a344-69f22d7d3faf Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/metadata/src/loaders/database-loader.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/metadata/src/loaders/database-loader.ts b/packages/metadata/src/loaders/database-loader.ts index dc6ca98f6..335831203 100644 --- a/packages/metadata/src/loaders/database-loader.ts +++ b/packages/metadata/src/loaders/database-loader.ts @@ -608,11 +608,8 @@ export class DatabaseLoader implements MetadataLoader { return; } - // Delete from the main metadata table - await this.driver.delete(this.tableName, { - object: this.tableName, - where: this.baseFilter(type, name), - }); + // Delete from the main metadata table using the record's ID + await this.driver.delete(this.tableName, existing.id as string); } } From 872ab54078263e7fc8aed3798e053ad76143f6bf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 03:39:27 +0000 Subject: [PATCH 12/12] Fix pnpm-lock.yaml: correct @ai-sdk/* version constraints and regenerate lockfile The studio package.json had @ai-sdk/* deps pinned to ^3.0.144 which doesn't exist for most packages (latest: anthropic@3.0.67, openai@3.0.51, google@3.0.59, gateway@3.0.91). Changed to ^3.0.0 and regenerated pnpm-lock.yaml. Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/01fb2ce7-c0b8-443b-8012-42553f2fc25a Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- apps/studio/package.json | 8 ++--- pnpm-lock.yaml | 73 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/apps/studio/package.json b/apps/studio/package.json index 09d00440a..75d64523d 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -17,10 +17,10 @@ "preview": "vite preview" }, "dependencies": { - "@ai-sdk/anthropic": "^3.0.144", - "@ai-sdk/gateway": "^3.0.144", - "@ai-sdk/google": "^3.0.144", - "@ai-sdk/openai": "^3.0.144", + "@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/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