@open-mercato/ai-assistant 0.4.11-develop.2226.2963a4b52d → 0.4.11-develop.2229.54cd746c6e

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/README.md +69 -9
  2. package/dist/modules/ai_assistant/api/route/route.js +32 -44
  3. package/dist/modules/ai_assistant/api/route/route.js.map +2 -2
  4. package/dist/modules/ai_assistant/lib/ai-sdk.js +1 -0
  5. package/dist/modules/ai_assistant/lib/ai-sdk.js.map +2 -2
  6. package/dist/modules/ai_assistant/lib/chat-config.js +52 -40
  7. package/dist/modules/ai_assistant/lib/chat-config.js.map +2 -2
  8. package/dist/modules/ai_assistant/lib/llm-adapters/anthropic.js +65 -0
  9. package/dist/modules/ai_assistant/lib/llm-adapters/anthropic.js.map +7 -0
  10. package/dist/modules/ai_assistant/lib/llm-adapters/google.js +59 -0
  11. package/dist/modules/ai_assistant/lib/llm-adapters/google.js.map +7 -0
  12. package/dist/modules/ai_assistant/lib/llm-adapters/openai.js +65 -0
  13. package/dist/modules/ai_assistant/lib/llm-adapters/openai.js.map +7 -0
  14. package/dist/modules/ai_assistant/lib/llm-bootstrap.js +47 -0
  15. package/dist/modules/ai_assistant/lib/llm-bootstrap.js.map +7 -0
  16. package/dist/modules/ai_assistant/lib/openai-compatible-presets.js +203 -0
  17. package/dist/modules/ai_assistant/lib/openai-compatible-presets.js.map +7 -0
  18. package/jest.config.cjs +1 -0
  19. package/package.json +4 -4
  20. package/src/modules/ai_assistant/api/route/route.ts +49 -46
  21. package/src/modules/ai_assistant/lib/__tests__/llm-adapters-anthropic.test.ts +72 -0
  22. package/src/modules/ai_assistant/lib/__tests__/llm-adapters-google.test.ts +71 -0
  23. package/src/modules/ai_assistant/lib/__tests__/llm-adapters-openai.test.ts +160 -0
  24. package/src/modules/ai_assistant/lib/__tests__/llm-bootstrap.test.ts +81 -0
  25. package/src/modules/ai_assistant/lib/ai-sdk.ts +10 -0
  26. package/src/modules/ai_assistant/lib/chat-config.ts +75 -42
  27. package/src/modules/ai_assistant/lib/llm-adapters/anthropic.ts +91 -0
  28. package/src/modules/ai_assistant/lib/llm-adapters/google.ts +80 -0
  29. package/src/modules/ai_assistant/lib/llm-adapters/openai.ts +145 -0
  30. package/src/modules/ai_assistant/lib/llm-bootstrap.ts +80 -0
  31. package/src/modules/ai_assistant/lib/openai-compatible-presets.ts +262 -0
