@open-mercato/ai-assistant 0.4.11-develop.2226.2963a4b52d → 0.4.11-develop.2231.cfcd603204

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,65 @@
1
+ import { createOpenAI } from "@ai-sdk/openai";
2
+ function readFirstNonEmpty(env, keys) {
3
+ for (const key of keys) {
4
+ const value = env[key];
5
+ if (typeof value === "string") {
6
+ const trimmed = value.trim();
7
+ if (trimmed.length > 0) return trimmed;
8
+ }
9
+ }
10
+ return null;
11
+ }
12
+ function createOpenAICompatibleProvider(preset) {
13
+ if (!preset.id || preset.id.length === 0) {
14
+ throw new Error("[OpenAIAdapter] Preset must have a non-empty id");
15
+ }
16
+ if (!preset.envKeys || preset.envKeys.length === 0) {
17
+ throw new Error(
18
+ `[OpenAIAdapter] Preset "${preset.id}" must declare at least one env key`
19
+ );
20
+ }
21
+ function resolveApiKey(env) {
22
+ return readFirstNonEmpty(env ?? process.env, preset.envKeys);
23
+ }
24
+ function resolveBaseURL(env) {
25
+ const lookup = env ?? process.env;
26
+ if (preset.baseURLEnvKeys && preset.baseURLEnvKeys.length > 0) {
27
+ const override = readFirstNonEmpty(lookup, preset.baseURLEnvKeys);
28
+ if (override) return override;
29
+ }
30
+ return preset.baseURL;
31
+ }
32
+ return {
33
+ id: preset.id,
34
+ name: preset.name,
35
+ envKeys: preset.envKeys,
36
+ defaultModel: preset.defaultModel,
37
+ defaultModels: preset.defaultModels,
38
+ isConfigured(env) {
39
+ return resolveApiKey(env) !== null;
40
+ },
41
+ resolveApiKey,
42
+ getConfiguredEnvKey(env) {
43
+ const lookup = env ?? process.env;
44
+ for (const key of preset.envKeys) {
45
+ const value = lookup[key];
46
+ if (typeof value === "string" && value.trim().length > 0) {
47
+ return key;
48
+ }
49
+ }
50
+ return preset.envKeys[0];
51
+ },
52
+ createModel(options) {
53
+ const baseURL = options.baseURL ?? resolveBaseURL();
54
+ const openai = createOpenAI({
55
+ apiKey: options.apiKey,
56
+ ...baseURL ? { baseURL } : {}
57
+ });
58
+ return openai(options.modelId);
59
+ }
60
+ };
61
+ }
62
+ export {
63
+ createOpenAICompatibleProvider
64
+ };
65
+ //# sourceMappingURL=openai.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../src/modules/ai_assistant/lib/llm-adapters/openai.ts"],
4
+ "sourcesContent": ["/**\n * OpenAIAdapter \u2014 implements the LlmProvider port for the OpenAI\n * chat-completions protocol.\n *\n * This single adapter serves OpenAI itself and every OpenAI-compatible\n * backend (DeepInfra, Groq, Together, Fireworks, Azure OpenAI, LiteLLM,\n * Ollama, LocalAI, vLLM, \u2026). Vendor-specific configuration \u2014 endpoint\n * URL, available models, env var conventions \u2014 lives in\n * `./openai-compatible-presets.ts` as plain data, not code.\n *\n * The factory {@link createOpenAICompatibleProvider} takes a preset and\n * returns a fully-configured `LlmProvider` that internally calls\n * `createOpenAI({ apiKey, baseURL })` from `@ai-sdk/openai`.\n *\n * @see packages/shared/src/lib/ai/llm-provider.ts\n * @see ./openai-compatible-presets.ts\n * @see .ai/specs/2026-04-14-llm-provider-ports-and-adapters.md\n */\n\nimport { createOpenAI } from '@ai-sdk/openai'\nimport type {\n EnvLookup,\n LlmCreateModelOptions,\n LlmModelInfo,\n LlmProvider,\n} from '@open-mercato/shared/lib/ai/llm-provider'\n\n/**\n * Configuration for a single OpenAI-compatible provider instance.\n *\n * Built-in presets live in {@link ./openai-compatible-presets.ts}.\n * Downstream applications may construct custom presets at bootstrap time\n * and register them with `llmProviderRegistry.register(\n * createOpenAICompatibleProvider(customPreset),\n * )`.\n */\nexport interface OpenAICompatiblePreset {\n /** Stable id (e.g. `openai`, `deepinfra`, `groq`, `together`). */\n id: string\n /** Human-readable display name. */\n name: string\n /**\n * Upstream base URL. Leave `undefined` to use the AI SDK default\n * (`https://api.openai.com/v1`). Required for DeepInfra, Groq, etc.\n */\n baseURL?: string\n /**\n * Env var names where the adapter looks for the API key, in priority\n * order. Each preset declares its own keys so unrelated presets never\n * accidentally share credentials (e.g. DeepInfra uses\n * `DEEPINFRA_API_KEY`, not `OPENAI_API_KEY`).\n */\n envKeys: readonly string[]\n /** Default model id used when the caller does not specify one. */\n defaultModel: string\n /** Curated model catalog shown in the UI dropdown. */\n defaultModels: readonly LlmModelInfo[]\n /**\n * Optional env var names for overriding the base URL at runtime.\n * Primarily used by presets that rely on a user-supplied URL\n * (Azure deployment, self-hosted LiteLLM, Ollama on a custom port).\n * The first non-empty value wins and overrides {@link baseURL}.\n */\n baseURLEnvKeys?: readonly string[]\n}\n\nfunction readFirstNonEmpty(\n env: EnvLookup,\n keys: readonly string[],\n): string | null {\n for (const key of keys) {\n const value = env[key]\n if (typeof value === 'string') {\n const trimmed = value.trim()\n if (trimmed.length > 0) return trimmed\n }\n }\n return null\n}\n\n/**\n * Builds a `LlmProvider` instance bound to a specific OpenAI-compatible\n * preset. The returned object is stateless and can be registered directly\n * with `llmProviderRegistry.register(...)`.\n */\nexport function createOpenAICompatibleProvider(\n preset: OpenAICompatiblePreset,\n): LlmProvider {\n if (!preset.id || preset.id.length === 0) {\n throw new Error('[OpenAIAdapter] Preset must have a non-empty id')\n }\n if (!preset.envKeys || preset.envKeys.length === 0) {\n throw new Error(\n `[OpenAIAdapter] Preset \"${preset.id}\" must declare at least one env key`,\n )\n }\n\n function resolveApiKey(env?: EnvLookup): string | null {\n return readFirstNonEmpty(env ?? process.env, preset.envKeys)\n }\n\n function resolveBaseURL(env?: EnvLookup): string | undefined {\n const lookup = env ?? process.env\n if (preset.baseURLEnvKeys && preset.baseURLEnvKeys.length > 0) {\n const override = readFirstNonEmpty(lookup, preset.baseURLEnvKeys)\n if (override) return override\n }\n return preset.baseURL\n }\n\n return {\n id: preset.id,\n name: preset.name,\n envKeys: preset.envKeys,\n defaultModel: preset.defaultModel,\n defaultModels: preset.defaultModels,\n\n isConfigured(env?: EnvLookup): boolean {\n return resolveApiKey(env) !== null\n },\n\n resolveApiKey,\n\n getConfiguredEnvKey(env?: EnvLookup): string {\n const lookup = env ?? process.env\n for (const key of preset.envKeys) {\n const value = lookup[key]\n if (typeof value === 'string' && value.trim().length > 0) {\n return key\n }\n }\n return preset.envKeys[0]\n },\n\n createModel(options: LlmCreateModelOptions): unknown {\n // Per-request baseURL override wins over preset/env defaults.\n const baseURL = options.baseURL ?? resolveBaseURL()\n const openai = createOpenAI({\n apiKey: options.apiKey,\n ...(baseURL ? { baseURL } : {}),\n })\n return openai(options.modelId)\n },\n }\n}\n"],
5
+ "mappings": "AAmBA,SAAS,oBAAoB;AA+C7B,SAAS,kBACP,KACA,MACe;AACf,aAAW,OAAO,MAAM;AACtB,UAAM,QAAQ,IAAI,GAAG;AACrB,QAAI,OAAO,UAAU,UAAU;AAC7B,YAAM,UAAU,MAAM,KAAK;AAC3B,UAAI,QAAQ,SAAS,EAAG,QAAO;AAAA,IACjC;AAAA,EACF;AACA,SAAO;AACT;AAOO,SAAS,+BACd,QACa;AACb,MAAI,CAAC,OAAO,MAAM,OAAO,GAAG,WAAW,GAAG;AACxC,UAAM,IAAI,MAAM,iDAAiD;AAAA,EACnE;AACA,MAAI,CAAC,OAAO,WAAW,OAAO,QAAQ,WAAW,GAAG;AAClD,UAAM,IAAI;AAAA,MACR,2BAA2B,OAAO,EAAE;AAAA,IACtC;AAAA,EACF;AAEA,WAAS,cAAc,KAAgC;AACrD,WAAO,kBAAkB,OAAO,QAAQ,KAAK,OAAO,OAAO;AAAA,EAC7D;AAEA,WAAS,eAAe,KAAqC;AAC3D,UAAM,SAAS,OAAO,QAAQ;AAC9B,QAAI,OAAO,kBAAkB,OAAO,eAAe,SAAS,GAAG;AAC7D,YAAM,WAAW,kBAAkB,QAAQ,OAAO,cAAc;AAChE,UAAI,SAAU,QAAO;AAAA,IACvB;AACA,WAAO,OAAO;AAAA,EAChB;AAEA,SAAO;AAAA,IACL,IAAI,OAAO;AAAA,IACX,MAAM,OAAO;AAAA,IACb,SAAS,OAAO;AAAA,IAChB,cAAc,OAAO;AAAA,IACrB,eAAe,OAAO;AAAA,IAEtB,aAAa,KAA0B;AACrC,aAAO,cAAc,GAAG,MAAM;AAAA,IAChC;AAAA,IAEA;AAAA,IAEA,oBAAoB,KAAyB;AAC3C,YAAM,SAAS,OAAO,QAAQ;AAC9B,iBAAW,OAAO,OAAO,SAAS;AAChC,cAAM,QAAQ,OAAO,GAAG;AACxB,YAAI,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,SAAS,GAAG;AACxD,iBAAO;AAAA,QACT;AAAA,MACF;AACA,aAAO,OAAO,QAAQ,CAAC;AAAA,IACzB;AAAA,IAEA,YAAY,SAAyC;AAEnD,YAAM,UAAU,QAAQ,WAAW,eAAe;AAClD,YAAM,SAAS,aAAa;AAAA,QAC1B,QAAQ,QAAQ;AAAA,QAChB,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,MAC/B,CAAC;AACD,aAAO,OAAO,QAAQ,OAAO;AAAA,IAC/B;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,47 @@
1
+ import { llmProviderRegistry } from "@open-mercato/shared/lib/ai/llm-provider-registry";
2
+ import { createAnthropicAdapter } from "./llm-adapters/anthropic.js";
3
+ import { createGoogleAdapter } from "./llm-adapters/google.js";
4
+ import { createOpenAICompatibleProvider } from "./llm-adapters/openai.js";
5
+ import { OPENAI_COMPATIBLE_PRESETS } from "./openai-compatible-presets.js";
6
+ let bootstrapped = false;
7
+ function registerBuiltInLlmProviders() {
8
+ if (bootstrapped && llmProviderRegistry.list().length > 0) {
9
+ return;
10
+ }
11
+ try {
12
+ llmProviderRegistry.register(createAnthropicAdapter());
13
+ } catch (error) {
14
+ console.warn(
15
+ "[LlmBootstrap] Failed to register Anthropic adapter:",
16
+ error instanceof Error ? error.message : error
17
+ );
18
+ }
19
+ try {
20
+ llmProviderRegistry.register(createGoogleAdapter());
21
+ } catch (error) {
22
+ console.warn(
23
+ "[LlmBootstrap] Failed to register Google adapter:",
24
+ error instanceof Error ? error.message : error
25
+ );
26
+ }
27
+ for (const preset of OPENAI_COMPATIBLE_PRESETS) {
28
+ try {
29
+ llmProviderRegistry.register(createOpenAICompatibleProvider(preset));
30
+ } catch (error) {
31
+ console.warn(
32
+ `[LlmBootstrap] Failed to register OpenAI-compatible preset "${preset.id}":`,
33
+ error instanceof Error ? error.message : error
34
+ );
35
+ }
36
+ }
37
+ bootstrapped = true;
38
+ }
39
+ function resetLlmBootstrapState() {
40
+ bootstrapped = false;
41
+ }
42
+ registerBuiltInLlmProviders();
43
+ export {
44
+ registerBuiltInLlmProviders,
45
+ resetLlmBootstrapState
46
+ };
47
+ //# sourceMappingURL=llm-bootstrap.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/ai_assistant/lib/llm-bootstrap.ts"],
4
+ "sourcesContent": ["/**\n * LLM provider bootstrap \u2014 registers built-in adapters and OpenAI-compatible\n * presets with the shared `llmProviderRegistry` singleton.\n *\n * This runs at module load via the side-effect import in `./ai-sdk.ts`.\n * Safe to call multiple times \u2014 each registration is idempotent (replaces\n * existing by id), so Next.js hot-reload does not duplicate providers.\n *\n * Adapter registration is wrapped in individual try/catch blocks. A\n * failing import (e.g. missing peer dependency) skips that adapter with a\n * `console.warn` but leaves the rest of the registry working.\n *\n * @see packages/shared/src/lib/ai/llm-provider-registry.ts\n * @see .ai/specs/2026-04-14-llm-provider-ports-and-adapters.md\n */\n\nimport { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'\nimport { createAnthropicAdapter } from './llm-adapters/anthropic'\nimport { createGoogleAdapter } from './llm-adapters/google'\nimport { createOpenAICompatibleProvider } from './llm-adapters/openai'\nimport { OPENAI_COMPATIBLE_PRESETS } from './openai-compatible-presets'\n\nlet bootstrapped = false\n\n/**\n * Registers all built-in LLM providers with the shared singleton.\n * Idempotent \u2014 the second and subsequent calls are no-ops (unless the\n * registry was reset by a test, in which case registration runs again).\n */\nexport function registerBuiltInLlmProviders(): void {\n if (bootstrapped && llmProviderRegistry.list().length > 0) {\n return\n }\n\n // Native protocol adapters.\n try {\n llmProviderRegistry.register(createAnthropicAdapter())\n } catch (error) {\n console.warn(\n '[LlmBootstrap] Failed to register Anthropic adapter:',\n error instanceof Error ? error.message : error,\n )\n }\n\n try {\n llmProviderRegistry.register(createGoogleAdapter())\n } catch (error) {\n console.warn(\n '[LlmBootstrap] Failed to register Google adapter:',\n error instanceof Error ? error.message : error,\n )\n }\n\n // OpenAI-compatible presets \u2014 all share one protocol adapter under the\n // hood but appear as separate providers in the registry.\n for (const preset of OPENAI_COMPATIBLE_PRESETS) {\n try {\n llmProviderRegistry.register(createOpenAICompatibleProvider(preset))\n } catch (error) {\n console.warn(\n `[LlmBootstrap] Failed to register OpenAI-compatible preset \"${preset.id}\":`,\n error instanceof Error ? error.message : error,\n )\n }\n }\n\n bootstrapped = true\n}\n\n/**\n * Resets the bootstrap state. Intended for tests that call\n * `llmProviderRegistry.reset()` and then want a fresh bootstrap run.\n */\nexport function resetLlmBootstrapState(): void {\n bootstrapped = false\n}\n\n// Auto-bootstrap on module load so any consumer importing from\n// `@open-mercato/ai-assistant/lib/llm-bootstrap` triggers registration.\nregisterBuiltInLlmProviders()\n"],
5
+ "mappings": "AAgBA,SAAS,2BAA2B;AACpC,SAAS,8BAA8B;AACvC,SAAS,2BAA2B;AACpC,SAAS,sCAAsC;AAC/C,SAAS,iCAAiC;AAE1C,IAAI,eAAe;AAOZ,SAAS,8BAAoC;AAClD,MAAI,gBAAgB,oBAAoB,KAAK,EAAE,SAAS,GAAG;AACzD;AAAA,EACF;AAGA,MAAI;AACF,wBAAoB,SAAS,uBAAuB,CAAC;AAAA,EACvD,SAAS,OAAO;AACd,YAAQ;AAAA,MACN;AAAA,MACA,iBAAiB,QAAQ,MAAM,UAAU;AAAA,IAC3C;AAAA,EACF;AAEA,MAAI;AACF,wBAAoB,SAAS,oBAAoB,CAAC;AAAA,EACpD,SAAS,OAAO;AACd,YAAQ;AAAA,MACN;AAAA,MACA,iBAAiB,QAAQ,MAAM,UAAU;AAAA,IAC3C;AAAA,EACF;AAIA,aAAW,UAAU,2BAA2B;AAC9C,QAAI;AACF,0BAAoB,SAAS,+BAA+B,MAAM,CAAC;AAAA,IACrE,SAAS,OAAO;AACd,cAAQ;AAAA,QACN,+DAA+D,OAAO,EAAE;AAAA,QACxE,iBAAiB,QAAQ,MAAM,UAAU;AAAA,MAC3C;AAAA,IACF;AAAA,EACF;AAEA,iBAAe;AACjB;AAMO,SAAS,yBAA+B;AAC7C,iBAAe;AACjB;AAIA,4BAA4B;",
6
+ "names": []
7
+ }
@@ -0,0 +1,203 @@
1
+ const OPENAI_PRESET = {
2
+ id: "openai",
3
+ name: "OpenAI",
4
+ baseURL: void 0,
5
+ envKeys: ["OPENAI_API_KEY", "OPENCODE_OPENAI_API_KEY"],
6
+ defaultModel: "gpt-5-mini",
7
+ defaultModels: [
8
+ {
9
+ id: "gpt-5-mini",
10
+ name: "GPT-5 Mini",
11
+ contextWindow: 128e3,
12
+ tags: ["budget"]
13
+ },
14
+ {
15
+ id: "gpt-5",
16
+ name: "GPT-5",
17
+ contextWindow: 128e3,
18
+ tags: ["flagship"]
19
+ },
20
+ {
21
+ id: "gpt-4o-mini",
22
+ name: "GPT-4o Mini",
23
+ contextWindow: 128e3,
24
+ tags: ["budget"]
25
+ },
26
+ {
27
+ id: "gpt-4o",
28
+ name: "GPT-4o",
29
+ contextWindow: 128e3
30
+ }
31
+ ]
32
+ };
33
+ const DEEPINFRA_PRESET = {
34
+ id: "deepinfra",
35
+ name: "DeepInfra",
36
+ baseURL: "https://api.deepinfra.com/v1/openai",
37
+ envKeys: ["DEEPINFRA_API_KEY"],
38
+ defaultModel: "zai-org/GLM-5.1",
39
+ defaultModels: [
40
+ {
41
+ id: "zai-org/GLM-5.1",
42
+ name: "GLM-5.1 (Zhipu)",
43
+ contextWindow: 202752,
44
+ tags: ["flagship"]
45
+ },
46
+ {
47
+ id: "zai-org/GLM-4.7-Flash",
48
+ name: "GLM-4.7 Flash",
49
+ contextWindow: 202752,
50
+ tags: ["budget"]
51
+ },
52
+ {
53
+ id: "Qwen/Qwen3-235B-A22B-Instruct-2507",
54
+ name: "Qwen3 235B (MoE)",
55
+ contextWindow: 262144,
56
+ tags: ["flagship"]
57
+ },
58
+ {
59
+ id: "meta-llama/Llama-4-Scout-17B-16E-Instruct",
60
+ name: "Llama 4 Scout",
61
+ contextWindow: 327680
62
+ },
63
+ {
64
+ id: "deepseek-ai/DeepSeek-V3.2-Exp",
65
+ name: "DeepSeek V3.2",
66
+ contextWindow: 163840,
67
+ tags: ["reasoning"]
68
+ },
69
+ {
70
+ id: "Qwen/Qwen3-Coder-30B-A3B-Instruct",
71
+ name: "Qwen3 Coder 30B",
72
+ contextWindow: 262144,
73
+ tags: ["coding"]
74
+ }
75
+ ]
76
+ };
77
+ const GROQ_PRESET = {
78
+ id: "groq",
79
+ name: "Groq",
80
+ baseURL: "https://api.groq.com/openai/v1",
81
+ envKeys: ["GROQ_API_KEY"],
82
+ defaultModel: "llama-3.3-70b-versatile",
83
+ defaultModels: [
84
+ {
85
+ id: "llama-3.3-70b-versatile",
86
+ name: "Llama 3.3 70B Versatile",
87
+ contextWindow: 131072
88
+ },
89
+ {
90
+ id: "llama-4-scout-17b",
91
+ name: "Llama 4 Scout 17B",
92
+ contextWindow: 131072
93
+ },
94
+ {
95
+ id: "mixtral-8x22b-32768",
96
+ name: "Mixtral 8x22B",
97
+ contextWindow: 32768
98
+ }
99
+ ]
100
+ };
101
+ const TOGETHER_PRESET = {
102
+ id: "together",
103
+ name: "Together AI",
104
+ baseURL: "https://api.together.xyz/v1",
105
+ envKeys: ["TOGETHER_API_KEY"],
106
+ defaultModel: "meta-llama/Llama-3.3-70B-Instruct-Turbo",
107
+ defaultModels: [
108
+ {
109
+ id: "meta-llama/Llama-3.3-70B-Instruct-Turbo",
110
+ name: "Llama 3.3 70B Turbo",
111
+ contextWindow: 131072
112
+ },
113
+ {
114
+ id: "Qwen/Qwen2.5-72B-Instruct-Turbo",
115
+ name: "Qwen 2.5 72B Turbo",
116
+ contextWindow: 32768
117
+ }
118
+ ]
119
+ };
120
+ const FIREWORKS_PRESET = {
121
+ id: "fireworks",
122
+ name: "Fireworks AI",
123
+ baseURL: "https://api.fireworks.ai/inference/v1",
124
+ envKeys: ["FIREWORKS_API_KEY"],
125
+ defaultModel: "accounts/fireworks/models/llama-v3p3-70b-instruct",
126
+ defaultModels: [
127
+ {
128
+ id: "accounts/fireworks/models/llama-v3p3-70b-instruct",
129
+ name: "Llama 3.3 70B",
130
+ contextWindow: 131072
131
+ }
132
+ ]
133
+ };
134
+ const AZURE_PRESET = {
135
+ id: "azure",
136
+ name: "Azure OpenAI",
137
+ baseURL: void 0,
138
+ baseURLEnvKeys: ["AZURE_OPENAI_BASE_URL"],
139
+ envKeys: ["AZURE_OPENAI_API_KEY"],
140
+ defaultModel: "gpt-5-mini",
141
+ defaultModels: [
142
+ {
143
+ id: "gpt-5-mini",
144
+ name: "GPT-5 Mini",
145
+ contextWindow: 128e3
146
+ },
147
+ {
148
+ id: "gpt-5",
149
+ name: "GPT-5",
150
+ contextWindow: 128e3
151
+ }
152
+ ]
153
+ };
154
+ const LITELLM_PRESET = {
155
+ id: "litellm",
156
+ name: "LiteLLM",
157
+ baseURL: "http://localhost:4000/v1",
158
+ baseURLEnvKeys: ["LITELLM_BASE_URL"],
159
+ envKeys: ["LITELLM_API_KEY"],
160
+ defaultModel: "gpt-4o-mini",
161
+ defaultModels: [
162
+ {
163
+ id: "gpt-4o-mini",
164
+ name: "GPT-4o Mini (via LiteLLM)",
165
+ contextWindow: 128e3
166
+ }
167
+ ]
168
+ };
169
+ const OLLAMA_PRESET = {
170
+ id: "ollama",
171
+ name: "Ollama (local)",
172
+ baseURL: "http://localhost:11434/v1",
173
+ baseURLEnvKeys: ["OLLAMA_BASE_URL"],
174
+ envKeys: ["OLLAMA_API_KEY"],
175
+ defaultModel: "llama3.3",
176
+ defaultModels: [
177
+ {
178
+ id: "llama3.3",
179
+ name: "Llama 3.3 (local)",
180
+ contextWindow: 131072
181
+ },
182
+ {
183
+ id: "qwen2.5-coder",
184
+ name: "Qwen 2.5 Coder (local)",
185
+ contextWindow: 131072,
186
+ tags: ["coding"]
187
+ }
188
+ ]
189
+ };
190
+ const OPENAI_COMPATIBLE_PRESETS = [
191
+ OPENAI_PRESET,
192
+ DEEPINFRA_PRESET,
193
+ GROQ_PRESET,
194
+ TOGETHER_PRESET,
195
+ FIREWORKS_PRESET,
196
+ AZURE_PRESET,
197
+ LITELLM_PRESET,
198
+ OLLAMA_PRESET
199
+ ];
200
+ export {
201
+ OPENAI_COMPATIBLE_PRESETS
202
+ };
203
+ //# sourceMappingURL=openai-compatible-presets.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/ai_assistant/lib/openai-compatible-presets.ts"],
4
+ "sourcesContent": ["/**\n * Curated registry of OpenAI-compatible LLM backends.\n *\n * Each preset is plain data \u2014 adding a new backend takes one entry in\n * this array, zero new adapter files, and zero changes to route handlers.\n * The {@link createOpenAICompatibleProvider} factory in `./llm-adapters/openai.ts`\n * turns each preset into a concrete `LlmProvider` at bootstrap time.\n *\n * Preset model catalogs are curated snapshots as of 2026-04-14 and should\n * be updated as upstream catalogs evolve. Users can always override the\n * selected model via the `OPENCODE_MODEL` env var without editing this\n * file.\n *\n * @see ./llm-adapters/openai.ts\n * @see .ai/specs/2026-04-14-llm-provider-ports-and-adapters.md\n */\n\nimport type { OpenAICompatiblePreset } from './llm-adapters/openai'\n\n/**\n * Standard OpenAI \u2014 default OpenAI API at api.openai.com.\n */\nconst OPENAI_PRESET: OpenAICompatiblePreset = {\n id: 'openai',\n name: 'OpenAI',\n baseURL: undefined,\n envKeys: ['OPENAI_API_KEY', 'OPENCODE_OPENAI_API_KEY'],\n defaultModel: 'gpt-5-mini',\n defaultModels: [\n {\n id: 'gpt-5-mini',\n name: 'GPT-5 Mini',\n contextWindow: 128000,\n tags: ['budget'],\n },\n {\n id: 'gpt-5',\n name: 'GPT-5',\n contextWindow: 128000,\n tags: ['flagship'],\n },\n {\n id: 'gpt-4o-mini',\n name: 'GPT-4o Mini',\n contextWindow: 128000,\n tags: ['budget'],\n },\n {\n id: 'gpt-4o',\n name: 'GPT-4o',\n contextWindow: 128000,\n },\n ],\n}\n\n/**\n * DeepInfra \u2014 hosts open-weight flagship models at 3-12\u00D7 lower cost than\n * the native APIs. The curated catalog targets the AI Assistant use case\n * (routing + tool use + conversational chat).\n */\nconst DEEPINFRA_PRESET: OpenAICompatiblePreset = {\n id: 'deepinfra',\n name: 'DeepInfra',\n baseURL: 'https://api.deepinfra.com/v1/openai',\n envKeys: ['DEEPINFRA_API_KEY'],\n defaultModel: 'zai-org/GLM-5.1',\n defaultModels: [\n {\n id: 'zai-org/GLM-5.1',\n name: 'GLM-5.1 (Zhipu)',\n contextWindow: 202752,\n tags: ['flagship'],\n },\n {\n id: 'zai-org/GLM-4.7-Flash',\n name: 'GLM-4.7 Flash',\n contextWindow: 202752,\n tags: ['budget'],\n },\n {\n id: 'Qwen/Qwen3-235B-A22B-Instruct-2507',\n name: 'Qwen3 235B (MoE)',\n contextWindow: 262144,\n tags: ['flagship'],\n },\n {\n id: 'meta-llama/Llama-4-Scout-17B-16E-Instruct',\n name: 'Llama 4 Scout',\n contextWindow: 327680,\n },\n {\n id: 'deepseek-ai/DeepSeek-V3.2-Exp',\n name: 'DeepSeek V3.2',\n contextWindow: 163840,\n tags: ['reasoning'],\n },\n {\n id: 'Qwen/Qwen3-Coder-30B-A3B-Instruct',\n name: 'Qwen3 Coder 30B',\n contextWindow: 262144,\n tags: ['coding'],\n },\n ],\n}\n\n/**\n * Groq \u2014 specializes in low-latency inference on LPU hardware.\n * Best suited for snappy tool-use and routing, less so for long reasoning.\n */\nconst GROQ_PRESET: OpenAICompatiblePreset = {\n id: 'groq',\n name: 'Groq',\n baseURL: 'https://api.groq.com/openai/v1',\n envKeys: ['GROQ_API_KEY'],\n defaultModel: 'llama-3.3-70b-versatile',\n defaultModels: [\n {\n id: 'llama-3.3-70b-versatile',\n name: 'Llama 3.3 70B Versatile',\n contextWindow: 131072,\n },\n {\n id: 'llama-4-scout-17b',\n name: 'Llama 4 Scout 17B',\n contextWindow: 131072,\n },\n {\n id: 'mixtral-8x22b-32768',\n name: 'Mixtral 8x22B',\n contextWindow: 32768,\n },\n ],\n}\n\n/**\n * Together AI \u2014 broad catalog of open-weight models with per-model pricing.\n */\nconst TOGETHER_PRESET: OpenAICompatiblePreset = {\n id: 'together',\n name: 'Together AI',\n baseURL: 'https://api.together.xyz/v1',\n envKeys: ['TOGETHER_API_KEY'],\n defaultModel: 'meta-llama/Llama-3.3-70B-Instruct-Turbo',\n defaultModels: [\n {\n id: 'meta-llama/Llama-3.3-70B-Instruct-Turbo',\n name: 'Llama 3.3 70B Turbo',\n contextWindow: 131072,\n },\n {\n id: 'Qwen/Qwen2.5-72B-Instruct-Turbo',\n name: 'Qwen 2.5 72B Turbo',\n contextWindow: 32768,\n },\n ],\n}\n\n/**\n * Fireworks AI \u2014 fast inference with a curated catalog.\n */\nconst FIREWORKS_PRESET: OpenAICompatiblePreset = {\n id: 'fireworks',\n name: 'Fireworks AI',\n baseURL: 'https://api.fireworks.ai/inference/v1',\n envKeys: ['FIREWORKS_API_KEY'],\n defaultModel: 'accounts/fireworks/models/llama-v3p3-70b-instruct',\n defaultModels: [\n {\n id: 'accounts/fireworks/models/llama-v3p3-70b-instruct',\n name: 'Llama 3.3 70B',\n contextWindow: 131072,\n },\n ],\n}\n\n/**\n * Azure OpenAI \u2014 enterprise Azure deployments. Base URL is deployment-\n * specific and must be provided via `AZURE_OPENAI_BASE_URL`.\n */\nconst AZURE_PRESET: OpenAICompatiblePreset = {\n id: 'azure',\n name: 'Azure OpenAI',\n baseURL: undefined,\n baseURLEnvKeys: ['AZURE_OPENAI_BASE_URL'],\n envKeys: ['AZURE_OPENAI_API_KEY'],\n defaultModel: 'gpt-5-mini',\n defaultModels: [\n {\n id: 'gpt-5-mini',\n name: 'GPT-5 Mini',\n contextWindow: 128000,\n },\n {\n id: 'gpt-5',\n name: 'GPT-5',\n contextWindow: 128000,\n },\n ],\n}\n\n/**\n * LiteLLM proxy \u2014 self-hosted router for arbitrary upstream providers.\n * Base URL must be supplied via `LITELLM_BASE_URL`.\n */\nconst LITELLM_PRESET: OpenAICompatiblePreset = {\n id: 'litellm',\n name: 'LiteLLM',\n baseURL: 'http://localhost:4000/v1',\n baseURLEnvKeys: ['LITELLM_BASE_URL'],\n envKeys: ['LITELLM_API_KEY'],\n defaultModel: 'gpt-4o-mini',\n defaultModels: [\n {\n id: 'gpt-4o-mini',\n name: 'GPT-4o Mini (via LiteLLM)',\n contextWindow: 128000,\n },\n ],\n}\n\n/**\n * Ollama \u2014 local model runner for development and offline use.\n * Default port 11434 can be overridden via `OLLAMA_BASE_URL`.\n */\nconst OLLAMA_PRESET: OpenAICompatiblePreset = {\n id: 'ollama',\n name: 'Ollama (local)',\n baseURL: 'http://localhost:11434/v1',\n baseURLEnvKeys: ['OLLAMA_BASE_URL'],\n envKeys: ['OLLAMA_API_KEY'],\n defaultModel: 'llama3.3',\n defaultModels: [\n {\n id: 'llama3.3',\n name: 'Llama 3.3 (local)',\n contextWindow: 131072,\n },\n {\n id: 'qwen2.5-coder',\n name: 'Qwen 2.5 Coder (local)',\n contextWindow: 131072,\n tags: ['coding'],\n },\n ],\n}\n\n/**\n * Built-in presets registered at bootstrap time. Order matters \u2014 it\n * determines the default iteration order of\n * `llmProviderRegistry.resolveFirstConfigured()` when no explicit order\n * is supplied.\n */\nexport const OPENAI_COMPATIBLE_PRESETS: readonly OpenAICompatiblePreset[] = [\n OPENAI_PRESET,\n DEEPINFRA_PRESET,\n GROQ_PRESET,\n TOGETHER_PRESET,\n FIREWORKS_PRESET,\n AZURE_PRESET,\n LITELLM_PRESET,\n OLLAMA_PRESET,\n]\n"],
5
+ "mappings": "AAsBA,MAAM,gBAAwC;AAAA,EAC5C,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,SAAS;AAAA,EACT,SAAS,CAAC,kBAAkB,yBAAyB;AAAA,EACrD,cAAc;AAAA,EACd,eAAe;AAAA,IACb;AAAA,MACE,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,eAAe;AAAA,MACf,MAAM,CAAC,QAAQ;AAAA,IACjB;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,eAAe;AAAA,MACf,MAAM,CAAC,UAAU;AAAA,IACnB;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,eAAe;AAAA,MACf,MAAM,CAAC,QAAQ;AAAA,IACjB;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,eAAe;AAAA,IACjB;AAAA,EACF;AACF;AAOA,MAAM,mBAA2C;AAAA,EAC/C,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,SAAS;AAAA,EACT,SAAS,CAAC,mBAAmB;AAAA,EAC7B,cAAc;AAAA,EACd,eAAe;AAAA,IACb;AAAA,MACE,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,eAAe;AAAA,MACf,MAAM,CAAC,UAAU;AAAA,IACnB;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,eAAe;AAAA,MACf,MAAM,CAAC,QAAQ;AAAA,IACjB;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,eAAe;AAAA,MACf,MAAM,CAAC,UAAU;AAAA,IACnB;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,eAAe;AAAA,IACjB;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,eAAe;AAAA,MACf,MAAM,CAAC,WAAW;AAAA,IACpB;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,eAAe;AAAA,MACf,MAAM,CAAC,QAAQ;AAAA,IACjB;AAAA,EACF;AACF;AAMA,MAAM,cAAsC;AAAA,EAC1C,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,SAAS;AAAA,EACT,SAAS,CAAC,cAAc;AAAA,EACxB,cAAc;AAAA,EACd,eAAe;AAAA,IACb;AAAA,MACE,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,eAAe;AAAA,IACjB;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,eAAe;AAAA,IACjB;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,eAAe;AAAA,IACjB;AAAA,EACF;AACF;AAKA,MAAM,kBAA0C;AAAA,EAC9C,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,SAAS;AAAA,EACT,SAAS,CAAC,kBAAkB;AAAA,EAC5B,cAAc;AAAA,EACd,eAAe;AAAA,IACb;AAAA,MACE,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,eAAe;AAAA,IACjB;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,eAAe;AAAA,IACjB;AAAA,EACF;AACF;AAKA,MAAM,mBAA2C;AAAA,EAC/C,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,SAAS;AAAA,EACT,SAAS,CAAC,mBAAmB;AAAA,EAC7B,cAAc;AAAA,EACd,eAAe;AAAA,IACb;AAAA,MACE,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,eAAe;AAAA,IACjB;AAAA,EACF;AACF;AAMA,MAAM,eAAuC;AAAA,EAC3C,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,SAAS;AAAA,EACT,gBAAgB,CAAC,uBAAuB;AAAA,EACxC,SAAS,CAAC,sBAAsB;AAAA,EAChC,cAAc;AAAA,EACd,eAAe;AAAA,IACb;AAAA,MACE,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,eAAe;AAAA,IACjB;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,eAAe;AAAA,IACjB;AAAA,EACF;AACF;AAMA,MAAM,iBAAyC;AAAA,EAC7C,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,SAAS;AAAA,EACT,gBAAgB,CAAC,kBAAkB;AAAA,EACnC,SAAS,CAAC,iBAAiB;AAAA,EAC3B,cAAc;AAAA,EACd,eAAe;AAAA,IACb;AAAA,MACE,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,eAAe;AAAA,IACjB;AAAA,EACF;AACF;AAMA,MAAM,gBAAwC;AAAA,EAC5C,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,SAAS;AAAA,EACT,gBAAgB,CAAC,iBAAiB;AAAA,EAClC,SAAS,CAAC,gBAAgB;AAAA,EAC1B,cAAc;AAAA,EACd,eAAe;AAAA,IACb;AAAA,MACE,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,eAAe;AAAA,IACjB;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,eAAe;AAAA,MACf,MAAM,CAAC,QAAQ;AAAA,IACjB;AAAA,EACF;AACF;AAQO,MAAM,4BAA+D;AAAA,EAC1E;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;",
6
+ "names": []
7
+ }
package/jest.config.cjs CHANGED
@@ -7,6 +7,7 @@ module.exports = {
7
7
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
8
8
  moduleNameMapper: {
9
9
  '^@open-mercato/ai-assistant/(.*)$': '<rootDir>/src/$1',
10
+ '^@open-mercato/shared/(.*)$': '<rootDir>/../shared/src/$1',
10
11
  },
11
12
  transform: {
12
13
  '^.+\\.tsx?$': [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/ai-assistant",
3
- "version": "0.4.11-develop.2226.2963a4b52d",
3
+ "version": "0.4.11-develop.2231.cfcd603204",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22.0.0"
@@ -98,12 +98,12 @@
98
98
  "zod-to-json-schema": "^3.25.1"
99
99
  },
100
100
  "peerDependencies": {
101
- "@open-mercato/shared": "0.4.11-develop.2226.2963a4b52d",
102
- "@open-mercato/ui": "0.4.11-develop.2226.2963a4b52d",
101
+ "@open-mercato/shared": "0.4.11-develop.2231.cfcd603204",
102
+ "@open-mercato/ui": "0.4.11-develop.2231.cfcd603204",
103
103
  "zod": ">=3.23.0"
104
104
  },
105
105
  "devDependencies": {
106
- "@open-mercato/cli": "0.4.11-develop.2226.2963a4b52d",
106
+ "@open-mercato/cli": "0.4.11-develop.2231.cfcd603204",
107
107
  "tsx": "^4.21.0"
108
108
  },
109
109
  "publishConfig": {
@@ -1,19 +1,11 @@
1
1
  import { NextResponse, type NextRequest } from 'next/server'
2
2
  import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
3
3
  import { generateObject } from '../../lib/ai-sdk'
4
- import {
5
- createOpenAI,
6
- createAnthropic,
7
- createGoogleGenerativeAI,
8
- } from '../../lib/ai-sdk'
9
4
  import { z } from 'zod'
10
5
  import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
11
6
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
12
- import {
13
- resolveFirstConfiguredOpenCodeProvider,
14
- resolveOpenCodeModel,
15
- resolveOpenCodeProviderApiKey,
16
- } from '@open-mercato/shared/lib/ai/opencode-provider'
7
+ import { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'
8
+ import { resolveOpenCodeModel } from '@open-mercato/shared/lib/ai/opencode-provider'
17
9
  import {
18
10
  resolveChatConfig,
19
11
  isProviderConfigured,
@@ -40,39 +32,43 @@ const RouteResultSchema = z.object({
40
32
  })
41
33
 
42
34
  function createRoutingModel(providerId: ChatProviderId, configuredModel?: string) {
43
- const { modelId, modelWithProvider } = resolveOpenCodeModel(providerId, {
44
- overrideModel: configuredModel,
45
- })
46
- const apiKey = resolveOpenCodeProviderApiKey(providerId)
47
- if (!apiKey) {
48
- throw new Error(`${providerId.toUpperCase()} API key not configured`)
35
+ const provider = llmProviderRegistry.get(providerId)
36
+ if (!provider) {
37
+ throw new Error(`Unknown provider: ${providerId}`)
49
38
  }
50
39
 
51
- switch (providerId) {
52
- case 'openai': {
53
- const openai = createOpenAI({ apiKey })
54
- return {
55
- model: openai(modelId) as unknown as Parameters<typeof generateObject>[0]['model'],
56
- modelWithProvider,
57
- }
58
- }
59
- case 'anthropic': {
60
- const anthropic = createAnthropic({ apiKey })
61
- return {
62
- model: anthropic(modelId) as unknown as Parameters<typeof generateObject>[0]['model'],
63
- modelWithProvider,
64
- }
65
- }
66
- case 'google': {
67
- const google = createGoogleGenerativeAI({ apiKey })
68
- return {
69
- model: google(modelId) as unknown as Parameters<typeof generateObject>[0]['model'],
70
- modelWithProvider,
71
- }
72
- }
73
- default:
74
- throw new Error(`Unknown provider: ${providerId}`)
40
+ // resolveOpenCodeModel is still used for token parsing and provider-prefix
41
+ // validation (`openai/gpt-5-mini` vs `anthropic/claude-…`). It falls back
42
+ // to the provider's defaultModel via the opencode-provider facade, which
43
+ // is only populated for the three native providers — if the registry
44
+ // returns a preset-based provider whose id is unknown to opencode-provider,
45
+ // we short-circuit and use the provider's own defaultModel.
46
+ let modelId: string
47
+ let modelWithProvider: string
48
+ try {
49
+ const resolved = resolveOpenCodeModel(providerId as 'anthropic' | 'openai' | 'google', {
50
+ overrideModel: configuredModel,
51
+ })
52
+ modelId = resolved.modelId
53
+ modelWithProvider = resolved.modelWithProvider
54
+ } catch {
55
+ // Preset-based provider or unknown id — fall back to the provider's own
56
+ // model list. The explicit override (if any) wins.
57
+ const requested = (configuredModel ?? '').trim()
58
+ modelId = requested.length > 0 ? requested : provider.defaultModel
59
+ modelWithProvider = `${providerId}/${modelId}`
75
60
  }
61
+
62
+ const apiKey = provider.resolveApiKey()
63
+ if (!apiKey) {
64
+ const envKey = provider.getConfiguredEnvKey()
65
+ throw new Error(`${envKey} not configured for provider "${providerId}"`)
66
+ }
67
+
68
+ const model = provider.createModel({ modelId, apiKey }) as unknown as Parameters<
69
+ typeof generateObject
70
+ >[0]['model']
71
+ return { model, modelWithProvider }
76
72
  }
77
73
 
78
74
  export async function POST(req: NextRequest) {
@@ -104,16 +100,23 @@ export async function POST(req: NextRequest) {
104
100
  const container = await createRequestContainer()
105
101
  let config = await resolveChatConfig(container)
106
102
 
107
- // Fallback to first configured provider
103
+ // Fallback to first configured provider from the LLM provider registry.
104
+ // Default walk order prioritizes the native adapters (backward compatible)
105
+ // before OpenAI-compatible presets.
108
106
  if (!config) {
109
- const configuredProvider = resolveFirstConfiguredOpenCodeProvider()
110
- if (!configuredProvider) {
107
+ const picked = llmProviderRegistry.resolveFirstConfigured({
108
+ order: ['anthropic', 'openai', 'google'],
109
+ })
110
+ if (!picked) {
111
111
  return NextResponse.json(
112
- { error: 'No AI provider configured. Please set an API key for OpenAI, Anthropic, or Google.' },
113
- { status: 503 }
112
+ {
113
+ error:
114
+ 'No AI provider configured. Please set an API key for one of the registered providers (Anthropic, OpenAI, Google, DeepInfra, Groq, …).',
115
+ },
116
+ { status: 503 },
114
117
  )
115
118
  }
116
- config = { providerId: configuredProvider, model: '', updatedAt: '' }
119
+ config = { providerId: picked.id, model: '', updatedAt: '' }
117
120
  }
118
121
 
119
122
  console.log('[AI Route] Using provider:', config.providerId)
@@ -0,0 +1,72 @@
1
+ import { createAnthropicAdapter } from '../llm-adapters/anthropic'
2
+
3
+ describe('AnthropicAdapter', () => {
4
+ const adapter = createAnthropicAdapter()
5
+
6
+ it('has expected id, name, envKeys and defaultModel', () => {
7
+ expect(adapter.id).toBe('anthropic')
8
+ expect(adapter.name).toBe('Anthropic')
9
+ expect(adapter.envKeys).toEqual(['ANTHROPIC_API_KEY', 'OPENCODE_ANTHROPIC_API_KEY'])
10
+ expect(adapter.defaultModel).toBe('claude-haiku-4-5-20251001')
11
+ expect(adapter.defaultModels.length).toBeGreaterThan(0)
12
+ expect(adapter.defaultModels[0].id).toBe('claude-haiku-4-5-20251001')
13
+ })
14
+
15
+ it('detects configuration from env', () => {
16
+ expect(
17
+ adapter.isConfigured({ ANTHROPIC_API_KEY: 'sk-ant-key' }),
18
+ ).toBe(true)
19
+ expect(adapter.isConfigured({ ANTHROPIC_API_KEY: '' })).toBe(false)
20
+ expect(adapter.isConfigured({ ANTHROPIC_API_KEY: ' ' })).toBe(false)
21
+ expect(adapter.isConfigured({})).toBe(false)
22
+ })
23
+
24
+ it('detects configuration from OPENCODE_* fallback env', () => {
25
+ expect(
26
+ adapter.isConfigured({ OPENCODE_ANTHROPIC_API_KEY: 'sk-ant-key' }),
27
+ ).toBe(true)
28
+ expect(
29
+ adapter.isConfigured({ OPENCODE_ANTHROPIC_API_KEY: '' }),
30
+ ).toBe(false)
31
+ })
32
+
33
+ it('resolves API key from env', () => {
34
+ expect(
35
+ adapter.resolveApiKey({ ANTHROPIC_API_KEY: 'sk-ant-key' }),
36
+ ).toBe('sk-ant-key')
37
+ expect(
38
+ adapter.resolveApiKey({ ANTHROPIC_API_KEY: ' sk-ant-key ' }),
39
+ ).toBe('sk-ant-key')
40
+ expect(adapter.resolveApiKey({ ANTHROPIC_API_KEY: '' })).toBeNull()
41
+ expect(adapter.resolveApiKey({})).toBeNull()
42
+ })
43
+
44
+ it('resolves API key from OPENCODE_* fallback env', () => {
45
+ expect(
46
+ adapter.resolveApiKey({ OPENCODE_ANTHROPIC_API_KEY: 'opencode-key' }),
47
+ ).toBe('opencode-key')
48
+ expect(
49
+ adapter.resolveApiKey({
50
+ ANTHROPIC_API_KEY: 'primary',
51
+ OPENCODE_ANTHROPIC_API_KEY: 'fallback',
52
+ }),
53
+ ).toBe('primary')
54
+ })
55
+
56
+ it('returns the configured env key name for diagnostics', () => {
57
+ expect(
58
+ adapter.getConfiguredEnvKey({ ANTHROPIC_API_KEY: 'sk-ant' }),
59
+ ).toBe('ANTHROPIC_API_KEY')
60
+ // Falls back to first declared key when none configured.
61
+ expect(adapter.getConfiguredEnvKey({})).toBe('ANTHROPIC_API_KEY')
62
+ })
63
+
64
+ it('createModel returns a non-null AI SDK model instance', () => {
65
+ const model = adapter.createModel({
66
+ apiKey: 'sk-ant-test',
67
+ modelId: 'claude-haiku-4-5-20251001',
68
+ })
69
+ expect(model).toBeDefined()
70
+ expect(model).not.toBeNull()
71
+ })
72
+ })