Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion agent-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,8 +309,9 @@ const plan = await agent.plan('compare pricing across 5 CDN providers')
| Anthropic Claude | `{ provider: 'anthropic', model: 'claude-sonnet-4-6' }` |
| OpenAI | `{ provider: 'openai', model: 'gpt-5.4' }` |
| Custom (OpenAI-compat) | `{ provider: 'custom-openai', model: 'your-model', baseURL: '...' }` |
| Atomic Chat (local) | `{ provider: 'atomic-chat', model: 'your-model-id' }` — defaults to `http://127.0.0.1:1337/v1`, keyless |

Set API keys via `apiKeys` option or environment variables (`GOOGLE_GENERATIVE_AI_API_KEY`, `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`).
Set API keys via `apiKeys` option or environment variables (`GOOGLE_GENERATIVE_AI_API_KEY`, `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`). Atomic Chat is keyless by default; override with `ATOMIC_CHAT_BASE_URL` / `ATOMIC_CHAT_API_KEY` if needed.

## OpenAPI spec

Expand Down
2 changes: 1 addition & 1 deletion agent-core/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ components:
properties:
provider:
type: string
enum: [anthropic, openai, google, gateway, custom-openai]
enum: [anthropic, openai, google, gateway, custom-openai, atomic-chat]
description: LLM provider.
model:
type: string
Expand Down
4 changes: 4 additions & 0 deletions agent-core/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -628,13 +628,17 @@ export function createAgentFromEnv(overrides?: Partial<CreateAgentOptions>): Fir
GOOGLE_GENERATIVE_AI_API_KEY: "google",
AI_GATEWAY_API_KEY: "gateway",
CUSTOM_OPENAI_API_KEY: "custom-openai",
ATOMIC_CHAT_API_KEY: "atomic-chat",
};
for (const [env, id] of Object.entries(envMap)) {
if (process.env[env]) apiKeys[id] = process.env[env]!;
}
if (process.env.CUSTOM_OPENAI_BASE_URL) {
apiKeys["custom-openai:baseURL"] = process.env.CUSTOM_OPENAI_BASE_URL;
}
if (process.env.ATOMIC_CHAT_BASE_URL) {
apiKeys["atomic-chat:baseURL"] = process.env.ATOMIC_CHAT_BASE_URL;
}

// Support both MODEL="provider:id" shorthand and the split
// MODEL_PROVIDER + MODEL_ID env vars. Overrides always win.
Expand Down
26 changes: 26 additions & 0 deletions agent-core/src/resolve-model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,32 @@ describe("resolveModel", () => {
).rejects.toThrow("CUSTOM_OPENAI_BASE_URL is not configured");
});

it("resolves atomic-chat with default baseURL and keyless token", async () => {
const model = await resolveModel({
provider: "atomic-chat",
model: "local-model",
});
expect(model).toMatchObject({
provider: "openai",
model: "local-model",
apiKey: "atomic-chat-local",
baseURL: "http://127.0.0.1:1337/v1",
});
});

it("resolves atomic-chat with custom baseURL and api key", async () => {
const model = await resolveModel(
{ provider: "atomic-chat", model: "qwen-local" },
{ "atomic-chat": "my-key", "atomic-chat:baseURL": "http://host.docker.internal:1337/v1" },
);
expect(model).toMatchObject({
provider: "openai",
model: "qwen-local",
apiKey: "my-key",
baseURL: "http://host.docker.internal:1337/v1",
});
});