@@ -0,0 +1,71 @@
1
+ import { createGoogleAdapter } from '../llm-adapters/google'
2
+
3
+ describe('GoogleAdapter', () => {
4
+ const adapter = createGoogleAdapter()
5
+
6
+ it('has expected id, name, envKeys and defaultModel', () => {
7
+ expect(adapter.id).toBe('google')
8
+ expect(adapter.name).toBe('Google')
9
+ expect(adapter.envKeys).toEqual(['GOOGLE_GENERATIVE_AI_API_KEY', 'OPENCODE_GOOGLE_API_KEY'])
10
+ expect(adapter.defaultModel).toBe('gemini-3-flash')
11
+ expect(adapter.defaultModels.length).toBeGreaterThan(0)
12
+ })
13
+
14
+ it('detects configuration from env', () => {
15
+ expect(
16
+ adapter.isConfigured({ GOOGLE_GENERATIVE_AI_API_KEY: 'AIza-key' }),
17
+ ).toBe(true)
18
+ expect(adapter.isConfigured({})).toBe(false)
19
+ expect(
20
+ adapter.isConfigured({ GOOGLE_GENERATIVE_AI_API_KEY: ' ' }),
21
+ ).toBe(false)
22
+ })
23
+
24
+ it('detects configuration from OPENCODE_* fallback env', () => {
25
+ expect(
26
+ adapter.isConfigured({ OPENCODE_GOOGLE_API_KEY: 'AIza-key' }),
27
+ ).toBe(true)
28
+ expect(
29
+ adapter.isConfigured({ OPENCODE_GOOGLE_API_KEY: '' }),
30
+ ).toBe(false)
31
+ })
32
+
33
+ it('resolves API key from env', () => {
34
+ expect(
35
+ adapter.resolveApiKey({ GOOGLE_GENERATIVE_AI_API_KEY: 'AIza-key' }),
36
+ ).toBe('AIza-key')
37
+ expect(adapter.resolveApiKey({})).toBeNull()
38
+ })
39
+
40
+ it('resolves API key from OPENCODE_* fallback env', () => {
41
+ expect(
42
+ adapter.resolveApiKey({ OPENCODE_GOOGLE_API_KEY: 'opencode-key' }),
43
+ ).toBe('opencode-key')
44
+ expect(
45
+ adapter.resolveApiKey({
46
+ GOOGLE_GENERATIVE_AI_API_KEY: 'primary',
47
+ OPENCODE_GOOGLE_API_KEY: 'fallback',
48
+ }),
49
+ ).toBe('primary')
50
+ })
51
+
52
+ it('returns the configured env key name for diagnostics', () => {
53
+ expect(
54
+ adapter.getConfiguredEnvKey({
55
+ GOOGLE_GENERATIVE_AI_API_KEY: 'AIza',
56
+ }),
57
+ ).toBe('GOOGLE_GENERATIVE_AI_API_KEY')
58
+ expect(adapter.getConfiguredEnvKey({})).toBe(
59
+ 'GOOGLE_GENERATIVE_AI_API_KEY',
60
+ )
61
+ })
62
+
63
+ it('createModel returns a non-null AI SDK model instance', () => {
64
+ const model = adapter.createModel({
65
+ apiKey: 'AIza-test',
66
+ modelId: 'gemini-3-flash',
67
+ })
68
+ expect(model).toBeDefined()
69
+ expect(model).not.toBeNull()
70
+ })
71
+ })
@@ -0,0 +1,160 @@
1
+ import {
2
+ createOpenAICompatibleProvider,
3
+ type OpenAICompatiblePreset,
4
+ } from '../llm-adapters/openai'
5
+ import { OPENAI_COMPATIBLE_PRESETS } from '../openai-compatible-presets'
6
+
7
+ const DEEPINFRA_PRESET: OpenAICompatiblePreset = {
8
+ id: 'deepinfra',
9
+ name: 'DeepInfra',
10
+ baseURL: 'https://api.deepinfra.com/v1/openai',
11
+ envKeys: ['DEEPINFRA_API_KEY'],
12
+ defaultModel: 'zai-org/GLM-5.1',
13
+ defaultModels: [
14
+ {
15
+ id: 'zai-org/GLM-5.1',
16
+ name: 'GLM-5.1',
17
+ contextWindow: 202752,
18
+ tags: ['flagship'],
19
+ },
20
+ ],
21
+ }
22
+
23
+ describe('OpenAIAdapter (OpenAI-compatible provider factory)', () => {
24
+ it('validates preset shape at construction time', () => {
25
+ expect(() =>
26
+ createOpenAICompatibleProvider({
27
+ ...DEEPINFRA_PRESET,
28
+ id: '',
29
+ }),
30
+ ).toThrow('must have a non-empty id')
31
+
32
+ expect(() =>
33
+ createOpenAICompatibleProvider({
34
+ ...DEEPINFRA_PRESET,
35
+ envKeys: [],
36
+ }),
37
+ ).toThrow('must declare at least one env key')
38
+ })
39
+
40
+ it('exposes preset metadata through the LlmProvider port', () => {
41
+ const provider = createOpenAICompatibleProvider(DEEPINFRA_PRESET)
42
+ expect(provider.id).toBe('deepinfra')
43
+ expect(provider.name).toBe('DeepInfra')
44
+ expect(provider.envKeys).toEqual(['DEEPINFRA_API_KEY'])
45
+ expect(provider.defaultModel).toBe('zai-org/GLM-5.1')
46
+ expect(provider.defaultModels[0].id).toBe('zai-org/GLM-5.1')
47
+ })
48
+
49
+ it('detects configuration via the preset-specific env key', () => {
50
+ const provider = createOpenAICompatibleProvider(DEEPINFRA_PRESET)
51
+ expect(provider.isConfigured({ DEEPINFRA_API_KEY: 'key' })).toBe(true)
52
+ // Unrelated env key does NOT configure the preset.
53
+ expect(provider.isConfigured({ OPENAI_API_KEY: 'unrelated' })).toBe(
54
+ false,
55
+ )
56
+ expect(provider.isConfigured({})).toBe(false)
57
+ })
58
+
59
+ it('resolves API key and env key name for diagnostics', () => {
60
+ const provider = createOpenAICompatibleProvider(DEEPINFRA_PRESET)
61
+ expect(
62
+ provider.resolveApiKey({ DEEPINFRA_API_KEY: 'secret' }),
63
+ ).toBe('secret')
64
+ expect(
65
+ provider.getConfiguredEnvKey({ DEEPINFRA_API_KEY: 'secret' }),
66
+ ).toBe('DEEPINFRA_API_KEY')
67
+ expect(provider.getConfiguredEnvKey({})).toBe('DEEPINFRA_API_KEY')
68
+ })
69
+
70
+ it('createModel returns a non-null AI SDK model instance', () => {
71
+ const provider = createOpenAICompatibleProvider(DEEPINFRA_PRESET)
72
+ const model = provider.createModel({
73
+ apiKey: 'test-key',
74
+ modelId: 'zai-org/GLM-5.1',
75
+ })
76
+ expect(model).toBeDefined()
77
+ expect(model).not.toBeNull()
78
+ })
79
+
80
+ it('createModel honors per-call baseURL override', () => {
81
+ const provider = createOpenAICompatibleProvider(DEEPINFRA_PRESET)
82
+ // AI SDK does not crash when baseURL is overridden per-call.
83
+ const model = provider.createModel({
84
+ apiKey: 'test-key',
85
+ modelId: 'zai-org/GLM-5.1',
86
+ baseURL: 'https://example.com/v1',
87
+ })
88
+ expect(model).toBeDefined()
89
+ })
90
+
91
+ it('azure preset honors baseURLEnvKeys override', () => {
92
+ const azurePreset = OPENAI_COMPATIBLE_PRESETS.find(
93
+ (p) => p.id === 'azure',
94
+ )
95
+ expect(azurePreset).toBeDefined()
96
+ expect(azurePreset?.baseURLEnvKeys).toContain('AZURE_OPENAI_BASE_URL')
97
+ // Sanity: the adapter can be created from the preset.
98
+ const provider = createOpenAICompatibleProvider(azurePreset!)
99
+ expect(provider.id).toBe('azure')
100
+ })
101
+ })
102
+
103
+ describe('OpenAI preset OPENCODE_* fallback env keys', () => {
104
+ it('openai preset includes OPENCODE_OPENAI_API_KEY fallback', () => {
105
+ const openaiPreset = OPENAI_COMPATIBLE_PRESETS.find((p) => p.id === 'openai')
106
+ expect(openaiPreset).toBeDefined()
107
+ expect(openaiPreset!.envKeys).toContain('OPENAI_API_KEY')
108
+ expect(openaiPreset!.envKeys).toContain('OPENCODE_OPENAI_API_KEY')
109
+ })
110
+
111
+ it('resolves API key from OPENCODE_OPENAI_API_KEY fallback', () => {
112
+ const openaiPreset = OPENAI_COMPATIBLE_PRESETS.find((p) => p.id === 'openai')
113
+ const provider = createOpenAICompatibleProvider(openaiPreset!)
114
+ expect(
115
+ provider.resolveApiKey({ OPENCODE_OPENAI_API_KEY: 'opencode-key' }),
116
+ ).toBe('opencode-key')
117
+ expect(
118
+ provider.resolveApiKey({
119
+ OPENAI_API_KEY: 'primary',
120
+ OPENCODE_OPENAI_API_KEY: 'fallback',
121
+ }),
122
+ ).toBe('primary')
123
+ })
124
+ })
125
+
126
+ describe('OPENAI_COMPATIBLE_PRESETS built-in catalog', () => {
127
+ it('ships at least 8 built-in presets including openai and deepinfra', () => {
128
+ expect(OPENAI_COMPATIBLE_PRESETS.length).toBeGreaterThanOrEqual(8)
129
+ const ids = OPENAI_COMPATIBLE_PRESETS.map((p) => p.id)
130
+ expect(ids).toContain('openai')
131
+ expect(ids).toContain('deepinfra')
132
+ expect(ids).toContain('groq')
133
+ expect(ids).toContain('together')
134
+ expect(ids).toContain('fireworks')
135
+ expect(ids).toContain('azure')
136
+ expect(ids).toContain('litellm')
137
+ expect(ids).toContain('ollama')
138
+ })
139
+
140
+ it('every preset has at least one model and one env key', () => {
141
+ for (const preset of OPENAI_COMPATIBLE_PRESETS) {
142
+ expect(preset.envKeys.length).toBeGreaterThan(0)
143
+ expect(preset.defaultModels.length).toBeGreaterThan(0)
144
+ expect(preset.defaultModel.length).toBeGreaterThan(0)
145
+ }
146
+ })
147
+
148
+ it('every preset defaultModel exists in its defaultModels array', () => {
149
+ for (const preset of OPENAI_COMPATIBLE_PRESETS) {
150
+ const ids = preset.defaultModels.map((m) => m.id)
151
+ expect(ids).toContain(preset.defaultModel)
152
+ }
153
+ })
154
+
155
+ it('every preset id is unique', () => {
156
+ const ids = OPENAI_COMPATIBLE_PRESETS.map((p) => p.id)
157
+ const unique = new Set(ids)
158
+ expect(unique.size).toBe(ids.length)
159
+ })
160
+ })
@@ -0,0 +1,81 @@
1
+ import { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'
2
+ import {
3
+ registerBuiltInLlmProviders,
4
+ resetLlmBootstrapState,
5
+ } from '../llm-bootstrap'
6
+
7
+ describe('llm-bootstrap — built-in provider registration', () => {
8
+ beforeEach(() => {
9
+ llmProviderRegistry.reset()
10
+ resetLlmBootstrapState()
11
+ })
12
+
13
+ it('registers the three native providers and all OpenAI-compatible presets', () => {
14
+ registerBuiltInLlmProviders()
15
+ const ids = llmProviderRegistry.list().map((p) => p.id)
16
+
17
+ // Native protocol adapters.
18
+ expect(ids).toContain('anthropic')
19
+ expect(ids).toContain('google')
20
+
21
+ // OpenAI-compatible presets.
22
+ expect(ids).toContain('openai')
23
+ expect(ids).toContain('deepinfra')
24
+ expect(ids).toContain('groq')
25
+ expect(ids).toContain('together')
26
+ expect(ids).toContain('fireworks')
27
+ expect(ids).toContain('azure')
28
+ expect(ids).toContain('litellm')
29
+ expect(ids).toContain('ollama')
30
+
31
+ expect(ids.length).toBeGreaterThanOrEqual(10)
32
+ })
33
+
34
+ it('is idempotent — calling twice does not duplicate entries', () => {
35
+ registerBuiltInLlmProviders()
36
+ const afterFirst = llmProviderRegistry.list().length
37
+ registerBuiltInLlmProviders()
38
+ const afterSecond = llmProviderRegistry.list().length
39
+ expect(afterSecond).toBe(afterFirst)
40
+ })
41
+
42
+ it('re-registers after registry reset when bootstrap state is reset', () => {
43
+ registerBuiltInLlmProviders()
44
+ expect(llmProviderRegistry.list().length).toBeGreaterThan(0)
45
+
46
+ llmProviderRegistry.reset()
47
+ resetLlmBootstrapState()
48
+ expect(llmProviderRegistry.list().length).toBe(0)
49
+
50
+ registerBuiltInLlmProviders()
51
+ expect(llmProviderRegistry.list().length).toBeGreaterThan(0)
52
+ })
53
+
54
+ it('anthropic provider comes from the AnthropicAdapter factory', () => {
55
+ registerBuiltInLlmProviders()
56
+ const anthropic = llmProviderRegistry.get('anthropic')
57
+ expect(anthropic).not.toBeNull()
58
+ expect(anthropic?.name).toBe('Anthropic')
59
+ expect(anthropic?.envKeys).toEqual(['ANTHROPIC_API_KEY', 'OPENCODE_ANTHROPIC_API_KEY'])
60
+ expect(anthropic?.defaultModel).toBe('claude-haiku-4-5-20251001')
61
+ })
62
+
63
+ it('deepinfra provider comes from the OpenAI-compatible preset', () => {
64
+ registerBuiltInLlmProviders()
65
+ const deepinfra = llmProviderRegistry.get('deepinfra')
66
+ expect(deepinfra).not.toBeNull()
67
+ expect(deepinfra?.envKeys).toEqual(['DEEPINFRA_API_KEY'])
68
+ expect(deepinfra?.defaultModel).toBe('zai-org/GLM-5.1')
69
+ const modelIds = deepinfra?.defaultModels.map((m) => m.id) ?? []
70
+ expect(modelIds).toContain('zai-org/GLM-5.1')
71
+ expect(modelIds).toContain('Qwen/Qwen3-235B-A22B-Instruct-2507')
72
+ })
73
+
74
+ it('resolveFirstConfigured picks a configured provider from the registry', () => {
75
+ registerBuiltInLlmProviders()
76
+ const picked = llmProviderRegistry.resolveFirstConfigured({
77
+ env: { DEEPINFRA_API_KEY: 'deepinfra-key' },
78
+ })
79
+ expect(picked?.id).toBe('deepinfra')
80
+ })
81
+ })
@@ -3,3 +3,13 @@ export { streamText, generateObject, stepCountIs } from 'ai'
3
3
  export { createOpenAI } from '@ai-sdk/openai'
4
4
  export { createAnthropic } from '@ai-sdk/anthropic'
5
5
  export { createGoogleGenerativeAI } from '@ai-sdk/google'
6
+
7
+ // Side-effect import: registers built-in LLM providers (Anthropic, Google,
8
+ // OpenAI + OpenAI-compatible presets for DeepInfra, Groq, Together, etc.)
9
+ // with the shared `llmProviderRegistry` singleton. Consumers that import
10
+ // from `./ai-sdk` transitively trigger provider bootstrap, so any module
11
+ // using `generateObject` / `streamText` already has the registry populated.
12
+ //
13
+ // @see ./llm-bootstrap.ts
14
+ // @see .ai/specs/2026-04-14-llm-provider-ports-and-adapters.md
15
+ import './llm-bootstrap'
@@ -1,13 +1,19 @@
1
1
  import type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib/module-config-service'
2
- import {
3
- OPEN_CODE_PROVIDER_IDS,
4
- OPEN_CODE_PROVIDERS,
5
- isOpenCodeProviderConfigured,
6
- type OpenCodeProviderId,
7
- } from '@open-mercato/shared/lib/ai/opencode-provider'
2
+ import { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'
3
+ import type { LlmProvider } from '@open-mercato/shared/lib/ai/llm-provider'
4
+ // Side-effect: ensures the registry is populated with built-in adapters
5
+ // and OpenAI-compatible presets before this module's getters run.
6
+ import './llm-bootstrap'
8
7
 
9
8
  // Types
10
- export type ChatProviderId = OpenCodeProviderId
9
+ //
10
+ // `ChatProviderId` was previously a narrow literal union of three ids
11
+ // (`'anthropic' | 'openai' | 'google'`). After the ports & adapters
12
+ // refactor the registry accepts any stable id string, so this type
13
+ // becomes `string`. Backward-compatibility note: downstream callers that
14
+ // used exhaustive switches on the old union must add a `default:` branch.
15
+ // See `.ai/specs/2026-04-14-llm-provider-ports-and-adapters.md`.
16
+ export type ChatProviderId = string
11
17
 
12
18
  export type ChatModelInfo = {
13
19
  id: string
@@ -31,51 +37,74 @@ export type ChatProviderConfig = {
31
37
  // Constants
32
38
  export const CHAT_CONFIG_KEY = 'chat_provider'
33
39
 
34
- export const CHAT_PROVIDERS: Record<ChatProviderId, ChatProviderInfo> = {
35
- openai: {
36
- name: OPEN_CODE_PROVIDERS.openai.name,
37
- envKeyRequired: OPEN_CODE_PROVIDERS.openai.envKeys[0],
38
- defaultModel: OPEN_CODE_PROVIDERS.openai.defaultModel,
39
- models: [
40
- { id: OPEN_CODE_PROVIDERS.openai.defaultModel, name: 'GPT-4o Mini', contextWindow: 128000 },
41
- ],
42
- },
43
- anthropic: {
44
- name: OPEN_CODE_PROVIDERS.anthropic.name,
45
- envKeyRequired: OPEN_CODE_PROVIDERS.anthropic.envKeys[0],
46
- defaultModel: OPEN_CODE_PROVIDERS.anthropic.defaultModel,
47
- models: [
48
- { id: OPEN_CODE_PROVIDERS.anthropic.defaultModel, name: 'Claude Haiku 4.5', contextWindow: 200000 },
49
- ],
50
- },
51
- google: {
52
- name: OPEN_CODE_PROVIDERS.google.name,
53
- envKeyRequired: OPEN_CODE_PROVIDERS.google.envKeys[0],
54
- defaultModel: OPEN_CODE_PROVIDERS.google.defaultModel,
55
- models: [
56
- { id: OPEN_CODE_PROVIDERS.google.defaultModel, name: 'Gemini 3 Flash', contextWindow: 1048576 },
57
- ],
58
- },
40
+ function providerToChatInfo(provider: LlmProvider): ChatProviderInfo {
41
+ return {
42
+ name: provider.name,
43
+ envKeyRequired: provider.envKeys[0],
44
+ defaultModel: provider.defaultModel,
45
+ models: provider.defaultModels.map((m) => ({
46
+ id: m.id,
47
+ name: m.name,
48
+ contextWindow: m.contextWindow,
49
+ })),
50
+ }
59
51
  }
60
52
 
53
+ /**
54
+ * `CHAT_PROVIDERS` is a dynamic getter that returns all providers
55
+ * registered with `llmProviderRegistry`. The shape
56
+ * (`Record<string, ChatProviderInfo>`) is preserved so existing code that
57
+ * indexed the map with a string literal (`CHAT_PROVIDERS['anthropic']`)
58
+ * keeps working — it is now a runtime lookup against the registry.
59
+ */
60
+ export const CHAT_PROVIDERS: Record<string, ChatProviderInfo> = new Proxy(
61
+ {} as Record<string, ChatProviderInfo>,
62
+ {
63
+ get(_target, prop: string): ChatProviderInfo | undefined {
64
+ if (typeof prop !== 'string') return undefined
65
+ const provider = llmProviderRegistry.get(prop)
66
+ return provider ? providerToChatInfo(provider) : undefined
67
+ },
68
+ has(_target, prop: string): boolean {
69
+ if (typeof prop !== 'string') return false
70
+ return llmProviderRegistry.get(prop) !== null
71
+ },
72
+ ownKeys(): string[] {
73
+ return llmProviderRegistry.list().map((p) => p.id)
74
+ },
75
+ getOwnPropertyDescriptor(_target, prop: string): PropertyDescriptor | undefined {
76
+ if (typeof prop !== 'string') return undefined
77
+ const provider = llmProviderRegistry.get(prop)
78
+ if (!provider) return undefined
79
+ return {
80
+ enumerable: true,
81
+ configurable: true,
82
+ value: providerToChatInfo(provider),
83
+ }
84
+ },
85
+ },
86
+ )
87
+
61
88
  export const DEFAULT_CHAT_CONFIG: Omit<ChatProviderConfig, 'updatedAt'> = {
62
89
  providerId: 'openai',
63
- model: OPEN_CODE_PROVIDERS.openai.defaultModel,
90
+ get model(): string {
91
+ // Lazy resolution so the bootstrap has a chance to register providers
92
+ // before the default is computed.
93
+ const provider = llmProviderRegistry.get('openai')
94
+ return provider?.defaultModel ?? 'gpt-5-mini'
95
+ },
64
96
  }
65
97
 
66
98
  // Provider configuration checks
67
99
  export function isProviderConfigured(providerId: ChatProviderId): boolean {
68
- return isOpenCodeProviderConfigured(providerId)
100
+ const provider = llmProviderRegistry.get(providerId)
101
+ return provider?.isConfigured() ?? false
69
102
  }
70
103
 
71
104
  export function getConfiguredProviders(): ChatProviderId[] {
72
- const providers: ChatProviderId[] = []
73
- for (const providerId of OPEN_CODE_PROVIDER_IDS) {
74
- if (isProviderConfigured(providerId)) {
75
- providers.push(providerId)
76
- }
77
- }
78
- return providers
105
+ return llmProviderRegistry
106
+ .listConfigured()
107
+ .map((p) => p.id)
79
108
  }
80
109
 
81
110
  // Config resolution
@@ -121,7 +150,11 @@ export async function saveChatConfig(
121
150
  }
122
151
 
123
152
  export function createDefaultConfig(): ChatProviderConfig {
124
- return { ...DEFAULT_CHAT_CONFIG, updatedAt: new Date().toISOString() }
153
+ return {
154
+ providerId: DEFAULT_CHAT_CONFIG.providerId,
155
+ model: DEFAULT_CHAT_CONFIG.model,
156
+ updatedAt: new Date().toISOString(),
157
+ }
125
158
  }
126
159
 
127
160
  // Get model info by ID
@@ -0,0 +1,91 @@
1
+ /**
2
+ * AnthropicAdapter — implements the LlmProvider port for Anthropic's
3
+ * Messages API (Claude Haiku, Sonnet, Opus).
4
+ *
5
+ * Wraps `createAnthropic({ apiKey })` from `@ai-sdk/anthropic` and exposes
6
+ * a curated model list for the AI Assistant UI dropdown.
7
+ *
8
+ * @see packages/shared/src/lib/ai/llm-provider.ts
9
+ * @see .ai/specs/2026-04-14-llm-provider-ports-and-adapters.md
10
+ */
11
+
12
+ import { createAnthropic } from '@ai-sdk/anthropic'
13
+ import type {
14
+ EnvLookup,
15
+ LlmCreateModelOptions,
16
+ LlmModelInfo,
17
+ LlmProvider,
18
+ } from '@open-mercato/shared/lib/ai/llm-provider'
19
+
20
+ const DEFAULT_MODEL = 'claude-haiku-4-5-20251001'
21
+
22
+ const DEFAULT_MODELS: readonly LlmModelInfo[] = [
23
+ {
24
+ id: 'claude-haiku-4-5-20251001',
25
+ name: 'Claude Haiku 4.5',
26
+ contextWindow: 200000,
27
+ tags: ['budget'],
28
+ },
29
+ {
30
+ id: 'claude-sonnet-4-6-20260107',
31
+ name: 'Claude Sonnet 4.6',
32
+ contextWindow: 200000,
33
+ tags: ['flagship'],
34
+ },
35
+ {
36
+ id: 'claude-opus-4-6-20260107',
37
+ name: 'Claude Opus 4.6',
38
+ contextWindow: 1000000,
39
+ tags: ['flagship', 'reasoning'],
40
+ },
41
+ ] as const
42
+
43
+ /**
44
+ * Factory returning a fresh `AnthropicAdapter` instance. The adapter is
45
+ * stateless — caller is free to reuse the returned object.
46
+ */
47
+ export function createAnthropicAdapter(): LlmProvider {
48
+ const envKeys = ['ANTHROPIC_API_KEY', 'OPENCODE_ANTHROPIC_API_KEY'] as const
49
+
50
+ function resolveApiKey(env?: EnvLookup): string | null {
51
+ const lookup = env ?? process.env
52
+ for (const key of envKeys) {
53
+ const value = lookup[key]
54
+ if (typeof value === 'string') {
55
+ const trimmed = value.trim()
56
+ if (trimmed.length > 0) return trimmed
57
+ }
58
+ }
59
+ return null
60
+ }
61
+
62
+ return {
63
+ id: 'anthropic',
64
+ name: 'Anthropic',
65
+ envKeys,
66
+ defaultModel: DEFAULT_MODEL,
67
+ defaultModels: DEFAULT_MODELS,
68
+
69
+ isConfigured(env?: EnvLookup): boolean {
70
+ return resolveApiKey(env) !== null
71
+ },
72
+
73
+ resolveApiKey,
74
+
75
+ getConfiguredEnvKey(env?: EnvLookup): string {
76
+ const lookup = env ?? process.env
77
+ for (const key of envKeys) {
78
+ const value = lookup[key]
79
+ if (typeof value === 'string' && value.trim().length > 0) {
80
+ return key
81
+ }
82
+ }
83
+ return envKeys[0]
84
+ },
85
+
86
+ createModel(options: LlmCreateModelOptions): unknown {
87
+ const anthropic = createAnthropic({ apiKey: options.apiKey })
88
+ return anthropic(options.modelId)
89
+ },
90
+ }
91
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * GoogleAdapter — implements the LlmProvider port for Google's Generative
3
+ * AI API (Gemini Flash, Pro).
4
+ *
5
+ * Wraps `createGoogleGenerativeAI({ apiKey })` from `@ai-sdk/google`.
6
+ *
7
+ * @see packages/shared/src/lib/ai/llm-provider.ts
8
+ * @see .ai/specs/2026-04-14-llm-provider-ports-and-adapters.md
9
+ */
10
+
11
+ import { createGoogleGenerativeAI } from '@ai-sdk/google'
12
+ import type {
13
+ EnvLookup,
14
+ LlmCreateModelOptions,
15
+ LlmModelInfo,
16
+ LlmProvider,
17
+ } from '@open-mercato/shared/lib/ai/llm-provider'
18
+
19
+ const DEFAULT_MODEL = 'gemini-3-flash'
20
+
21
+ const DEFAULT_MODELS: readonly LlmModelInfo[] = [
22
+ {
23
+ id: 'gemini-3-flash',
24
+ name: 'Gemini 3 Flash',
25
+ contextWindow: 1048576,
26
+ tags: ['budget'],
27
+ },
28
+ {
29
+ id: 'gemini-3-pro',
30
+ name: 'Gemini 3 Pro',
31
+ contextWindow: 1048576,
32
+ tags: ['flagship'],
33
+ },
34
+ ] as const
35
+
36
+ export function createGoogleAdapter(): LlmProvider {
37
+ const envKeys = ['GOOGLE_GENERATIVE_AI_API_KEY', 'OPENCODE_GOOGLE_API_KEY'] as const
38
+
39
+ function resolveApiKey(env?: EnvLookup): string | null {
40
+ const lookup = env ?? process.env
41
+ for (const key of envKeys) {
42
+ const value = lookup[key]
43
+ if (typeof value === 'string') {
44
+ const trimmed = value.trim()
45
+ if (trimmed.length > 0) return trimmed
46
+ }
47
+ }
48
+ return null
49
+ }
50
+
51
+ return {
52
+ id: 'google',
53
+ name: 'Google',
54
+ envKeys,
55
+ defaultModel: DEFAULT_MODEL,
56
+ defaultModels: DEFAULT_MODELS,
57
+
58
+ isConfigured(env?: EnvLookup): boolean {
59
+ return resolveApiKey(env) !== null
60
+ },
61
+
62
+ resolveApiKey,
63
+
64
+ getConfiguredEnvKey(env?: EnvLookup): string {
65
+ const lookup = env ?? process.env
66
+ for (const key of envKeys) {
67
+ const value = lookup[key]
68
+ if (typeof value === 'string' && value.trim().length > 0) {
69
+ return key
70
+ }
71
+ }
72
+ return envKeys[0]
73
+ },
74
+
75
+ createModel(options: LlmCreateModelOptions): unknown {
76
+ const google = createGoogleGenerativeAI({ apiKey: options.apiKey })
77
+ return google(options.modelId)
78
+ },
79
+ }
80
+ }