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
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Gini's **runtime is the gateway**: a single Bun process per instance owns state
- Authenticated localhost gateway and a Next.js + Tailwind + shadcn/ui control plane
- Persistent chat, runs, tasks, approvals, traces, audit events, jobs, memories, and skills
- Approval-gated file, terminal, and code tools
- Provider support: Codex OAuth; OpenAI / Azure OpenAI / DeepSeek / OpenRouter API keys; the first-party Anthropic Claude API; Amazon Bedrock (model-agnostic Converse, AWS SigV4 — Claude, Nova, Llama, Mistral, DeepSeek); and any OpenAI-compatible local server
- Provider support: Codex OAuth; OpenAI / Azure OpenAI / DeepSeek / Atlas Cloud / OpenRouter API keys; the first-party Anthropic Claude API; Amazon Bedrock (model-agnostic Converse, AWS SigV4 — Claude, Nova, Llama, Mistral, DeepSeek); and any OpenAI-compatible local server
- Local embeddings, reranking, and voice-message speech-to-text by default
- Parallel instances with isolated state, ports, and logs
- In-chat actionable controls — secure credential/secret fields, sign-in and sensitive-step handoff into the agent's browser, choice prompts, and confirm-before-send — so the agent unblocks itself in context instead of stranding you
Expand Down Expand Up @@ -129,6 +129,7 @@ Run `gini setup` for an interactive picker, or configure directly:
gini provider set codex gpt-5.5 # Codex OAuth (reads ~/.codex/auth.json)
gini provider set openai gpt-5.4-mini # uses $OPENAI_API_KEY
gini provider set openrouter <model> # uses $OPENROUTER_API_KEY
gini provider set atlascloud qwen/qwen3.5-flash # uses $ATLASCLOUD_API_KEY
gini provider set local <model> --base-url http://127.0.0.1:8000/v1
gini provider set anthropic claude-opus-4-8 # first-party Claude API, uses $ANTHROPIC_API_KEY
# Amazon Bedrock: model-agnostic Converse, SigV4-signed with AWS keys you enter via the web form or `gini setup`
Expand All @@ -139,11 +140,11 @@ gini provider set azure gpt-4o \
--deployment <deployment> --api-version 2024-10-21 --auth-scheme api-key # uses $AZURE_OPENAI_API_KEY
```

For step-by-step setup of each provider — getting credentials, installing any prerequisite tooling (Ollama, …), and configuring it in the CLI or web — see the per-provider guides: [OpenAI](docs/providers/openai.md), [Anthropic](docs/providers/anthropic.md), [Amazon Bedrock](docs/providers/bedrock.md), [Azure OpenAI](docs/providers/azure.md), [OpenRouter](docs/providers/openrouter.md), [DeepSeek](docs/providers/deepseek.md), [Codex](docs/providers/codex.md), and [Local](docs/providers/local.md). The [providers index](docs/providers/README.md) lists them all with their auth model at a glance.
For step-by-step setup of each provider — getting credentials, installing any prerequisite tooling (Ollama, …), and configuring it in the CLI or web — see the per-provider guides: [OpenAI](docs/providers/openai.md), [Anthropic](docs/providers/anthropic.md), [Amazon Bedrock](docs/providers/bedrock.md), [Azure OpenAI](docs/providers/azure.md), [OpenRouter](docs/providers/openrouter.md), [DeepSeek](docs/providers/deepseek.md), [Atlas Cloud](docs/providers/atlascloud.md), [Codex](docs/providers/codex.md), and [Local](docs/providers/local.md). The [providers index](docs/providers/README.md) lists them all with their auth model at a glance.

The `local` provider works with any OpenAI-compatible server (oMLX, vLLM, LM Studio, llama.cpp). The `azure` provider targets an Azure OpenAI resource: set `--base-url` to `https://<resource>.openai.azure.com` and pick a deployment; `--api-version` defaults to a GA value and `--auth-scheme` defaults to `api-key` (a resource key), with `bearer` available for an Entra token. API keys are read from environment variables, and Codex OAuth is read from `~/.codex/auth.json` (or `CODEX_AUTH_JSON`) — nothing is written to Gini config. Run `gini --help` for the full flag set, or see [provider-extra-body.md](docs/adr/provider-extra-body.md) for the `--extra-body` contract and [Azure OpenAI As A First-Class Provider](docs/adr/azure-provider.md) for the Azure routing contract. When a credential fails mid-chat, see [Codex re-authentication](docs/providers/codex.md#re-authentication) and [Provider Re-Authentication Guidance](docs/adr/provider-reauth-guidance.md).

