@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,145 @@
1
+ /**
2
+ * OpenAIAdapter — implements the LlmProvider port for the OpenAI
3
+ * chat-completions protocol.
4
+ *
5
+ * This single adapter serves OpenAI itself and every OpenAI-compatible
6
+ * backend (DeepInfra, Groq, Together, Fireworks, Azure OpenAI, LiteLLM,
7
+ * Ollama, LocalAI, vLLM, …). Vendor-specific configuration — endpoint
8
+ * URL, available models, env var conventions — lives in
9
+ * `./openai-compatible-presets.ts` as plain data, not code.
10
+ *
11
+ * The factory {@link createOpenAICompatibleProvider} takes a preset and
12
+ * returns a fully-configured `LlmProvider` that internally calls
13
+ * `createOpenAI({ apiKey, baseURL })` from `@ai-sdk/openai`.
14
+ *
15
+ * @see packages/shared/src/lib/ai/llm-provider.ts
16
+ * @see ./openai-compatible-presets.ts
17
+ * @see .ai/specs/2026-04-14-llm-provider-ports-and-adapters.md
18
+ */
19
+
20
+ import { createOpenAI } from '@ai-sdk/openai'
21
+ import type {
22
+ EnvLookup,
23
+ LlmCreateModelOptions,
24
+ LlmModelInfo,
25
+ LlmProvider,
26
+ } from '@open-mercato/shared/lib/ai/llm-provider'
27
+
28
+ /**
29
+ * Configuration for a single OpenAI-compatible provider instance.
30
+ *
31
+ * Built-in presets live in {@link ./openai-compatible-presets.ts}.
32
+ * Downstream applications may construct custom presets at bootstrap time
33
+ * and register them with `llmProviderRegistry.register(
34
+ * createOpenAICompatibleProvider(customPreset),
35
+ * )`.
36
+ */
37
+ export interface OpenAICompatiblePreset {
38
+ /** Stable id (e.g. `openai`, `deepinfra`, `groq`, `together`). */
39
+ id: string
40
+ /** Human-readable display name. */
41
+ name: string
42
+ /**
43
+ * Upstream base URL. Leave `undefined` to use the AI SDK default
44
+ * (`https://api.openai.com/v1`). Required for DeepInfra, Groq, etc.
45
+ */
46
+ baseURL?: string
47
+ /**
48
+ * Env var names where the adapter looks for the API key, in priority
49
+ * order. Each preset declares its own keys so unrelated presets never
50
+ * accidentally share credentials (e.g. DeepInfra uses
51
+ * `DEEPINFRA_API_KEY`, not `OPENAI_API_KEY`).
52
+ */
53
+ envKeys: readonly string[]
54
+ /** Default model id used when the caller does not specify one. */
55
+ defaultModel: string
56
+ /** Curated model catalog shown in the UI dropdown. */
57
+ defaultModels: readonly LlmModelInfo[]
58
+ /**
59
+ * Optional env var names for overriding the base URL at runtime.
60
+ * Primarily used by presets that rely on a user-supplied URL
61
+ * (Azure deployment, self-hosted LiteLLM, Ollama on a custom port).
62
+ * The first non-empty value wins and overrides {@link baseURL}.
63
+ */
64
+ baseURLEnvKeys?: readonly string[]
65
+ }
66
+
67
+ function readFirstNonEmpty(
68
+ env: EnvLookup,
69
+ keys: readonly string[],
70
+ ): string | null {
71
+ for (const key of keys) {
72
+ const value = env[key]
73
+ if (typeof value === 'string') {
74
+ const trimmed = value.trim()
75
+ if (trimmed.length > 0) return trimmed
76
+ }
77
+ }
78
+ return null
79
+ }
80
+
81
+ /**
82
+ * Builds a `LlmProvider` instance bound to a specific OpenAI-compatible
83
+ * preset. The returned object is stateless and can be registered directly
84
+ * with `llmProviderRegistry.register(...)`.
85
+ */
86
+ export function createOpenAICompatibleProvider(
87
+ preset: OpenAICompatiblePreset,
88
+ ): LlmProvider {
89
+ if (!preset.id || preset.id.length === 0) {
90
+ throw new Error('[OpenAIAdapter] Preset must have a non-empty id')
91
+ }
92
+ if (!preset.envKeys || preset.envKeys.length === 0) {
93
+ throw new Error(
94
+ `[OpenAIAdapter] Preset "${preset.id}" must declare at least one env key`,
95
+ )
96
+ }
97
+
98
+ function resolveApiKey(env?: EnvLookup): string | null {
99
+ return readFirstNonEmpty(env ?? process.env, preset.envKeys)
100
+ }
101
+
102
+ function resolveBaseURL(env?: EnvLookup): string | undefined {
103
+ const lookup = env ?? process.env
104
+ if (preset.baseURLEnvKeys && preset.baseURLEnvKeys.length > 0) {
105
+ const override = readFirstNonEmpty(lookup, preset.baseURLEnvKeys)
106
+ if (override) return override
107
+ }
108
+ return preset.baseURL
109
+ }
110
+
111
+ return {
112
+ id: preset.id,
113
+ name: preset.name,
114
+ envKeys: preset.envKeys,
115
+ defaultModel: preset.defaultModel,
116
+ defaultModels: preset.defaultModels,
117
+
118
+ isConfigured(env?: EnvLookup): boolean {
119
+ return resolveApiKey(env) !== null
120
+ },
121
+
122
+ resolveApiKey,
123
+
124
+ getConfiguredEnvKey(env?: EnvLookup): string {
125
+ const lookup = env ?? process.env
126
+ for (const key of preset.envKeys) {
127
+ const value = lookup[key]
128
+ if (typeof value === 'string' && value.trim().length > 0) {
129
+ return key
130
+ }
131
+ }
132
+ return preset.envKeys[0]
133
+ },
134
+
135
+ createModel(options: LlmCreateModelOptions): unknown {
136
+ // Per-request baseURL override wins over preset/env defaults.
137
+ const baseURL = options.baseURL ?? resolveBaseURL()
138
+ const openai = createOpenAI({
139
+ apiKey: options.apiKey,
140
+ ...(baseURL ? { baseURL } : {}),
141
+ })
142
+ return openai(options.modelId)
143
+ },
144
+ }
145
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * LLM provider bootstrap — registers built-in adapters and OpenAI-compatible
3
+ * presets with the shared `llmProviderRegistry` singleton.
4
+ *
5
+ * This runs at module load via the side-effect import in `./ai-sdk.ts`.
6
+ * Safe to call multiple times — each registration is idempotent (replaces
7
+ * existing by id), so Next.js hot-reload does not duplicate providers.
8
+ *
9
+ * Adapter registration is wrapped in individual try/catch blocks. A
10
+ * failing import (e.g. missing peer dependency) skips that adapter with a
11
+ * `console.warn` but leaves the rest of the registry working.
12
+ *
13
+ * @see packages/shared/src/lib/ai/llm-provider-registry.ts
14
+ * @see .ai/specs/2026-04-14-llm-provider-ports-and-adapters.md
15
+ */
16
+
17
+ import { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'
18
+ import { createAnthropicAdapter } from './llm-adapters/anthropic'
19
+ import { createGoogleAdapter } from './llm-adapters/google'
20
+ import { createOpenAICompatibleProvider } from './llm-adapters/openai'
21
+ import { OPENAI_COMPATIBLE_PRESETS } from './openai-compatible-presets'
22
+
23
+ let bootstrapped = false
24
+
25
+ /**
26
+ * Registers all built-in LLM providers with the shared singleton.
27
+ * Idempotent — the second and subsequent calls are no-ops (unless the
28
+ * registry was reset by a test, in which case registration runs again).
29
+ */
30
+ export function registerBuiltInLlmProviders(): void {
31
+ if (bootstrapped && llmProviderRegistry.list().length > 0) {
32
+ return
33
+ }
34
+
35
+ // Native protocol adapters.
36
+ try {
37
+ llmProviderRegistry.register(createAnthropicAdapter())
38
+ } catch (error) {
39
+ console.warn(
40
+ '[LlmBootstrap] Failed to register Anthropic adapter:',
41
+ error instanceof Error ? error.message : error,
42
+ )
43
+ }
44
+
45
+ try {
46
+ llmProviderRegistry.register(createGoogleAdapter())
47
+ } catch (error) {
48
+ console.warn(
49
+ '[LlmBootstrap] Failed to register Google adapter:',
50
+ error instanceof Error ? error.message : error,
51
+ )
52
+ }
53
+
54
+ // OpenAI-compatible presets — all share one protocol adapter under the
55
+ // hood but appear as separate providers in the registry.
56
+ for (const preset of OPENAI_COMPATIBLE_PRESETS) {
57
+ try {
58
+ llmProviderRegistry.register(createOpenAICompatibleProvider(preset))
59
+ } catch (error) {
60
+ console.warn(
61
+ `[LlmBootstrap] Failed to register OpenAI-compatible preset "${preset.id}":`,
62
+ error instanceof Error ? error.message : error,
63
+ )
64
+ }
65
+ }
66
+
67
+ bootstrapped = true
68
+ }
69
+
70
+ /**
71
+ * Resets the bootstrap state. Intended for tests that call
72
+ * `llmProviderRegistry.reset()` and then want a fresh bootstrap run.
73
+ */
74
+ export function resetLlmBootstrapState(): void {
75
+ bootstrapped = false
76
+ }
77
+
78
+ // Auto-bootstrap on module load so any consumer importing from
79
+ // `@open-mercato/ai-assistant/lib/llm-bootstrap` triggers registration.
80
+ registerBuiltInLlmProviders()
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Curated registry of OpenAI-compatible LLM backends.
3
+ *
4
+ * Each preset is plain data — adding a new backend takes one entry in
5
+ * this array, zero new adapter files, and zero changes to route handlers.
6
+ * The {@link createOpenAICompatibleProvider} factory in `./llm-adapters/openai.ts`
7
+ * turns each preset into a concrete `LlmProvider` at bootstrap time.
8
+ *
9
+ * Preset model catalogs are curated snapshots as of 2026-04-14 and should
10
+ * be updated as upstream catalogs evolve. Users can always override the
11
+ * selected model via the `OPENCODE_MODEL` env var without editing this
12
+ * file.
13
+ *
14
+ * @see ./llm-adapters/openai.ts
15
+ * @see .ai/specs/2026-04-14-llm-provider-ports-and-adapters.md
16
+ */
17
+
18
+ import type { OpenAICompatiblePreset } from './llm-adapters/openai'
19
+
20
+ /**
21
+ * Standard OpenAI — default OpenAI API at api.openai.com.
22
+ */
23
+ const OPENAI_PRESET: OpenAICompatiblePreset = {
24
+ id: 'openai',
25
+ name: 'OpenAI',
26
+ baseURL: undefined,
27
+ envKeys: ['OPENAI_API_KEY', 'OPENCODE_OPENAI_API_KEY'],
28
+ defaultModel: 'gpt-5-mini',
29
+ defaultModels: [
30
+ {
31
+ id: 'gpt-5-mini',
32
+ name: 'GPT-5 Mini',
33
+ contextWindow: 128000,
34
+ tags: ['budget'],
35
+ },
36
+ {
37
+ id: 'gpt-5',
38
+ name: 'GPT-5',
39
+ contextWindow: 128000,
40
+ tags: ['flagship'],
41
+ },
42
+ {
43
+ id: 'gpt-4o-mini',
44
+ name: 'GPT-4o Mini',
45
+ contextWindow: 128000,
46
+ tags: ['budget'],
47
+ },
48
+ {
49
+ id: 'gpt-4o',
50
+ name: 'GPT-4o',
51
+ contextWindow: 128000,
52
+ },
53
+ ],
54
+ }
55
+
56
+ /**
57
+ * DeepInfra — hosts open-weight flagship models at 3-12× lower cost than
58
+ * the native APIs. The curated catalog targets the AI Assistant use case
59
+ * (routing + tool use + conversational chat).
60
+ */
61
+ const DEEPINFRA_PRESET: OpenAICompatiblePreset = {
62
+ id: 'deepinfra',
63
+ name: 'DeepInfra',
64
+ baseURL: 'https://api.deepinfra.com/v1/openai',
65
+ envKeys: ['DEEPINFRA_API_KEY'],
66
+ defaultModel: 'zai-org/GLM-5.1',
67
+ defaultModels: [
68
+ {
69
+ id: 'zai-org/GLM-5.1',
70
+ name: 'GLM-5.1 (Zhipu)',
71
+ contextWindow: 202752,
72
+ tags: ['flagship'],
73
+ },
74
+ {
75
+ id: 'zai-org/GLM-4.7-Flash',
76
+ name: 'GLM-4.7 Flash',
77
+ contextWindow: 202752,
78
+ tags: ['budget'],
79
+ },
80
+ {
81
+ id: 'Qwen/Qwen3-235B-A22B-Instruct-2507',
82
+ name: 'Qwen3 235B (MoE)',
83
+ contextWindow: 262144,
84
+ tags: ['flagship'],
85
+ },
86
+ {
87
+ id: 'meta-llama/Llama-4-Scout-17B-16E-Instruct',
88
+ name: 'Llama 4 Scout',
89
+ contextWindow: 327680,
90
+ },
91
+ {
92
+ id: 'deepseek-ai/DeepSeek-V3.2-Exp',
93
+ name: 'DeepSeek V3.2',
94
+ contextWindow: 163840,
95
+ tags: ['reasoning'],
96
+ },
97
+ {
98
+ id: 'Qwen/Qwen3-Coder-30B-A3B-Instruct',
99
+ name: 'Qwen3 Coder 30B',
100
+ contextWindow: 262144,
101
+ tags: ['coding'],
102
+ },
103
+ ],
104
+ }
105
+
106
+ /**
107
+ * Groq — specializes in low-latency inference on LPU hardware.
108
+ * Best suited for snappy tool-use and routing, less so for long reasoning.
109
+ */
110
+ const GROQ_PRESET: OpenAICompatiblePreset = {
111
+ id: 'groq',
112
+ name: 'Groq',
113
+ baseURL: 'https://api.groq.com/openai/v1',
114
+ envKeys: ['GROQ_API_KEY'],
115
+ defaultModel: 'llama-3.3-70b-versatile',
116
+ defaultModels: [
117
+ {
118
+ id: 'llama-3.3-70b-versatile',
119
+ name: 'Llama 3.3 70B Versatile',
120
+ contextWindow: 131072,
121
+ },
122
+ {
123
+ id: 'llama-4-scout-17b',
124
+ name: 'Llama 4 Scout 17B',
125
+ contextWindow: 131072,
126
+ },
127
+ {
128
+ id: 'mixtral-8x22b-32768',
129
+ name: 'Mixtral 8x22B',
130
+ contextWindow: 32768,
131
+ },
132
+ ],
133
+ }
134
+
135
+ /**
136
+ * Together AI — broad catalog of open-weight models with per-model pricing.
137
+ */
138
+ const TOGETHER_PRESET: OpenAICompatiblePreset = {
139
+ id: 'together',
140
+ name: 'Together AI',
141
+ baseURL: 'https://api.together.xyz/v1',
142
+ envKeys: ['TOGETHER_API_KEY'],
143
+ defaultModel: 'meta-llama/Llama-3.3-70B-Instruct-Turbo',
144
+ defaultModels: [
145
+ {
146
+ id: 'meta-llama/Llama-3.3-70B-Instruct-Turbo',
147
+ name: 'Llama 3.3 70B Turbo',
148
+ contextWindow: 131072,
149
+ },
150
+ {
151
+ id: 'Qwen/Qwen2.5-72B-Instruct-Turbo',
152
+ name: 'Qwen 2.5 72B Turbo',
153
+ contextWindow: 32768,
154
+ },
155
+ ],
156
+ }
157
+
158
+ /**
159
+ * Fireworks AI — fast inference with a curated catalog.
160
+ */
161
+ const FIREWORKS_PRESET: OpenAICompatiblePreset = {
162
+ id: 'fireworks',
163
+ name: 'Fireworks AI',
164
+ baseURL: 'https://api.fireworks.ai/inference/v1',
165
+ envKeys: ['FIREWORKS_API_KEY'],
166
+ defaultModel: 'accounts/fireworks/models/llama-v3p3-70b-instruct',
167
+ defaultModels: [
168
+ {
169
+ id: 'accounts/fireworks/models/llama-v3p3-70b-instruct',
170
+ name: 'Llama 3.3 70B',
171
+ contextWindow: 131072,
172
+ },
173
+ ],
174
+ }
175
+
176
+ /**
177
+ * Azure OpenAI — enterprise Azure deployments. Base URL is deployment-
178
+ * specific and must be provided via `AZURE_OPENAI_BASE_URL`.
179
+ */
180
+ const AZURE_PRESET: OpenAICompatiblePreset = {
181
+ id: 'azure',
182
+ name: 'Azure OpenAI',
183
+ baseURL: undefined,
184
+ baseURLEnvKeys: ['AZURE_OPENAI_BASE_URL'],
185
+ envKeys: ['AZURE_OPENAI_API_KEY'],
186
+ defaultModel: 'gpt-5-mini',
187
+ defaultModels: [
188
+ {
189
+ id: 'gpt-5-mini',
190
+ name: 'GPT-5 Mini',
191
+ contextWindow: 128000,
192
+ },
193
+ {
194
+ id: 'gpt-5',
195
+ name: 'GPT-5',
196
+ contextWindow: 128000,
197
+ },
198
+ ],
199
+ }
200
+
201
+ /**
202
+ * LiteLLM proxy — self-hosted router for arbitrary upstream providers.
203
+ * Base URL must be supplied via `LITELLM_BASE_URL`.
204
+ */
205
+ const LITELLM_PRESET: OpenAICompatiblePreset = {
206
+ id: 'litellm',
207
+ name: 'LiteLLM',
208
+ baseURL: 'http://localhost:4000/v1',
209
+ baseURLEnvKeys: ['LITELLM_BASE_URL'],
210
+ envKeys: ['LITELLM_API_KEY'],
211
+ defaultModel: 'gpt-4o-mini',
212
+ defaultModels: [
213
+ {
214
+ id: 'gpt-4o-mini',
215
+ name: 'GPT-4o Mini (via LiteLLM)',
216
+ contextWindow: 128000,
217
+ },
218
+ ],
219
+ }
220
+
221
+ /**
222
+ * Ollama — local model runner for development and offline use.
223
+ * Default port 11434 can be overridden via `OLLAMA_BASE_URL`.
224
+ */
225
+ const OLLAMA_PRESET: OpenAICompatiblePreset = {
226
+ id: 'ollama',
227
+ name: 'Ollama (local)',
228
+ baseURL: 'http://localhost:11434/v1',
229
+ baseURLEnvKeys: ['OLLAMA_BASE_URL'],
230
+ envKeys: ['OLLAMA_API_KEY'],
231
+ defaultModel: 'llama3.3',
232
+ defaultModels: [
233
+ {
234
+ id: 'llama3.3',
235
+ name: 'Llama 3.3 (local)',
236
+ contextWindow: 131072,
237
+ },
238
+ {
239
+ id: 'qwen2.5-coder',
240
+ name: 'Qwen 2.5 Coder (local)',
241
+ contextWindow: 131072,
242
+ tags: ['coding'],
243
+ },
244
+ ],
245
+ }
246
+
247
+ /**
248
+ * Built-in presets registered at bootstrap time. Order matters — it
249
+ * determines the default iteration order of
250
+ * `llmProviderRegistry.resolveFirstConfigured()` when no explicit order
251
+ * is supplied.
252
+ */
253
+ export const OPENAI_COMPATIBLE_PRESETS: readonly OpenAICompatiblePreset[] = [
254
+ OPENAI_PRESET,
255
+ DEEPINFRA_PRESET,
256
+ GROQ_PRESET,
257
+ TOGETHER_PRESET,
258
+ FIREWORKS_PRESET,
259
+ AZURE_PRESET,
260
+ LITELLM_PRESET,
261
+ OLLAMA_PRESET,
262
+ ]