ada-agent 0.2.0 → 0.3.0

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 (39) hide show
  1. package/README.md +262 -263
  2. package/bench/README.md +88 -88
  3. package/bench/swebench.mjs +242 -242
  4. package/docs/architecture.md +163 -163
  5. package/docs/architecture.svg +73 -73
  6. package/docs/cloudflare.md +81 -81
  7. package/docs/connectors.md +49 -49
  8. package/docs/integrations.md +62 -62
  9. package/package.json +66 -65
  10. package/skills/aesthetic-direction/SKILL.md +24 -24
  11. package/skills/color-palette/SKILL.md +24 -24
  12. package/skills/component-library/SKILL.md +23 -23
  13. package/skills/dark-mode/SKILL.md +24 -24
  14. package/skills/dashboard-ui/SKILL.md +23 -23
  15. package/skills/design-system/SKILL.md +24 -24
  16. package/skills/design-tokens/SKILL.md +24 -24
  17. package/skills/empty-states/SKILL.md +23 -23
  18. package/skills/hero-section/SKILL.md +23 -23
  19. package/skills/micro-interactions/SKILL.md +23 -23
  20. package/skills/motion-design/SKILL.md +23 -23
  21. package/skills/page-transitions/SKILL.md +23 -23
  22. package/skills/pricing-page/SKILL.md +23 -23
  23. package/skills/scroll-animation/SKILL.md +23 -23
  24. package/skills/skeleton-loader/SKILL.md +23 -23
  25. package/skills/tailwind-theme/SKILL.md +24 -24
  26. package/skills/typography/SKILL.md +24 -24
  27. package/skills/ui-polish/SKILL.md +24 -24
  28. package/skills/ui-review/SKILL.md +24 -24
  29. package/skills/web-fonts/SKILL.md +24 -24
  30. package/src/client/autostart.ts +93 -0
  31. package/src/client/catalog.json +1 -1
  32. package/src/client/cli.ts +1275 -1262
  33. package/src/client/models-dev.ts +106 -106
  34. package/src/selfcheck.ts +404 -390
  35. package/src/server/config.ts +65 -65
  36. package/src/server/providers/openai-compat.ts +78 -78
  37. package/src/server/providers/registry.ts +32 -32
  38. package/src/server/router.ts +33 -33
  39. package/src/shared/types.ts +21 -21