`gini setup`'s interactive picker covers every provider — OpenAI, Codex, Anthropic, Amazon Bedrock, Azure OpenAI, OpenRouter, DeepSeek, and Local — prompting for whatever each one needs (an API key, the AWS access key + secret for Bedrock, the resource endpoint and deployment for Azure, the base URL for Local). `gini provider set …` (above) and the web **Settings → Add provider** form remain available for scripted or non-interactive configuration.
`gini setup`'s interactive picker covers every provider — OpenAI, Codex, Anthropic, Amazon Bedrock, Azure OpenAI, OpenRouter, DeepSeek, Atlas Cloud, and Local — prompting for whatever each one needs (an API key, the AWS access key + secret for Bedrock, the resource endpoint and deployment for Azure, the base URL for Local). `gini provider set …` (above) and the web **Settings → Add provider** form remain available for scripted or non-interactive configuration.

## Parallel Instances

Expand Down
1 change: 1 addition & 0 deletions docs/providers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ provider in Gini (both the CLI and the web Add Provider form).
| Azure OpenAI | API key / Entra | `AZURE_OPENAI_API_KEY` | [azure.md](azure.md) |
| OpenRouter | API key | `OPENROUTER_API_KEY` | [openrouter.md](openrouter.md) |
| DeepSeek | API key | `DEEPSEEK_API_KEY` | [deepseek.md](deepseek.md) |
| Atlas Cloud | API key | `ATLASCLOUD_API_KEY` | [atlascloud.md](atlascloud.md) |
| Codex (OpenAI OAuth) | OAuth / CLI | `~/.codex/auth.json` (no key) | [codex.md](codex.md) |
| Local (OpenAI-compatible) | none / optional key | `GINI_LOCAL_API_KEY` (optional) | [local.md](local.md) |

Expand Down
48 changes: 48 additions & 0 deletions docs/providers/atlascloud.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Atlas Cloud

Atlas Cloud is an OpenAI-compatible API-key provider. Gini talks to
`https://api.atlascloud.ai/v1` and authenticates with a Bearer key.

## Step 1 - Get an API key

1. Sign in to the [Atlas Cloud console](https://www.atlascloud.ai/console/api-keys).
2. Create an API key and copy it.
3. Check the live model catalog in Atlas Cloud before pinning a model for
production use.

## Step 2 - Set the key

Gini reads the key from the `ATLASCLOUD_API_KEY` environment variable. Set it in
your shell or in `~/.gini/secrets.env` for persistence:

```bash
# ~/.gini/secrets.env (created mode 0600)
ATLASCLOUD_API_KEY=ak-...
```

The web Add Provider form writes this for you.

## Step 3 - Configure the provider in Gini

### CLI

```bash
gini provider set atlascloud qwen/qwen3.5-flash
```

The default model is `qwen/qwen3.5-flash`. You can also pin another Atlas Cloud
chat model, such as `deepseek-ai/deepseek-v4-pro`. The base URL defaults to
`https://api.atlascloud.ai/v1`; override it only for a compatible proxy with
`--base-url`.

### Web

Open **Settings -> Add provider -> Atlas Cloud**, paste the key, and pick or type
an Atlas Cloud model ID.

## Re-authentication

Atlas Cloud is an API-key provider, so a credential failure surfaces the
provider error and links to **Settings -> Providers** to paste a new key. Rotate
keys from the Atlas Cloud console when needed. See ADR
[provider-reauth-guidance.md](../adr/provider-reauth-guidance.md).
2 changes: 1 addition & 1 deletion packages/runtime/src/model-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export function anthropicMirrorModelId(bedrockModelId: string): string | undefin
// revisions and features first), codex's OAuth bundle beats the metered
// clouds, and openrouter/local are deliberate opt-ins that should never
// win a tie. Unknown providers sort last in catalog order.
const ROUTE_PRIORITY = ["openai", "anthropic", "deepseek", "codex", "azure", "bedrock", "openrouter", "local"] as const;
const ROUTE_PRIORITY = ["openai", "anthropic", "deepseek", "atlascloud", "codex", "azure", "bedrock", "openrouter", "local"] as const;

function routePriority(provider: ProviderCatalogItem["name"]): number {
const index = (ROUTE_PRIORITY as readonly string[]).indexOf(provider);
Expand Down
7 changes: 7 additions & 0 deletions packages/runtime/src/provider-capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ export function resolveProviderContextWindowTokens(provider: ProviderConfig): nu
return openrouterContextWindowTokens(model);
case "deepseek":
return deepseekContextWindowTokens(model);
case "atlascloud":
return FALLBACK_CONTEXT_WINDOW_TOKENS;
case "anthropic":
return anthropicContextWindowTokens(model);
case "bedrock":
Expand Down Expand Up @@ -292,6 +294,11 @@ export function resolveProviderModality(provider: ProviderConfig): ProviderModal
case "deepseek":
// Confirmed text-only API — no image/file content part.
return { vision: false, nativeDocs: false };
case "atlascloud":
// Atlas Cloud's OpenAI-compatible LLM endpoint is used here for text chat
// models. Keep attachments conservative until the runtime has per-model
// modality discovery for Atlas model ids.
return { vision: false, nativeDocs: false };
case "anthropic":
// Claude Opus/Sonnet/Haiku accept image input and ingest documents
// natively via the Messages API (image + document content blocks). The
Expand Down
22 changes: 22 additions & 0 deletions packages/runtime/src/provider.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,28 @@ describe("provider integration (against in-process mock server)", () => {
});
});

