@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.
- package/README.md +69 -9
- package/dist/modules/ai_assistant/api/route/route.js +32 -44
- package/dist/modules/ai_assistant/api/route/route.js.map +2 -2
- package/dist/modules/ai_assistant/lib/ai-sdk.js +1 -0
- package/dist/modules/ai_assistant/lib/ai-sdk.js.map +2 -2
- package/dist/modules/ai_assistant/lib/chat-config.js +52 -40
- package/dist/modules/ai_assistant/lib/chat-config.js.map +2 -2
- package/dist/modules/ai_assistant/lib/llm-adapters/anthropic.js +65 -0
- package/dist/modules/ai_assistant/lib/llm-adapters/anthropic.js.map +7 -0
- package/dist/modules/ai_assistant/lib/llm-adapters/google.js +59 -0
- package/dist/modules/ai_assistant/lib/llm-adapters/google.js.map +7 -0
- package/dist/modules/ai_assistant/lib/llm-adapters/openai.js +65 -0
- package/dist/modules/ai_assistant/lib/llm-adapters/openai.js.map +7 -0
- package/dist/modules/ai_assistant/lib/llm-bootstrap.js +47 -0
- package/dist/modules/ai_assistant/lib/llm-bootstrap.js.map +7 -0
- package/dist/modules/ai_assistant/lib/openai-compatible-presets.js +203 -0
- package/dist/modules/ai_assistant/lib/openai-compatible-presets.js.map +7 -0
- package/jest.config.cjs +1 -0
- package/package.json +4 -4
- package/src/modules/ai_assistant/api/route/route.ts +49 -46
- package/src/modules/ai_assistant/lib/__tests__/llm-adapters-anthropic.test.ts +72 -0
- package/src/modules/ai_assistant/lib/__tests__/llm-adapters-google.test.ts +71 -0
- package/src/modules/ai_assistant/lib/__tests__/llm-adapters-openai.test.ts +160 -0
- package/src/modules/ai_assistant/lib/__tests__/llm-bootstrap.test.ts +81 -0
- package/src/modules/ai_assistant/lib/ai-sdk.ts +10 -0
- package/src/modules/ai_assistant/lib/chat-config.ts +75 -42
- package/src/modules/ai_assistant/lib/llm-adapters/anthropic.ts +91 -0
- package/src/modules/ai_assistant/lib/llm-adapters/google.ts +80 -0
- package/src/modules/ai_assistant/lib/llm-adapters/openai.ts +145 -0
- package/src/modules/ai_assistant/lib/llm-bootstrap.ts +80 -0
- 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": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/jest.config.cjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/ai-assistant",
|
|
3
|
-
"version": "0.4.11-develop.
|
|
3
|
+
"version": "0.4.11-develop.2229.54cd746c6e",
|
|
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.
|
|
102
|
-
"@open-mercato/ui": "0.4.11-develop.
|
|
101
|
+
"@open-mercato/shared": "0.4.11-develop.2229.54cd746c6e",
|
|
102
|
+
"@open-mercato/ui": "0.4.11-develop.2229.54cd746c6e",
|
|
103
103
|
"zod": ">=3.23.0"
|
|
104
104
|
},
|
|
105
105
|
"devDependencies": {
|
|
106
|
-
"@open-mercato/cli": "0.4.11-develop.
|
|
106
|
+
"@open-mercato/cli": "0.4.11-develop.2229.54cd746c6e",
|
|
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
|
-
|
|
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
|
|
44
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
110
|
-
|
|
107
|
+
const picked = llmProviderRegistry.resolveFirstConfigured({
|
|
108
|
+
order: ['anthropic', 'openai', 'google'],
|
|
109
|
+
})
|
|
110
|
+
if (!picked) {
|
|
111
111
|
return NextResponse.json(
|
|
112
|
-
{
|
|
113
|
-
|
|
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:
|
|
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
|
+
})
|