@@ -1,65 +1,65 @@
1
- // Backend configuration: provider upstreams, keys, client-key auth, port.
2
- // Everything is env-driven. The backend is the only place provider keys live.
3
-
4
- import { getCredential } from "./credentials.ts";
5
- import type { ProviderName } from "../shared/types.ts";
6
-
7
- export interface ProviderDef {
8
- baseURL: string; // OpenAI-compatible base (…/v1) — every provider is proxied as-is
9
- keyEnv: string; // env var holding this provider's key ("" = keyless, e.g. local Ollama)
10
- }
11
-
12
- export const PROVIDERS: Record<ProviderName, ProviderDef> = {
13
- openai: { baseURL: "https://api.openai.com/v1", keyEnv: "OPENAI_API_KEY" },
14
- anthropic: { baseURL: "https://api.anthropic.com/v1", keyEnv: "ANTHROPIC_API_KEY" },
15
- google: { baseURL: "https://generativelanguage.googleapis.com/v1beta/openai", keyEnv: "GEMINI_API_KEY" },
16
- mistral: { baseURL: "https://api.mistral.ai/v1", keyEnv: "MISTRAL_API_KEY" },
17
- openrouter: { baseURL: "https://openrouter.ai/api/v1", keyEnv: "OPENROUTER_API_KEY" },
18
- groq: { baseURL: "https://api.groq.com/openai/v1", keyEnv: "GROQ_API_KEY" },
19
- deepseek: { baseURL: "https://api.deepseek.com", keyEnv: "DEEPSEEK_API_KEY" },
20
- together: { baseURL: "https://api.together.xyz/v1", keyEnv: "TOGETHER_API_KEY" },
21
- xai: { baseURL: "https://api.x.ai/v1", keyEnv: "XAI_API_KEY" },
22
- dashscope: {
23
- baseURL: process.env.DASHSCOPE_BASE_URL ?? "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
24
- keyEnv: "DASHSCOPE_API_KEY",
25
- },
26
- // GitHub Copilot — OpenAI-compatible chat endpoint. COPILOT_API_KEY must be a Copilot *bearer*
27
- // token (exchanged from a GitHub OAuth token at /copilot_internal/v2/token — that exchange is not
28
- // implemented here; it needs a Copilot subscription). Required headers are added in the adapter.
29
- copilot: { baseURL: process.env.COPILOT_BASE_URL ?? "https://api.githubcopilot.com", keyEnv: "COPILOT_API_KEY" },
30
- // Cloudflare Workers AI / AI Gateway — OpenAI-compatible. Workers AI: set CLOUDFLARE_ACCOUNT_ID +
31
- // CLOUDFLARE_API_TOKEN (default URL). AI Gateway: point CLOUDFLARE_BASE_URL at the gateway URL.
32
- // Model ids are `@cf/<vendor>/<model>` (e.g. @cf/moonshotai/kimi-k2.7-code) — sent through as-is.
33
- cloudflare: {
34
- baseURL: process.env.CLOUDFLARE_BASE_URL ?? `https://api.cloudflare.com/client/v4/accounts/${process.env.CLOUDFLARE_ACCOUNT_ID ?? ""}/ai/v1`,
35
- keyEnv: "CLOUDFLARE_API_TOKEN",
36
- },
37
- ollama: { baseURL: process.env.OLLAMA_BASE_URL ?? "http://localhost:11434/v1", keyEnv: "" },
38
- };
39
-
40
- export const PORT = Number(process.env.ADA_PORT) || 8787;
41
-
42
- /** The ada client keys allowed to use this backend. null = auth disabled (dev mode). */
43
- export function clientKeys(): string[] | null {
44
- const v = process.env.ADA_CLIENT_KEYS;
45
- if (!v) return null;
46
- return v.split(",").map((s) => s.trim()).filter(Boolean);
47
- }
48
-
49
- /** The upstream provider key: env var first, then a stored credential (API key or OAuth token). */
50
- export function providerKey(p: ProviderName): string | undefined {
51
- const env = PROVIDERS[p].keyEnv;
52
- if (env && process.env[env]) return process.env[env];
53
- const cred = getCredential(p);
54
- if (cred) return cred.type === "oauth" ? cred.access : cred.key;
55
- return undefined; // keyless provider (Ollama) or unconfigured
56
- }
57
-
58
- /** A provider is usable if it's keyless, its key env var is set, or a credential is stored. */
59
- export function isConfigured(p: ProviderName): boolean {
60
- return PROVIDERS[p].keyEnv === "" || !!process.env[PROVIDERS[p].keyEnv] || !!getCredential(p);
61
- }
62
-
63
- export function configuredProviders(): ProviderName[] {
64
- return (Object.keys(PROVIDERS) as ProviderName[]).filter(isConfigured);
65
- }
1
+ // Backend configuration: provider upstreams, keys, client-key auth, port.
2
+ // Everything is env-driven. The backend is the only place provider keys live.
3
+
4
+ import { getCredential } from "./credentials.ts";
5
+ import type { ProviderName } from "../shared/types.ts";
6
+
7
+ export interface ProviderDef {
8
+ baseURL: string; // OpenAI-compatible base (…/v1) — every provider is proxied as-is
9
+ keyEnv: string; // env var holding this provider's key ("" = keyless, e.g. local Ollama)
10
+ }
11
+
12
+ export const PROVIDERS: Record<ProviderName, ProviderDef> = {
13
+ openai: { baseURL: "https://api.openai.com/v1", keyEnv: "OPENAI_API_KEY" },
14
+ anthropic: { baseURL: "https://api.anthropic.com/v1", keyEnv: "ANTHROPIC_API_KEY" },
15
+ google: { baseURL: "https://generativelanguage.googleapis.com/v1beta/openai", keyEnv: "GEMINI_API_KEY" },
16
+ mistral: { baseURL: "https://api.mistral.ai/v1", keyEnv: "MISTRAL_API_KEY" },
17
+ openrouter: { baseURL: "https://openrouter.ai/api/v1", keyEnv: "OPENROUTER_API_KEY" },
18
+ groq: { baseURL: "https://api.groq.com/openai/v1", keyEnv: "GROQ_API_KEY" },
19
+ deepseek: { baseURL: "https://api.deepseek.com", keyEnv: "DEEPSEEK_API_KEY" },
20
+ together: { baseURL: "https://api.together.xyz/v1", keyEnv: "TOGETHER_API_KEY" },
21
+ xai: { baseURL: "https://api.x.ai/v1", keyEnv: "XAI_API_KEY" },
22
+ dashscope: {
23
+ baseURL: process.env.DASHSCOPE_BASE_URL ?? "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
24
+ keyEnv: "DASHSCOPE_API_KEY",
25
+ },
26
+ // GitHub Copilot — OpenAI-compatible chat endpoint. COPILOT_API_KEY must be a Copilot *bearer*
27
+ // token (exchanged from a GitHub OAuth token at /copilot_internal/v2/token — that exchange is not
28
+ // implemented here; it needs a Copilot subscription). Required headers are added in the adapter.
29
+ copilot: { baseURL: process.env.COPILOT_BASE_URL ?? "https://api.githubcopilot.com", keyEnv: "COPILOT_API_KEY" },
30
+ // Cloudflare Workers AI / AI Gateway — OpenAI-compatible. Workers AI: set CLOUDFLARE_ACCOUNT_ID +
31
+ // CLOUDFLARE_API_TOKEN (default URL). AI Gateway: point CLOUDFLARE_BASE_URL at the gateway URL.
32
+ // Model ids are `@cf/<vendor>/<model>` (e.g. @cf/moonshotai/kimi-k2.7-code) — sent through as-is.
33
+ cloudflare: {
34
+ baseURL: process.env.CLOUDFLARE_BASE_URL ?? `https://api.cloudflare.com/client/v4/accounts/${process.env.CLOUDFLARE_ACCOUNT_ID ?? ""}/ai/v1`,
35
+ keyEnv: "CLOUDFLARE_API_TOKEN",
36
+ },
37
+ ollama: { baseURL: process.env.OLLAMA_BASE_URL ?? "http://localhost:11434/v1", keyEnv: "" },
38
+ };
39
+
40
+ export const PORT = Number(process.env.ADA_PORT) || 8787;
41
+
42
+ /** The ada client keys allowed to use this backend. null = auth disabled (dev mode). */
43
+ export function clientKeys(): string[] | null {
44
+ const v = process.env.ADA_CLIENT_KEYS;
45
+ if (!v) return null;
46
+ return v.split(",").map((s) => s.trim()).filter(Boolean);
47
+ }
48
+
49
+ /** The upstream provider key: env var first, then a stored credential (API key or OAuth token). */
50
+ export function providerKey(p: ProviderName): string | undefined {
51
+ const env = PROVIDERS[p].keyEnv;
52
+ if (env && process.env[env]) return process.env[env];
53
+ const cred = getCredential(p);
54
+ if (cred) return cred.type === "oauth" ? cred.access : cred.key;
55
+ return undefined; // keyless provider (Ollama) or unconfigured
56
+ }
57
+
58
+ /** A provider is usable if it's keyless, its key env var is set, or a credential is stored. */
59
+ export function isConfigured(p: ProviderName): boolean {
60
+ return PROVIDERS[p].keyEnv === "" || !!process.env[PROVIDERS[p].keyEnv] || !!getCredential(p);
61
+ }
62
+
63
+ export function configuredProviders(): ProviderName[] {
64
+ return (Object.keys(PROVIDERS) as ProviderName[]).filter(isConfigured);
65
+ }
@@ -1,78 +1,78 @@
1
- // OpenAI-compatible adapter. Covers every provider that speaks the OpenAI Chat
2
- // Completions format: OpenAI, Mistral, Groq, DeepSeek, xAI, OpenRouter, Together, Ollama,
3
- // and Gemini (via Google's OpenAI-compatible endpoint). Because the client also speaks
4
- // that format, this adapter just swaps in the upstream base URL + key and streams the
5
- // response straight back — no translation needed.
6
-
7
- import type { ProviderName } from "../../shared/types.ts";
8
- import { PROVIDERS, providerKey } from "../config.ts";
9
- import { SSE_HEADERS } from "../sse.ts";
10
- import type { Adapter, ChatRequest } from "./adapter.ts";
11
-
12
- function authHeaders(provider: ProviderName): Record<string, string> {
13
- const key = providerKey(provider);
14
- const base: Record<string, string> = key ? { authorization: `Bearer ${key}` } : {};
15
- // GitHub Copilot's endpoint requires these editor-identification headers.
16
- if (provider === "copilot") return { ...base, "Copilot-Integration-Id": "vscode-chat", "Editor-Version": "ada/0.0.1", "Editor-Plugin-Version": "ada/0.0.1" };
17
- return base;
18
- }
19
-
20
- export const openAICompatAdapter: Adapter = {
21
- async chat({ provider, body, res }: ChatRequest): Promise<void> {
22
- const def = PROVIDERS[provider];
23
- // Strip a leading "<provider>/" the router used only to disambiguate (copilot/groq/together) — the
24
- // endpoint wants the bare id. (Cloudflare's "@cf/…" ids aren't "cloudflare/…", so they pass through.)
25
- const prefix = `${provider}/`;
26
- const outBody = typeof body.model === "string" && body.model.startsWith(prefix) ? { ...body, model: body.model.slice(prefix.length) } : body;
27
- let upstream: Awaited<ReturnType<typeof fetch>>;
28
- try {
29
- upstream = await fetch(`${def.baseURL}/chat/completions`, {
30
- method: "POST",
31
- headers: { "content-type": "application/json", ...authHeaders(provider) },
32
- body: JSON.stringify(outBody),
33
- });
34
- } catch (e) {
35
- res.writeHead(502, { "content-type": "application/json" });
36
- res.end(
37
- JSON.stringify({
38
- error: { message: `could not reach ${provider} upstream at ${def.baseURL}: ${e instanceof Error ? e.message : String(e)}` },
39
- }),
40
- );
41
- return;
42
- }
43
-
44
- if (!upstream.ok || !upstream.body) {
45
- const text = await upstream.text().catch(() => "");
46
- res.writeHead(upstream.status || 502, { "content-type": "application/json" });
47
- res.end(text || JSON.stringify({ error: { message: `upstream error ${upstream.status}` } }));
48
- return;
49
- }
50
-
51
- if (body.stream) {
52
- res.writeHead(200, SSE_HEADERS);
53
- const reader = upstream.body.getReader();
54
- for (;;) {
55
- const { done, value } = await reader.read();
56
- if (done) break;
57
- if (value) res.write(Buffer.from(value));
58
- }
59
- res.end();
60
- } else {
61
- const text = await upstream.text();
62
- res.writeHead(upstream.status, { "content-type": upstream.headers.get("content-type") ?? "application/json" });
63
- res.end(text);
64
- }
65
- },
66
-
67
- async listModels(provider: ProviderName): Promise<string[]> {
68
- const def = PROVIDERS[provider];
69
- try {
70
- const r = await fetch(`${def.baseURL}/models`, { headers: authHeaders(provider) });
71
- if (!r.ok) return [];
72
- const j = (await r.json()) as { data?: Array<{ id?: unknown }> };
73
- return (j.data ?? []).map((m) => m.id).filter((x): x is string => typeof x === "string");
74
- } catch {
75
- return [];
76
- }
77
- },
78
- };
1
+ // OpenAI-compatible adapter. Covers every provider that speaks the OpenAI Chat
2
+ // Completions format: OpenAI, Mistral, Groq, DeepSeek, xAI, OpenRouter, Together, Ollama,
3
+ // and Gemini (via Google's OpenAI-compatible endpoint). Because the client also speaks
4
+ // that format, this adapter just swaps in the upstream base URL + key and streams the
5
+ // response straight back — no translation needed.
6
+
7
+ import type { ProviderName } from "../../shared/types.ts";
8
+ import { PROVIDERS, providerKey } from "../config.ts";
9
+ import { SSE_HEADERS } from "../sse.ts";
10
+ import type { Adapter, ChatRequest } from "./adapter.ts";
11
+
12
+ function authHeaders(provider: ProviderName): Record<string, string> {
13
+ const key = providerKey(provider);
14
+ const base: Record<string, string> = key ? { authorization: `Bearer ${key}` } : {};
15
+ // GitHub Copilot's endpoint requires these editor-identification headers.
16
+ if (provider === "copilot") return { ...base, "Copilot-Integration-Id": "vscode-chat", "Editor-Version": "ada/0.0.1", "Editor-Plugin-Version": "ada/0.0.1" };
17
+ return base;
18
+ }
19
+
20
+ export const openAICompatAdapter: Adapter = {
21
+ async chat({ provider, body, res }: ChatRequest): Promise<void> {
22
+ const def = PROVIDERS[provider];
23
+ // Strip a leading "<provider>/" the router used only to disambiguate (copilot/groq/together) — the
24
+ // endpoint wants the bare id. (Cloudflare's "@cf/…" ids aren't "cloudflare/…", so they pass through.)
25
+ const prefix = `${provider}/`;
26
+ const outBody = typeof body.model === "string" && body.model.startsWith(prefix) ? { ...body, model: body.model.slice(prefix.length) } : body;
27
+ let upstream: Awaited<ReturnType<typeof fetch>>;
28
+ try {
29
+ upstream = await fetch(`${def.baseURL}/chat/completions`, {
30
+ method: "POST",
31
+ headers: { "content-type": "application/json", ...authHeaders(provider) },
32
+ body: JSON.stringify(outBody),
33
+ });
34
+ } catch (e) {
35
+ res.writeHead(502, { "content-type": "application/json" });
36
+ res.end(
37
+ JSON.stringify({
38
+ error: { message: `could not reach ${provider} upstream at ${def.baseURL}: ${e instanceof Error ? e.message : String(e)}` },
39
+ }),
40
+ );
41
+ return;
42
+ }
43
+
44
+ if (!upstream.ok || !upstream.body) {
45
+ const text = await upstream.text().catch(() => "");
46
+ res.writeHead(upstream.status || 502, { "content-type": "application/json" });
47
+ res.end(text || JSON.stringify({ error: { message: `upstream error ${upstream.status}` } }));
48
+ return;
49
+ }
50
+
51
+ if (body.stream) {
52
+ res.writeHead(200, SSE_HEADERS);
53
+ const reader = upstream.body.getReader();
54
+ for (;;) {
55
+ const { done, value } = await reader.read();
56
+ if (done) break;
57
+ if (value) res.write(Buffer.from(value));
58
+ }
59
+ res.end();
60
+ } else {
61
+ const text = await upstream.text();
62
+ res.writeHead(upstream.status, { "content-type": upstream.headers.get("content-type") ?? "application/json" });
63
+ res.end(text);
64
+ }
65
+ },
66
+
67
+ async listModels(provider: ProviderName): Promise<string[]> {
68
+ const def = PROVIDERS[provider];
69
+ try {
70
+ const r = await fetch(`${def.baseURL}/models`, { headers: authHeaders(provider) });
71
+ if (!r.ok) return [];
72
+ const j = (await r.json()) as { data?: Array<{ id?: unknown }> };
73
+ return (j.data ?? []).map((m) => m.id).filter((x): x is string => typeof x === "string");
74
+ } catch {
75
+ return [];
76
+ }
77
+ },
78
+ };
@@ -1,32 +1,32 @@
1
- // Provider → adapter map. This table is the whole routing story at a glance:
2
- // who shares the OpenAI-compatible adapter, and who has a dedicated one.
3
- //
4
- // Adding support is obvious from here:
5
- // - new model on an existing provider → nothing to change
6
- // - new OpenAI-compatible provider → add it in config.ts + a line below
7
- // - new provider with a divergent format → write an adapter, map it below
8
-
9
- import type { ProviderName } from "../../shared/types.ts";
10
- import type { Adapter } from "./adapter.ts";
11
- import { anthropicAdapter } from "./anthropic.ts";
12
- import { openAICompatAdapter } from "./openai-compat.ts";
13
-
14
- const ADAPTERS: Record<ProviderName, Adapter> = {
15
- anthropic: anthropicAdapter, // native: Anthropic Messages API
16
- openai: openAICompatAdapter,
17
- google: openAICompatAdapter, // via Google's OpenAI-compatible endpoint
18
- mistral: openAICompatAdapter,
19
- openrouter: openAICompatAdapter,
20
- groq: openAICompatAdapter,
21
- deepseek: openAICompatAdapter,
22
- together: openAICompatAdapter,
23
- xai: openAICompatAdapter,
24
- dashscope: openAICompatAdapter, // Alibaba Qwen via DashScope's OpenAI-compatible endpoint
25
- copilot: openAICompatAdapter, // GitHub Copilot's OpenAI-compatible endpoint (+ custom headers in the adapter)
26
- cloudflare: openAICompatAdapter, // Cloudflare Workers AI / AI Gateway (OpenAI-compatible)
27
- ollama: openAICompatAdapter,
28
- };
29
-
30
- export function adapterFor(provider: ProviderName): Adapter {
31
- return ADAPTERS[provider];
32
- }
1
+ // Provider → adapter map. This table is the whole routing story at a glance:
2
+ // who shares the OpenAI-compatible adapter, and who has a dedicated one.
3
+ //
4
+ // Adding support is obvious from here:
5
+ // - new model on an existing provider → nothing to change
6
+ // - new OpenAI-compatible provider → add it in config.ts + a line below
7
+ // - new provider with a divergent format → write an adapter, map it below
8
+
9
+ import type { ProviderName } from "../../shared/types.ts";
10
+ import type { Adapter } from "./adapter.ts";
11
+ import { anthropicAdapter } from "./anthropic.ts";
12
+ import { openAICompatAdapter } from "./openai-compat.ts";
13
+
14
+ const ADAPTERS: Record<ProviderName, Adapter> = {
15
+ anthropic: anthropicAdapter, // native: Anthropic Messages API
16
+ openai: openAICompatAdapter,
17
+ google: openAICompatAdapter, // via Google's OpenAI-compatible endpoint
18
+ mistral: openAICompatAdapter,
19
+ openrouter: openAICompatAdapter,
20
+ groq: openAICompatAdapter,
21
+ deepseek: openAICompatAdapter,
22
+ together: openAICompatAdapter,
23
+ xai: openAICompatAdapter,
24
+ dashscope: openAICompatAdapter, // Alibaba Qwen via DashScope's OpenAI-compatible endpoint
25
+ copilot: openAICompatAdapter, // GitHub Copilot's OpenAI-compatible endpoint (+ custom headers in the adapter)
26
+ cloudflare: openAICompatAdapter, // Cloudflare Workers AI / AI Gateway (OpenAI-compatible)
27
+ ollama: openAICompatAdapter,
28
+ };
29
+
30
+ export function adapterFor(provider: ProviderName): Adapter {
31
+ return ADAPTERS[provider];
32
+ }
@@ -1,33 +1,33 @@
1
- // Map a model id (and optional explicit provider) to a provider.
2
- // Order matters: explicit wins; then the shape of the id (namespaced / local); then base-name prefixes.
3
-
4
- import type { ProviderName } from "../shared/types.ts";
5
- import { PROVIDERS } from "./config.ts";
6
-
7
- export function route(model: string, explicit?: string): ProviderName {
8
- if (explicit && explicit in PROVIDERS) return explicit as ProviderName;
9
-
10
- const m = model.toLowerCase();
11
-
12
- // "vendor/model" → OpenRouter's namespacing convention. Checked before base-name prefixes
13
- // so e.g. "mistralai/…" routes to OpenRouter, not the Mistral API.
14
- // Prefixed ids that must beat the OpenRouter "/" rule below:
15
- if (m.startsWith("@cf/")) return "cloudflare"; // Cloudflare Workers AI model ids
16
- if (m.startsWith("copilot/")) return "copilot";
17
- // `groq/…` / `together/…` disambiguate shared model names (llama-3.3, gemma2…) that no prefix can.
18
- if (m.startsWith("groq/")) return "groq";
19
- if (m.startsWith("together/")) return "together";
20
- if (m.includes("/")) return "openrouter";
21
- // "model:tag" → a local Ollama model (e.g. gemma4:latest).
22
- if (m.includes(":")) return "ollama";
23
-
24
- if (/^(gpt|o1|o3|o4|chatgpt|text-|davinci)/.test(m)) return "openai";
25
- if (m.startsWith("claude")) return "anthropic";
26
- if (m.startsWith("gemini") || m.startsWith("gemma")) return "google";
27
- if (/^(mistral|codestral|magistral|ministral|devstral|pixtral|open-mi)/.test(m)) return "mistral";
28
- if (m.startsWith("grok")) return "xai";
29
- if (m.startsWith("deepseek")) return "deepseek";
30
- if (m.startsWith("qwen") || m.startsWith("qwq")) return "dashscope";
31
-
32
- return "openrouter"; // default: one key, every model
33
- }
1
+ // Map a model id (and optional explicit provider) to a provider.
2
+ // Order matters: explicit wins; then the shape of the id (namespaced / local); then base-name prefixes.
3
+
4
+ import type { ProviderName } from "../shared/types.ts";
5
+ import { PROVIDERS } from "./config.ts";
6
+
7
+ export function route(model: string, explicit?: string): ProviderName {
8
+ if (explicit && explicit in PROVIDERS) return explicit as ProviderName;
9
+
10
+ const m = model.toLowerCase();
11
+
12
+ // "vendor/model" → OpenRouter's namespacing convention. Checked before base-name prefixes
13
+ // so e.g. "mistralai/…" routes to OpenRouter, not the Mistral API.
14
+ // Prefixed ids that must beat the OpenRouter "/" rule below:
15
+ if (m.startsWith("@cf/")) return "cloudflare"; // Cloudflare Workers AI model ids
16
+ if (m.startsWith("copilot/")) return "copilot";
17
+ // `groq/…` / `together/…` disambiguate shared model names (llama-3.3, gemma2…) that no prefix can.
18
+ if (m.startsWith("groq/")) return "groq";
19
+ if (m.startsWith("together/")) return "together";
20
+ if (m.includes("/")) return "openrouter";
21
+ // "model:tag" → a local Ollama model (e.g. gemma4:latest).
22
+ if (m.includes(":")) return "ollama";
23
+
24
+ if (/^(gpt|o1|o3|o4|chatgpt|text-|davinci)/.test(m)) return "openai";
25
+ if (m.startsWith("claude")) return "anthropic";
26
+ if (m.startsWith("gemini") || m.startsWith("gemma")) return "google";
27
+ if (/^(mistral|codestral|magistral|ministral|devstral|pixtral|open-mi)/.test(m)) return "mistral";
28
+ if (m.startsWith("grok")) return "xai";
29
+ if (m.startsWith("deepseek")) return "deepseek";
30
+ if (m.startsWith("qwen") || m.startsWith("qwq")) return "dashscope";
31
+
32
+ return "openrouter"; // default: one key, every model
33
+ }
@@ -1,21 +1,21 @@
1
- // Types shared between the ada client and backend.
2
-
3
- export type ProviderName =
4
- | "openai"
5
- | "anthropic"
6
- | "google"
7
- | "mistral"
8
- | "openrouter"
9
- | "groq"
10
- | "deepseek"
11
- | "together"
12
- | "xai"
13
- | "dashscope"
14
- | "copilot"
15
- | "cloudflare"
16
- | "ollama";
17
-
18
- export interface ModelInfo {
19
- id: string;
20
- provider: ProviderName;
21
- }
1
+ // Types shared between the ada client and backend.
2
+
3
+ export type ProviderName =
4
+ | "openai"
5
+ | "anthropic"
6
+ | "google"
7
+ | "mistral"
8
+ | "openrouter"
9
+ | "groq"
10
+ | "deepseek"
11
+ | "together"
12
+ | "xai"
13
+ | "dashscope"
14
+ | "copilot"
15
+ | "cloudflare"
16
+ | "ollama";
17
+
18
+ export interface ModelInfo {
19
+ id: string;
20
+ provider: ProviderName;
21
+ }