test("atlascloud generateTaskSummary uses the OpenAI-compatible endpoint and bearer key", async () => {
await withMockServer(async ({ server, setEnv, uniqueEnvName }) => {
const keyEnv = uniqueEnvName();
setEnv(keyEnv, "test-atlas-key");
const provider = normalizeProvider({
name: "atlascloud",
model: "qwen/qwen3.5-flash",
baseUrl: server.url,
apiKeyEnv: keyEnv
});
const result = await generateTaskSummary(cfg(provider), "summarize via atlas");
expect(result.text).toBe("mock-echo: summarize via atlas");
expect(server.received).toHaveLength(1);
const captured = server.received[0]!;
expect(captured.method).toBe("POST");
expect(captured.url.endsWith("/v1/chat/completions")).toBe(true);
expect(captured.headers.authorization).toBe("[REDACTED]");
const sent = captured.body as Record<string, unknown>;
expect(sent.model).toBe("qwen/qwen3.5-flash");
});
});

test("local provider sends no Authorization header when api key env is unset", async () => {
await withMockServer(async ({ server, uniqueEnvName }) => {
const keyEnv = uniqueEnvName();
Expand Down
28 changes: 23 additions & 5 deletions packages/runtime/src/provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -880,24 +880,34 @@ describe("provider", () => {

// ---------------- extraBody plumbing (oMLX-style chat_template_kwargs) ----------------

// The openai/openrouter/local providers all forward `extraBody` into the
// The openai/openrouter/atlascloud/local providers all forward `extraBody` into the
// request body of every chat-completions call (tool-calling, structured,
// vision, generateTaskSummary). Codex uses /responses with its own shape
// and is not expected to honor extraBody — verified separately by the
// /responses tests above which already assert the request body keys.

test("normalizeProvider preserves extraBody for local/openai/openrouter and drops it for echo/codex", () => {
test("normalizeProvider preserves extraBody for local/openai/openrouter/atlascloud and drops it for echo/codex", () => {
const extraBody = { chat_template_kwargs: { enable_thinking: true } };
expect(normalizeProvider({ name: "local", model: "m", extraBody }).extraBody).toEqual(extraBody);
expect(normalizeProvider({ name: "openai", model: "m", extraBody }).extraBody).toEqual(extraBody);
expect(normalizeProvider({ name: "openrouter", model: "m", extraBody }).extraBody).toEqual(extraBody);
expect(normalizeProvider({ name: "atlascloud", model: "m", extraBody }).extraBody).toEqual(extraBody);
expect(normalizeProvider({ name: "azure", model: "m", baseUrl: "https://r.openai.azure.com", extraBody }).extraBody).toEqual(extraBody);
// echo and codex don't carry extraBody through. Echo is deterministic and
// bypasses the HTTP path; codex uses /responses (different wire shape).
expect(normalizeProvider({ name: "echo", model: "m", extraBody }).extraBody).toBeUndefined();
expect(normalizeProvider({ name: "codex", model: "m", extraBody }).extraBody).toBeUndefined();
});

test("normalizeProvider applies Atlas Cloud OpenAI-compatible defaults", () => {
expect(normalizeProvider({ name: "atlascloud", model: "" })).toEqual({
name: "atlascloud",
model: "qwen/qwen3.5-flash",
baseUrl: "https://api.atlascloud.ai/v1",
apiKeyEnv: "ATLASCLOUD_API_KEY"
});
});

test("normalizeProvider applies azure defaults: api-key auth, GA api-version, AZURE_OPENAI_API_KEY env", () => {
const provider = normalizeProvider({ name: "azure", model: "gpt-4o", baseUrl: "https://r.openai.azure.com" });
expect(provider.name).toBe("azure");
Expand Down Expand Up @@ -1047,15 +1057,20 @@ describe("provider", () => {
});

test("non-azure providers stay configured from their env var alone", () => {
const prev = process.env.OPENAI_API_KEY;
const prevOpenAI = process.env.OPENAI_API_KEY;
const prevAtlas = process.env.ATLASCLOUD_API_KEY;
process.env.OPENAI_API_KEY = "sk-x";
process.env.ATLASCLOUD_API_KEY = "atlas-key";
try {
// openai has a working default endpoint, so the env key is enough even
// when it isn't the active provider.
expect(isProviderConfigured("openai", "azure", undefined, undefined)).toBe(true);
expect(isProviderConfigured("atlascloud", "azure", undefined, undefined)).toBe(true);
} finally {
if (prev === undefined) delete process.env.OPENAI_API_KEY;
else process.env.OPENAI_API_KEY = prev;
if (prevOpenAI === undefined) delete process.env.OPENAI_API_KEY;
else process.env.OPENAI_API_KEY = prevOpenAI;
if (prevAtlas === undefined) delete process.env.ATLASCLOUD_API_KEY;
else process.env.ATLASCLOUD_API_KEY = prevAtlas;
}
});

Expand Down Expand Up @@ -3585,6 +3600,7 @@ describe("resolveDispatchProvider (transient fallback)", () => {
"OPENAI_API_KEY",
"OPENROUTER_API_KEY",
"DEEPSEEK_API_KEY",
"ATLASCLOUD_API_KEY",
"GINI_LOCAL_API_KEY",
"ANTHROPIC_API_KEY",
"AZURE_OPENAI_API_KEY",
Expand Down Expand Up @@ -3824,6 +3840,7 @@ describe("auth-error classification", () => {
expect(providerDisplayLabel("openai")).toBe("OpenAI");
expect(providerDisplayLabel("openrouter")).toBe("OpenRouter");
expect(providerDisplayLabel("deepseek")).toBe("DeepSeek");
expect(providerDisplayLabel("atlascloud")).toBe("Atlas Cloud");
expect(providerDisplayLabel("azure")).toBe("Azure OpenAI");
expect(providerDisplayLabel("local")).toBe("Local");
expect(providerDisplayLabel("echo")).toBe("Gini Echo");
Expand All @@ -3839,6 +3856,7 @@ describe("auth-error classification", () => {
expect(providerReauth("bedrock")).toEqual({ kind: "aws", url: "/settings" });
expect(providerReauth("openai")).toEqual({ kind: "settings", url: "/settings" });
expect(providerReauth("deepseek")).toEqual({ kind: "settings", url: "/settings" });
expect(providerReauth("atlascloud")).toEqual({ kind: "settings", url: "/settings" });
expect(providerReauth("openrouter")).toEqual({ kind: "settings", url: "/settings" });
expect(providerReauth("azure")).toEqual({ kind: "settings", url: "/settings" });
expect(providerReauth("local")).toEqual({ kind: "settings", url: "/settings" });
Expand Down
27 changes: 27 additions & 0 deletions packages/runtime/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ const DEFAULT_CODEX_MODEL = "gpt-5.5";
const DEFAULT_CODEX_AUTH_PATH = "~/.codex/auth.json";
const DEFAULT_DEEPSEEK_BASE_URL = "https://api.deepseek.com/v1";
const DEFAULT_DEEPSEEK_MODEL = "deepseek-v4-flash";
const DEFAULT_ATLASCLOUD_BASE_URL = "https://api.atlascloud.ai/v1";
const DEFAULT_ATLASCLOUD_MODEL = "qwen/qwen3.5-flash";
// Anthropic Messages API (first-party Claude). The default baseUrl is the bare
// host (no /v1) — callAnthropicMessages appends /v1/messages. Auth is the
// ANTHROPIC_API_KEY (sk-ant…) in the x-api-key header. Amazon Bedrock is a
Expand Down Expand Up @@ -156,6 +158,7 @@ const PROVIDER_API_KEY_ENV: Record<string, string> = {
openai: "OPENAI_API_KEY",
openrouter: "OPENROUTER_API_KEY",
deepseek: "DEEPSEEK_API_KEY",
atlascloud: "ATLASCLOUD_API_KEY",
local: "GINI_LOCAL_API_KEY",
anthropic: "ANTHROPIC_API_KEY",
azure: "AZURE_OPENAI_API_KEY"
Expand Down Expand Up @@ -405,6 +408,16 @@ export function providerCatalog(): ProviderCatalogItem[] {
capabilities: ["chat-completions", "tool-calling"],
costHint: "external"
},
{
id: "atlascloud",
name: "atlascloud",
displayName: "Atlas Cloud",
baseUrl: DEFAULT_ATLASCLOUD_BASE_URL,
auth: "env",
models: [DEFAULT_ATLASCLOUD_MODEL, "deepseek-ai/deepseek-v4-pro"],
capabilities: ["chat-completions", "tool-calling", "openai-compatible"],
costHint: "external"
},
{
id: "azure",
name: "azure",
Expand Down Expand Up @@ -450,6 +463,8 @@ export function providerDisplayLabel(name: ProviderName): string {
return "OpenRouter";
case "deepseek":
return "DeepSeek";
case "atlascloud":
return "Atlas Cloud";
case "anthropic":
return "Anthropic";
case "bedrock":
Expand Down Expand Up @@ -3521,6 +3536,7 @@ export async function generateTaskSummary(
provider.name === "openrouter" ||
provider.name === "local" ||
provider.name === "deepseek" ||
provider.name === "atlascloud" ||
// Azure OpenAI exposes deployment-scoped chat/completions, not the flat
// /responses surface this path uses for standard OpenAI — route it through
// the chat-completions builder so the URL + api-key header come out right.
Expand Down Expand Up @@ -3665,6 +3681,7 @@ export async function generateStructured<T>(
provider.name === "local" ||
provider.name === "openai" ||
provider.name === "deepseek" ||
provider.name === "atlascloud" ||
provider.name === "azure"
) {
return callStructuredChatCompletions(provider, request);
Expand Down Expand Up @@ -3893,6 +3910,15 @@ export function normalizeProvider(provider: ProviderConfig): ProviderConfig {
...(extraBody ? { extraBody } : {})
};
}
if (provider.name === "atlascloud") {
return {
name: "atlascloud",
model: provider.model || DEFAULT_ATLASCLOUD_MODEL,
baseUrl: pickBaseUrl(provider.baseUrl, DEFAULT_ATLASCLOUD_BASE_URL),
apiKeyEnv: provider.apiKeyEnv ?? "ATLASCLOUD_API_KEY",
...(provider.extraBody ? { extraBody: provider.extraBody } : {})
};
}
if (provider.name === "codex") {
return {
name: "codex",
Expand Down Expand Up @@ -4839,6 +4865,7 @@ function defaultBaseUrl(provider: ProviderConfig): string {
if (provider.name === "openrouter") return "https://openrouter.ai/api/v1";
if (provider.name === "local") return "http://127.0.0.1:11434/v1";
if (provider.name === "deepseek") return DEFAULT_DEEPSEEK_BASE_URL;
if (provider.name === "atlascloud") return DEFAULT_ATLASCLOUD_BASE_URL;
if (provider.name === "anthropic") return DEFAULT_ANTHROPIC_BASE_URL;
if (provider.name === "bedrock") return bedrockRuntimeBaseUrl(provider.awsRegion ?? DEFAULT_BEDROCK_REGION);
// Azure has no universal default — it is per-resource. Return empty so a
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime/src/runtime/setup-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ describe("setup-api", () => {
test("status: providerConfigured reflects the codex platform default on a fresh instance", () => {
const status = getSetupStatus(config);
expect(status.ok).toBe(true);
expect(status.providers).toEqual(["openai", "codex", "openrouter", "deepseek", "local", "anthropic", "bedrock", "azure"]);
expect(status.providers).toEqual(["openai", "codex", "openrouter", "deepseek", "atlascloud", "local", "anthropic", "bedrock", "azure"]);
// Platform default is "codex". providerHealth treats codex as
// configured when the runtime can find an auth.json; in this
// scratch env there is none (CODEX_AUTH_JSON is scrubbed in
Expand Down
Loading