it("prefers config.apiKey over apiKeys map", async () => {
const model = await resolveModel(
{ provider: "google", model: "gemini-flash", apiKey: "config-key" },
Expand Down
14 changes: 13 additions & 1 deletion agent-core/src/resolve-model.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import type { LanguageModel } from "ai";
import type { ModelConfig } from "./types";

/** Placeholder API key for keyless local Atomic Chat (OpenAI client requires a string). */
export const ATOMIC_CHAT_DEFAULT_API_KEY = "atomic-chat-local";
export const ATOMIC_CHAT_DEFAULT_BASE_URL = "http://127.0.0.1:1337/v1";

/**
* Resolve a ModelConfig to an AI SDK LanguageModel instance.
* API keys can come from the config itself or the apiKeys map.
Expand Down Expand Up @@ -41,6 +45,14 @@ export async function resolveModel(
baseURL,
})(config.model);
}
case "atomic-chat": {
const { createOpenAI } = await import("@ai-sdk/openai");
const baseURL = config.baseURL || apiKeys?.["atomic-chat:baseURL"] || ATOMIC_CHAT_DEFAULT_BASE_URL;
return createOpenAI({
apiKey: keyFor("atomic-chat") || ATOMIC_CHAT_DEFAULT_API_KEY,
baseURL,
})(config.model);
}
case "google": {
const { createGoogleGenerativeAI } = await import("@ai-sdk/google");
return createGoogleGenerativeAI({ apiKey: keyFor("google") })(config.model);
Expand All @@ -52,7 +64,7 @@ export async function resolveModel(
const looksLikeModelId = /[-.]/.test(config.provider);
const hint = looksLikeModelId
? `. Did you mean MODEL="anthropic:${config.provider}" or similar? Format is "provider:model-id"`
: `. Supported: anthropic, openai, google, gateway, custom-openai`;
: `. Supported: anthropic, openai, google, gateway, custom-openai, atomic-chat`;
throw new Error(`Unsupported provider: "${config.provider}"${hint}`);
}
}
Expand Down
4 changes: 2 additions & 2 deletions agent-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ export interface Toolkit {
}

export interface ModelConfig {
/** LLM provider. "gateway" = Vercel AI Gateway, "custom-openai" = any OpenAI-compatible endpoint */
provider: "gateway" | "anthropic" | "openai" | "google" | "custom-openai";
/** LLM provider. "gateway" = Vercel AI Gateway, "custom-openai" = any OpenAI-compatible endpoint, "atomic-chat" = local Atomic Chat */
provider: "gateway" | "anthropic" | "openai" | "google" | "custom-openai" | "atomic-chat";
/** Model ID (e.g. "claude-sonnet-4-6", "gemini-3-flash-preview", "gpt-5.4") */
model: string;
/** Override the provider API key for this specific model */
Expand Down
3 changes: 2 additions & 1 deletion agent-templates/express/agent-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,8 +309,9 @@ const plan = await agent.plan('compare pricing across 5 CDN providers')
| Anthropic Claude | `{ provider: 'anthropic', model: 'claude-sonnet-4-6' }` |
| OpenAI | `{ provider: 'openai', model: 'gpt-5.4' }` |
| Custom (OpenAI-compat) | `{ provider: 'custom-openai', model: 'your-model', baseURL: '...' }` |
| Atomic Chat (local) | `{ provider: 'atomic-chat', model: 'your-model-id' }` — defaults to `http://127.0.0.1:1337/v1`, keyless |

Set API keys via `apiKeys` option or environment variables (`GOOGLE_GENERATIVE_AI_API_KEY`, `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`).
Set API keys via `apiKeys` option or environment variables (`GOOGLE_GENERATIVE_AI_API_KEY`, `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`). Atomic Chat is keyless by default; override with `ATOMIC_CHAT_BASE_URL` / `ATOMIC_CHAT_API_KEY` if needed.

## OpenAPI spec

Expand Down
2 changes: 1 addition & 1 deletion agent-templates/express/agent-core/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ components:
properties:
provider:
type: string
enum: [anthropic, openai, google, gateway, custom-openai]
enum: [anthropic, openai, google, gateway, custom-openai, atomic-chat]
description: LLM provider.
model:
type: string
Expand Down
4 changes: 4 additions & 0 deletions agent-templates/express/agent-core/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -628,13 +628,17 @@ export function createAgentFromEnv(overrides?: Partial<CreateAgentOptions>): Fir
GOOGLE_GENERATIVE_AI_API_KEY: "google",
AI_GATEWAY_API_KEY: "gateway",
CUSTOM_OPENAI_API_KEY: "custom-openai",
ATOMIC_CHAT_API_KEY: "atomic-chat",
};
for (const [env, id] of Object.entries(envMap)) {
if (process.env[env]) apiKeys[id] = process.env[env]!;
}
if (process.env.CUSTOM_OPENAI_BASE_URL) {
apiKeys["custom-openai:baseURL"] = process.env.CUSTOM_OPENAI_BASE_URL;
}
if (process.env.ATOMIC_CHAT_BASE_URL) {
apiKeys["atomic-chat:baseURL"] = process.env.ATOMIC_CHAT_BASE_URL;
}

// Support both MODEL="provider:id" shorthand and the split
// MODEL_PROVIDER + MODEL_ID env vars. Overrides always win.
Expand Down
26 changes: 26 additions & 0 deletions agent-templates/express/agent-core/src/resolve-model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,32 @@ describe("resolveModel", () => {
).rejects.toThrow("CUSTOM_OPENAI_BASE_URL is not configured");
});

it("resolves atomic-chat with default baseURL and keyless token", async () => {
const model = await resolveModel({
provider: "atomic-chat",
model: "local-model",
});
expect(model).toMatchObject({
provider: "openai",
model: "local-model",
apiKey: "atomic-chat-local",
baseURL: "http://127.0.0.1:1337/v1",
});
});

it("resolves atomic-chat with custom baseURL and api key", async () => {
const model = await resolveModel(
{ provider: "atomic-chat", model: "qwen-local" },
{ "atomic-chat": "my-key", "atomic-chat:baseURL": "http://host.docker.internal:1337/v1" },
);
expect(model).toMatchObject({
provider: "openai",
model: "qwen-local",
apiKey: "my-key",
baseURL: "http://host.docker.internal:1337/v1",
});
});

it("prefers config.apiKey over apiKeys map", async () => {
const model = await resolveModel(
{ provider: "google", model: "gemini-flash", apiKey: "config-key" },
Expand Down
14 changes: 13 additions & 1 deletion agent-templates/express/agent-core/src/resolve-model.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import type { LanguageModel } from "ai";
import type { ModelConfig } from "./types";

/** Placeholder API key for keyless local Atomic Chat (OpenAI client requires a string). */
export const ATOMIC_CHAT_DEFAULT_API_KEY = "atomic-chat-local";
export const ATOMIC_CHAT_DEFAULT_BASE_URL = "http://127.0.0.1:1337/v1";

/**
* Resolve a ModelConfig to an AI SDK LanguageModel instance.
* API keys can come from the config itself or the apiKeys map.
Expand Down Expand Up @@ -41,6 +45,14 @@ export async function resolveModel(
baseURL,
})(config.model);
}
case "atomic-chat": {
const { createOpenAI } = await import("@ai-sdk/openai");
const baseURL = config.baseURL || apiKeys?.["atomic-chat:baseURL"] || ATOMIC_CHAT_DEFAULT_BASE_URL;
return createOpenAI({
apiKey: keyFor("atomic-chat") || ATOMIC_CHAT_DEFAULT_API_KEY,
baseURL,
})(config.model);
}
case "google": {
const { createGoogleGenerativeAI } = await import("@ai-sdk/google");
return createGoogleGenerativeAI({ apiKey: keyFor("google") })(config.model);
Expand All @@ -52,7 +64,7 @@ export async function resolveModel(
const looksLikeModelId = /[-.]/.test(config.provider);
const hint = looksLikeModelId
? `. Did you mean MODEL="anthropic:${config.provider}" or similar? Format is "provider:model-id"`
: `. Supported: anthropic, openai, google, gateway, custom-openai`;
: `. Supported: anthropic, openai, google, gateway, custom-openai, atomic-chat`;
throw new Error(`Unsupported provider: "${config.provider}"${hint}`);
}
}
Expand Down
4 changes: 2 additions & 2 deletions agent-templates/express/agent-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ export interface Toolkit {
}

export interface ModelConfig {
/** LLM provider. "gateway" = Vercel AI Gateway, "custom-openai" = any OpenAI-compatible endpoint */
provider: "gateway" | "anthropic" | "openai" | "google" | "custom-openai";
/** LLM provider. "gateway" = Vercel AI Gateway, "custom-openai" = any OpenAI-compatible endpoint, "atomic-chat" = local Atomic Chat */
provider: "gateway" | "anthropic" | "openai" | "google" | "custom-openai" | "atomic-chat";
/** Model ID (e.g. "claude-sonnet-4-6", "gemini-3-flash-preview", "gpt-5.4") */
model: string;
/** Override the provider API key for this specific model */
Expand Down
17 changes: 16 additions & 1 deletion agent-templates/express/scripts/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,25 @@ const providerKeyEnv: Record<string, string> = {
google: "GOOGLE_GENERATIVE_AI_API_KEY",
gateway: "AI_GATEWAY_API_KEY",
"custom-openai": "CUSTOM_OPENAI_API_KEY",
"atomic-chat": "ATOMIC_CHAT_API_KEY",
};
const requiredKey = providerKeyEnv[provider];
if (requiredKey) {
if (requiredKey && provider !== "atomic-chat") {
checkKey(requiredKey, `${requiredKey} (for provider "${provider}")`, true);
} else if (provider === "atomic-chat") {
const key = process.env.ATOMIC_CHAT_API_KEY;
if (key) checkKey("ATOMIC_CHAT_API_KEY", "ATOMIC_CHAT_API_KEY (optional)", false);
}

// --- atomic-chat baseURL (optional override) ---
if (provider === "atomic-chat") {
const baseURL = process.env.ATOMIC_CHAT_BASE_URL ?? "http://127.0.0.1:1337/v1";
try {
new URL(baseURL);
add("ATOMIC_CHAT_BASE_URL", "ok", baseURL);
} catch {
add("ATOMIC_CHAT_BASE_URL", "fail", `invalid URL: "${baseURL}"`);
}
}

// --- custom-openai baseURL ---
Expand Down
3 changes: 2 additions & 1 deletion agent-templates/library/agent-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,8 +309,9 @@ const plan = await agent.plan('compare pricing across 5 CDN providers')
| Anthropic Claude | `{ provider: 'anthropic', model: 'claude-sonnet-4-6' }` |
| OpenAI | `{ provider: 'openai', model: 'gpt-5.4' }` |
| Custom (OpenAI-compat) | `{ provider: 'custom-openai', model: 'your-model', baseURL: '...' }` |
| Atomic Chat (local) | `{ provider: 'atomic-chat', model: 'your-model-id' }` — defaults to `http://127.0.0.1:1337/v1`, keyless |

Set API keys via `apiKeys` option or environment variables (`GOOGLE_GENERATIVE_AI_API_KEY`, `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`).
Set API keys via `apiKeys` option or environment variables (`GOOGLE_GENERATIVE_AI_API_KEY`, `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`). Atomic Chat is keyless by default; override with `ATOMIC_CHAT_BASE_URL` / `ATOMIC_CHAT_API_KEY` if needed.

## OpenAPI spec

Expand Down
2 changes: 1 addition & 1 deletion agent-templates/library/agent-core/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ components:
properties:
provider:
type: string
enum: [anthropic, openai, google, gateway, custom-openai]
enum: [anthropic, openai, google, gateway, custom-openai, atomic-chat]
description: LLM provider.
model:
type: string
Expand Down
4 changes: 4 additions & 0 deletions agent-templates/library/agent-core/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -628,13 +628,17 @@ export function createAgentFromEnv(overrides?: Partial<CreateAgentOptions>): Fir
GOOGLE_GENERATIVE_AI_API_KEY: "google",
AI_GATEWAY_API_KEY: "gateway",
CUSTOM_OPENAI_API_KEY: "custom-openai",
ATOMIC_CHAT_API_KEY: "atomic-chat",
};
for (const [env, id] of Object.entries(envMap)) {
if (process.env[env]) apiKeys[id] = process.env[env]!;
}
if (process.env.CUSTOM_OPENAI_BASE_URL) {
apiKeys["custom-openai:baseURL"] = process.env.CUSTOM_OPENAI_BASE_URL;
}
if (process.env.ATOMIC_CHAT_BASE_URL) {
apiKeys["atomic-chat:baseURL"] = process.env.ATOMIC_CHAT_BASE_URL;
}

// Support both MODEL="provider:id" shorthand and the split
// MODEL_PROVIDER + MODEL_ID env vars. Overrides always win.
Expand Down
26 changes: 26 additions & 0 deletions agent-templates/library/agent-core/src/resolve-model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,32 @@ describe("resolveModel", () => {
).rejects.toThrow("CUSTOM_OPENAI_BASE_URL is not configured");
});

it("resolves atomic-chat with default baseURL and keyless token", async () => {
const model = await resolveModel({
provider: "atomic-chat",
model: "local-model",
});
expect(model).toMatchObject({
provider: "openai",
model: "local-model",
apiKey: "atomic-chat-local",
baseURL: "http://127.0.0.1:1337/v1",
});
});

it("resolves atomic-chat with custom baseURL and api key", async () => {
const model = await resolveModel(
{ provider: "atomic-chat", model: "qwen-local" },
{ "atomic-chat": "my-key", "atomic-chat:baseURL": "http://host.docker.internal:1337/v1" },
);
expect(model).toMatchObject({
provider: "openai",
model: "qwen-local",
apiKey: "my-key",
baseURL: "http://host.docker.internal:1337/v1",
});
});

it("prefers config.apiKey over apiKeys map", async () => {
const model = await resolveModel(
{ provider: "google", model: "gemini-flash", apiKey: "config-key" },
Expand Down
14 changes: 13 additions & 1 deletion agent-templates/library/agent-core/src/resolve-model.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import type { LanguageModel } from "ai";
import type { ModelConfig } from "./types";

/** Placeholder API key for keyless local Atomic Chat (OpenAI client requires a string). */
export const ATOMIC_CHAT_DEFAULT_API_KEY = "atomic-chat-local";
export const ATOMIC_CHAT_DEFAULT_BASE_URL = "http://127.0.0.1:1337/v1";

/**
* Resolve a ModelConfig to an AI SDK LanguageModel instance.
* API keys can come from the config itself or the apiKeys map.
Expand Down Expand Up @@ -41,6 +45,14 @@ export async function resolveModel(
baseURL,
})(config.model);
}
case "atomic-chat": {
const { createOpenAI } = await import("@ai-sdk/openai");
const baseURL = config.baseURL || apiKeys?.["atomic-chat:baseURL"] || ATOMIC_CHAT_DEFAULT_BASE_URL;
return createOpenAI({
apiKey: keyFor("atomic-chat") || ATOMIC_CHAT_DEFAULT_API_KEY,
baseURL,
})(config.model);
}
case "google": {
const { createGoogleGenerativeAI } = await import("@ai-sdk/google");
return createGoogleGenerativeAI({ apiKey: keyFor("google") })(config.model);
Expand All @@ -52,7 +64,7 @@ export async function resolveModel(
const looksLikeModelId = /[-.]/.test(config.provider);
const hint = looksLikeModelId
? `. Did you mean MODEL="anthropic:${config.provider}" or similar? Format is "provider:model-id"`
: `. Supported: anthropic, openai, google, gateway, custom-openai`;
: `. Supported: anthropic, openai, google, gateway, custom-openai, atomic-chat`;
throw new Error(`Unsupported provider: "${config.provider}"${hint}`);
}
}
Expand Down
4 changes: 2 additions & 2 deletions agent-templates/library/agent-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ export interface Toolkit {
}

export interface ModelConfig {
/** LLM provider. "gateway" = Vercel AI Gateway, "custom-openai" = any OpenAI-compatible endpoint */
provider: "gateway" | "anthropic" | "openai" | "google" | "custom-openai";
/** LLM provider. "gateway" = Vercel AI Gateway, "custom-openai" = any OpenAI-compatible endpoint, "atomic-chat" = local Atomic Chat */
provider: "gateway" | "anthropic" | "openai" | "google" | "custom-openai" | "atomic-chat";
/** Model ID (e.g. "claude-sonnet-4-6", "gemini-3-flash-preview", "gpt-5.4") */
model: string;
/** Override the provider API key for this specific model */
Expand Down
Loading