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
1 change: 1 addition & 0 deletions core/llm/autodetect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
} from "./templates/edit.js";

const PROVIDER_HANDLES_TEMPLATING: string[] = [
"atomic-chat",
"lmstudio",
"lemonade",
"openai",
Expand Down
12 changes: 12 additions & 0 deletions core/llm/llms/AtomicChat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { LLMOptions } from "../../index.js";

import OpenAI from "./OpenAI.js";

class AtomicChat extends OpenAI {
static providerName = "atomic-chat";
static defaultOptions: Partial<LLMOptions> = {
apiBase: "http://127.0.0.1:1337/v1/",
};
}

export default AtomicChat;
6 changes: 6 additions & 0 deletions core/llm/llms/OpenAI-compatible.vitest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import OpenRouter from "./OpenRouter.js";
import xAI from "./xAI.js";
import Mistral from "./Mistral.js";
import Mimo from "./Mimo.js";
import AtomicChat from "./AtomicChat.js";
import LMStudio from "./LMStudio.js";
import Cerebras from "./Cerebras.js";
import DeepInfra from "./DeepInfra.js";
Expand Down Expand Up @@ -306,6 +307,11 @@ createOpenAISubclassTests(Mimo, {
defaultApiBase: "https://api.xiaomimimo.com/v1/",
});

createOpenAISubclassTests(AtomicChat, {
providerName: "atomic-chat",
defaultApiBase: "http://127.0.0.1:1337/v1/",
});

createOpenAISubclassTests(LMStudio, {
providerName: "lmstudio",
defaultApiBase: "http://localhost:1234/v1/",
Expand Down
2 changes: 2 additions & 0 deletions core/llm/llms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { renderTemplatedString } from "../../util/handlebars/renderTemplatedString";
import { BaseLLM } from "../index";
import Anthropic from "./Anthropic";
import AtomicChat from "./AtomicChat";
import Asksage from "./Asksage";
import Azure from "./Azure";
import Bedrock from "./Bedrock";
Expand Down Expand Up @@ -73,6 +74,7 @@ import xAI from "./xAI";
import zAI from "./zAI";
export const LLMClasses = [
Anthropic,
AtomicChat,
Cohere,
CometAPI,
FunctionNetwork,
Expand Down
71 changes: 71 additions & 0 deletions docs/customize/model-providers/more/atomic-chat.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
title: "Atomic Chat"
description: "Configure Atomic Chat with Continue for local LLM inference through an OpenAI-compatible API"
---

<Info>
Get started with [Atomic Chat](https://atomic.chat/) — a desktop app that serves local models at `http://127.0.0.1:1337/v1`
</Info>

## Overview

Atomic Chat runs open-source models on your machine and exposes an OpenAI-compatible HTTP API. Continue connects to that endpoint for chat, edit, and agent workflows.

## Configuration

### Option 1: Using the Continue UI (Recommended)

1. Open the model selector in Continue
2. Choose **Add Model**
3. Select **Atomic Chat** from the provider list
4. Pick a model from the autodetected list (requires Atomic Chat to be running)

### Option 2: Manual configuration

<Tabs>
<Tab title="YAML">
```yaml title="config.yaml"
name: My Config
version: 0.0.1
schema: v1

models:
- name: Atomic Chat
provider: atomic-chat
model: <MODEL_ID>
apiBase: http://127.0.0.1:1337/v1/
```
</Tab>
<Tab title="JSON (Deprecated)">
```json title="config.json"
{
"models": [
{
"title": "Atomic Chat",
"provider": "atomic-chat",
"model": "<MODEL_ID>",
"apiBase": "http://127.0.0.1:1337/v1/"
}
]
}
```
</Tab>
</Tabs>

Replace `<MODEL_ID>` with an id from Atomic Chat. List available models:

```bash
curl -s http://127.0.0.1:1337/v1/models | jq '.data[].id'
```

## Getting started

1. Install and open [Atomic Chat](https://atomic.chat/)
2. Load a model in the app so the API is listening on port **1337** (or **1338** if you changed the port)
3. Add **Atomic Chat** as a provider in Continue and select your model

## Tool calling

For agent mode and tools, prefer models with strong tool-calling support (for example Qwen-Coder or DeepSeek-Coder variants).

[View the source](https://github.com/continuedev/continue/blob/main/core/llm/llms/AtomicChat.ts)
Binary file added gui/public/logos/atomic-chat.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
61 changes: 48 additions & 13 deletions gui/src/forms/AddModelForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
ArrowPathIcon,
ArrowTopRightOnSquareIcon,
} from "@heroicons/react/24/outline";
import { useCallback, useContext, useEffect, useState } from "react";
import { useCallback, useContext, useEffect, useRef, useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { Button, Input, StyledActionButton } from "../components";
import Alert from "../components/gui/Alert";
Expand Down Expand Up @@ -30,6 +30,7 @@ const MODEL_PROVIDERS_URL =
"https://docs.continue.dev/customize/model-providers";
const CODESTRAL_URL = "https://console.mistral.ai/codestral";
const CONTINUE_SETUP_URL = "https://docs.continue.dev/setup/overview";
const LOCAL_DYNAMIC_MODEL_PROVIDERS = ["atomic-chat"];

export function AddModelForm({
onDone,
Expand All @@ -49,39 +50,62 @@ export function AddModelForm({
[],
);
const [isFetchingModels, setIsFetchingModels] = useState(false);
const selectedProviderRef = useRef(selectedProvider.provider);
const fetchGenerationRef = useRef(0);

selectedProviderRef.current = selectedProvider.provider;

useEffect(() => {
void initializeDynamicModels(ideMessenger);
}, []);

useEffect(() => {
setFetchedModelsList([]);
fetchGenerationRef.current += 1;
}, [selectedProvider]);

const handleFetchModels = useCallback(async () => {
const apiKey = formMethods.watch("apiKey");
const apiBase = formMethods.watch("apiBase");
if (!apiKey) return;
const providerAtFetchTime = selectedProviderRef.current;
const isLocalDynamic =
LOCAL_DYNAMIC_MODEL_PROVIDERS.includes(providerAtFetchTime);
if (!apiKey && !isLocalDynamic) return;

const providerAtFetchTime = selectedProvider.provider;
const fetchGeneration = fetchGenerationRef.current;
setIsFetchingModels(true);
try {
const models = await fetchProviderModels(
ideMessenger,
providerAtFetchTime,
apiKey,
apiBase,
);
setFetchedModelsList((prev) =>
selectedProvider.provider === providerAtFetchTime ? models : prev,
apiKey || undefined,
apiBase || selectedProvider.params?.apiBase,
);
if (
fetchGenerationRef.current !== fetchGeneration ||
selectedProviderRef.current !== providerAtFetchTime
) {
return;
}
setFetchedModelsList(models);
if (isLocalDynamic && models.length > 0) {
setSelectedModel((current) =>
current.params.model === "AUTODETECT" ? models[0] : current,
);
}
} catch (error) {
console.error("Failed to fetch models:", error);
} finally {
setIsFetchingModels(false);
}
}, [ideMessenger, selectedProvider, formMethods]);

useEffect(() => {
if (LOCAL_DYNAMIC_MODEL_PROVIDERS.includes(selectedProvider.provider)) {
void handleFetchModels();
}
}, [selectedProvider.provider, handleFetchModels]);

const popularProviderTitles = [
providers["openai"]?.title || "",
providers["anthropic"]?.title || "",
Expand Down Expand Up @@ -112,6 +136,10 @@ export function AddModelForm({
: selectedProvider.apiKeyUrl;

function isDisabled() {
if (selectedProvider.provider === "atomic-chat") {
return selectedModel.params.model === "AUTODETECT";
}

if (selectedProvider.downloadUrl) {
return false;
}
Expand All @@ -127,7 +155,11 @@ export function AddModelForm({
}

useEffect(() => {
setSelectedModel(selectedProvider.packages[0]);
const defaultModel =
selectedProvider.packages.find(
(pkg) => pkg.params.model !== "AUTODETECT",
) ?? selectedProvider.packages[0];
setSelectedModel(defaultModel);
if (!selectedProvider.tags?.includes(ModelProviderTags.RequiresApiKey)) {
formMethods.setValue("apiKey", "");
}
Expand Down Expand Up @@ -245,10 +277,13 @@ export function AddModelForm({
type="button"
title="Use entered API key to fetch available models"
className={`cursor-pointer border-none bg-transparent p-0 ${
apiKeyValue &&
apiKeyValue.length > 0 &&
selectedProvider.provider !== "ollama" &&
selectedProvider.provider !== "openrouter"
(apiKeyValue &&
apiKeyValue.length > 0 &&
selectedProvider.provider !== "ollama" &&
selectedProvider.provider !== "openrouter") ||
LOCAL_DYNAMIC_MODEL_PROVIDERS.includes(
selectedProvider.provider,
)
? `text-description-muted hover:text-foreground`
: "invisible"
}`}
Expand Down
46 changes: 45 additions & 1 deletion gui/src/pages/AddNewModel/configs/fetchProviderModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,23 @@ function toOpenRouterPackage(model: FetchedModel): ModelPackage {
};
}

function toAtomicChatPackage(model: FetchedModel): ModelPackage {
const id = model.modelId ?? model.name;
return {
title: model.name,
description: model.description ?? model.name,
params: {
title: model.name,
model: id,
...modelParams(model),
},
isOpenSource: true,
tags: [ModelProviderTags.Local, ModelProviderTags.OpenSource],
providerOptions: ["atomic-chat"],
icon: model.icon ?? "atomic-chat.png",
};
}

function toGenericPackage(model: FetchedModel, provider: string): ModelPackage {
const id = model.modelId ?? model.name;
return {
Expand Down Expand Up @@ -96,10 +113,13 @@ async function fetchModels(
export async function fetchProviderModels(
ideMessenger: IIdeMessenger,
provider: string,
apiKey: string,
apiKey?: string,
apiBase?: string,
): Promise<ModelPackage[]> {
const models = await fetchModels(ideMessenger, provider, apiKey, apiBase);
if (provider === "atomic-chat") {
return models.map(toAtomicChatPackage);
}
return models.map((m) => toGenericPackage(m, provider));
}

Expand Down Expand Up @@ -141,4 +161,28 @@ export async function initializeDynamicModels(ideMessenger: IIdeMessenger) {
} catch (error) {
console.error("Failed to initialize OpenRouter models:", error);
}

if (providers.atomicChat) {
const autodetect = {
...models.AUTODETECT,
params: { ...models.AUTODETECT.params, title: "Atomic Chat" },
};

try {
const fetched = await fetchModels(
ideMessenger,
"atomic-chat",
undefined,
providers.atomicChat.params.apiBase,
);
if (fetched.length > 0) {
providers.atomicChat.packages = fetched.map(toAtomicChatPackage);
} else {
providers.atomicChat.packages = [autodetect];
}
} catch (error) {
console.error("Failed to initialize Atomic Chat models:", error);
providers.atomicChat.packages = [autodetect];
}
}
}
31 changes: 31 additions & 0 deletions gui/src/pages/AddNewModel/configs/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,37 @@ Select the \`GPT-4o\` model below to complete your provider configuration, but n
],
apiKeyUrl: "https://console.x.ai/",
},
atomicChat: {
title: "Atomic Chat",
provider: "atomic-chat",
description: "Local LLMs via the Atomic Chat desktop app",
longDescription:
"Atomic Chat runs models on your machine and serves them through an OpenAI-compatible API (default `http://127.0.0.1:1337/v1`).\n\n1. Download from [atomic.chat](https://atomic.chat/) and open the app\n2. Load a model so the local API is running\n3. Add Atomic Chat in Continue and choose a model from the list\n\nModel ids must match those returned by `GET /v1/models`.",
icon: "atomic-chat.png",
tags: [ModelProviderTags.Local, ModelProviderTags.OpenSource],
params: {
apiBase: "http://127.0.0.1:1337/v1/",
},
packages: [
{
...models.AUTODETECT,
params: {
...models.AUTODETECT.params,
title: "Atomic Chat",
},
},
],
collectInputFor: [
...completionParamsInputsConfigs,
{
...apiBaseInput,
defaultValue: "http://127.0.0.1:1337/v1/",
required: true,
},
],
downloadUrl: "https://atomic.chat/",
refPage: "atomic-chat",
},
lemonade: {
title: "Lemonade",
provider: "lemonade",
Expand Down
2 changes: 2 additions & 0 deletions packages/openai-adapters/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@ export function constructLlmApi(config: LLMConfig): BaseLlmApi | undefined {
case "llama.cpp":
case "llamafile":
return openAICompatible("http://localhost:8000/", config);
case "atomic-chat":
return openAICompatible("http://127.0.0.1:1337/v1/", config);
case "lmstudio":
return openAICompatible("http://localhost:1234/", config);
case "ollama":
Expand Down
1 change: 1 addition & 0 deletions packages/openai-adapters/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export const OpenAIConfigSchema = BasePlusConfig.extend({
z.literal("function-network"),
z.literal("llama.cpp"),
z.literal("llamafile"),
z.literal("atomic-chat"),
z.literal("lmstudio"),
z.literal("ollama"),
z.literal("cerebras"),
Expand Down
Loading