diff --git a/appcast.xml b/appcast.xml index c1919972b22..bf80ac55964 100644 --- a/appcast.xml +++ b/appcast.xml @@ -245,4 +245,4 @@ - \ No newline at end of file + diff --git a/docs/concepts/model-providers.md b/docs/concepts/model-providers.md index ebcf7e49290..69ecef519f4 100644 --- a/docs/concepts/model-providers.md +++ b/docs/concepts/model-providers.md @@ -252,6 +252,16 @@ OpenClaw ships with the pi‑ai catalog. These providers require **no** See [/providers/kilocode](/providers/kilocode) for setup details. +### DeepInfra + +- Provider: `deepinfra` +- Auth: `DEEPINFRA_API_KEY` +- Example model: `deepinfra/openai/gpt-oss-120b` +- CLI: `openclaw onboard --deepinfra-api-key ` +- Base URL: `https://api.deepinfra.com/v1/openai/` + +See [/providers/deepinfra](/providers/deepinfra) for setup details. + ### Other bundled provider plugins - OpenRouter: `openrouter` (`OPENROUTER_API_KEY`) diff --git a/docs/docs.json b/docs/docs.json index a941bec2601..fdc0386c643 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -1135,6 +1135,7 @@ "providers/cloudflare-ai-gateway", "providers/claude-max-api-proxy", "providers/deepgram", + "providers/deepinfra", "providers/github-copilot", "providers/google", "providers/groq", diff --git a/docs/providers/deepinfra.md b/docs/providers/deepinfra.md new file mode 100644 index 00000000000..71c63372459 --- /dev/null +++ b/docs/providers/deepinfra.md @@ -0,0 +1,62 @@ +--- +summary: "Use DeepInfra's unified API to access the most popular open source models in OpenClaw" +read_when: + - You want a single API key for the top open source LLMs + - You want to run models via DeepInfra's API in OpenClaw +--- + +# DeepInfra + +DeepInfra provides a **unified API** that routes requests to the most popular open source models behind a single +endpoint and API key. It is OpenAI-compatible, so most OpenAI SDKs work by switching the base URL. + +## Getting an API key + +1. Go to [https://deepinfra.com/](https://deepinfra.com/) +2. Sign in or create an account +3. Navigate to Dashboard / Keys and generate a new API key or use the auto created one + +## CLI setup + +```bash +openclaw onboard --deepinfra-api-key +``` + +Or set the environment variable: + +```bash +export DEEPINFRA_API_KEY="" # pragma: allowlist secret +``` + +## Config snippet + +```json5 +{ + env: { DEEPINFRA_API_KEY: "" }, // pragma: allowlist secret + agents: { + defaults: { + model: { primary: "deepinfra/openai/gpt-oss-120b" }, + }, + }, +} +``` + +## Available models + +OpenClaw dynamically discovers available DeepInfra models at startup. Use +`/models deepinfra` to see the full list of models available with your account. + +Any model available on [DeepInfra.com](https://deepinfra.com/) can be used with the `deepinfra/` prefix: + +``` +deepinfra/minimaxai/minimax-m2.5 +deepinfra/zai-org/glm-5 +deepinfra/moonshotai/kimi-k2.5 +...and many more +``` + +## Notes + +- Model refs are `deepinfra//` (e.g., `deepinfra/qwen/qwen3-max`). +- Default model: `deepinfra/openai/gpt-oss-120b` +- Base URL: `https://api.deepinfra.com/v1/openai/` diff --git a/docs/providers/index.md b/docs/providers/index.md index 93ccdf27635..b031e637ca4 100644 --- a/docs/providers/index.md +++ b/docs/providers/index.md @@ -29,6 +29,7 @@ Looking for chat channel docs (WhatsApp/Telegram/Discord/Slack/Mattermost (plugi - [Amazon Bedrock](/providers/bedrock) - [Anthropic (API + Claude Code CLI)](/providers/anthropic) - [Cloudflare AI Gateway](/providers/cloudflare-ai-gateway) +- [DeepInfra](/providers/deepinfra) - [GLM models](/providers/glm) - [Google (Gemini)](/providers/google) - [Groq (LPU inference)](/providers/groq) diff --git a/extensions/deepinfra/index.ts b/extensions/deepinfra/index.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/extensions/deepinfra/provider-catalog.ts b/extensions/deepinfra/provider-catalog.ts new file mode 100644 index 00000000000..d2ac6db1885 --- /dev/null +++ b/extensions/deepinfra/provider-catalog.ts @@ -0,0 +1,34 @@ +import { + DEEPINFRA_BASE_URL, + DEEPINFRA_DEFAULT_CONTEXT_WINDOW, + DEEPINFRA_DEFAULT_COST, + DEEPINFRA_DEFAULT_MAX_TOKENS, + DEEPINFRA_MODEL_CATALOG, +} from "../providers/deepinfra-shared.ts"; + +import { discoverDeepInfraModels } from "./deepinfra-models.js"; + +export async function buildDeepInfraProviderWithDiscovery(): Promise { + const models = await discoverDeepInfraModels(); + return { + baseUrl: DEEPINFRA_BASE_URL, + api: "openai-completions", + models, + }; +} + +export function buildDeepInfraStaticProvider(): ProviderConfig { + return { + baseUrl: DEEPINFRA_BASE_URL, + api: "openai-completions", + models: DEEPINFRA_MODEL_CATALOG.map((model) => ({ + id: model.id, + name: model.name, + reasoning: model.reasoning, + input: model.input, + cost: DEEPINFRA_DEFAULT_COST, + contextWindow: model.contextWindow ?? DEEPINFRA_DEFAULT_CONTEXT_WINDOW, + maxTokens: model.maxTokens ?? DEEPINFRA_DEFAULT_MAX_TOKENS, + })), + }; +} diff --git a/src/agents/deepinfra-models.test.ts b/src/agents/deepinfra-models.test.ts new file mode 100644 index 00000000000..945d527efde --- /dev/null +++ b/src/agents/deepinfra-models.test.ts @@ -0,0 +1,277 @@ +import { describe, expect, it, vi } from "vitest"; +import { discoverDeepInfraModels, DEEPINFRA_MODELS_URL } from "./deepinfra-models.js"; + +// discoverDeepInfraModels checks for VITEST env and returns static catalog, +// so we need to temporarily unset it to test the fetch path. + +function makeModelEntry(overrides: Record = {}) { + return { + id: "openai/gpt-oss-120b", + object: "model", + owned_by: "deepinfra", + metadata: { + description: "A powerful model", + context_length: 131072, + max_tokens: 131072, + pricing: { + input_tokens: 3.0, + output_tokens: 15.0, + cache_read_tokens: 0.3, + }, + tags: ["vision", "reasoning_effort", "prompt_cache"], + }, + ...overrides, + }; +} + +function makeTextOnlyEntry(overrides: Record = {}) { + return makeModelEntry({ + id: "minimaxai/minimax-m2.5", + metadata: { + description: "Text only model", + context_length: 196608, + max_tokens: 196608, + pricing: { + input_tokens: 1.0, + output_tokens: 2.0, + }, + tags: [], + }, + ...overrides, + }); +} + +async function withFetchPathTest( + mockFetch: ReturnType, + runAssertions: () => Promise, +) { + const origNodeEnv = process.env.NODE_ENV; + const origVitest = process.env.VITEST; + delete process.env.NODE_ENV; + delete process.env.VITEST; + + vi.stubGlobal("fetch", mockFetch); + + try { + await runAssertions(); + } finally { + if (origNodeEnv === undefined) { + delete process.env.NODE_ENV; + } else { + process.env.NODE_ENV = origNodeEnv; + } + if (origVitest === undefined) { + delete process.env.VITEST; + } else { + process.env.VITEST = origVitest; + } + vi.unstubAllGlobals(); + } +} + +describe("discoverDeepInfraModels", () => { + it("returns static catalog in test environment", async () => { + const models = await discoverDeepInfraModels(); + expect(models.length).toBeGreaterThan(0); + expect(models.some((m) => m.id === "openai/gpt-oss-120b")).toBe(true); + }); + + it("static catalog has correct defaults for default model", async () => { + const models = await discoverDeepInfraModels(); + const defaultModel = models.find((m) => m.id === "openai/gpt-oss-120b"); + expect(defaultModel).toBeDefined(); + expect(defaultModel?.name).toBe("gpt-oss-120b"); + expect(defaultModel?.reasoning).toBe(true); + expect(defaultModel?.input).toEqual(["text", "image"]); + expect(defaultModel?.contextWindow).toBe(131072); + expect(defaultModel?.maxTokens).toBe(131072); + expect(defaultModel?.cost).toEqual({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }); + }); +}); + +describe("discoverDeepInfraModels (fetch path)", () => { + it("fetches from the correct URL with Accept header", async () => { + const mockFetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ data: [makeModelEntry()] }), + }); + await withFetchPathTest(mockFetch, async () => { + await discoverDeepInfraModels(); + expect(mockFetch).toHaveBeenCalledWith( + DEEPINFRA_MODELS_URL, + expect.objectContaining({ + headers: { Accept: "application/json" }, + }), + ); + }); + }); + + it("parses model pricing correctly", async () => { + const mockFetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ data: [makeModelEntry()] }), + }); + await withFetchPathTest(mockFetch, async () => { + const models = await discoverDeepInfraModels(); + const model = models.find((m) => m.id === "openai/gpt-oss-120b"); + expect(model).toBeDefined(); + expect(model?.cost.input).toBeCloseTo(3.0); + expect(model?.cost.output).toBeCloseTo(15.0); + expect(model?.cost.cacheRead).toBeCloseTo(0.3); + expect(model?.cost.cacheWrite).toBe(0); + }); + }); + + it("detects vision models with image modality", async () => { + const mockFetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ data: [makeModelEntry()] }), + }); + await withFetchPathTest(mockFetch, async () => { + const models = await discoverDeepInfraModels(); + const model = models.find((m) => m.id === "openai/gpt-oss-120b"); + expect(model?.input).toEqual(["text", "image"]); + }); + }); + + it("detects text-only models without image modality", async () => { + const mockFetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ data: [makeTextOnlyEntry()] }), + }); + await withFetchPathTest(mockFetch, async () => { + const models = await discoverDeepInfraModels(); + const model = models.find((m) => m.id === "minimaxai/minimax-m2.5"); + expect(model?.input).toEqual(["text"]); + }); + }); + + it("detects reasoning models via reasoning_effort tag", async () => { + const mockFetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ data: [makeModelEntry(), makeTextOnlyEntry()] }), + }); + await withFetchPathTest(mockFetch, async () => { + const models = await discoverDeepInfraModels(); + expect(models.find((m) => m.id === "openai/gpt-oss-120b")?.reasoning).toBe(true); + expect(models.find((m) => m.id === "minimaxai/minimax-m2.5")?.reasoning).toBe(false); + }); + }); + + it("uses defaults when context_length and max_tokens are missing", async () => { + const entryNoLimits = makeModelEntry({ + id: "some/model", + metadata: { + pricing: { input_tokens: 1, output_tokens: 2 }, + tags: [], + }, + }); + const mockFetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ data: [entryNoLimits] }), + }); + await withFetchPathTest(mockFetch, async () => { + const models = await discoverDeepInfraModels(); + const model = models.find((m) => m.id === "some/model"); + expect(model?.contextWindow).toBe(128000); + expect(model?.maxTokens).toBe(8192); + }); + }); + + it("uses zero cost when pricing fields are missing", async () => { + const entryNoPricing = makeModelEntry({ + id: "some/free-model", + metadata: { + context_length: 32000, + max_tokens: 4096, + tags: [], + }, + }); + const mockFetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ data: [entryNoPricing] }), + }); + await withFetchPathTest(mockFetch, async () => { + const models = await discoverDeepInfraModels(); + const model = models.find((m) => m.id === "some/free-model"); + expect(model?.cost).toEqual({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }); + }); + }); + + it("skips models with null metadata (embeddings, image-gen, etc.)", async () => { + const embeddingEntry = { id: "BAAI/bge-m3", object: "model", metadata: null }; + const mockFetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ data: [embeddingEntry, makeModelEntry()] }), + }); + await withFetchPathTest(mockFetch, async () => { + const models = await discoverDeepInfraModels(); + expect(models.some((m) => m.id === "BAAI/bge-m3")).toBe(false); + expect(models.some((m) => m.id === "openai/gpt-oss-120b")).toBe(true); + }); + }); + + it("deduplicates models with the same id", async () => { + const mockFetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ data: [makeModelEntry(), makeModelEntry()] }), + }); + await withFetchPathTest(mockFetch, async () => { + const models = await discoverDeepInfraModels(); + const matches = models.filter((m) => m.id === "openai/gpt-oss-120b"); + expect(matches.length).toBe(1); + }); + }); + + it("falls back to static catalog on network error", async () => { + const mockFetch = vi.fn().mockRejectedValue(new Error("network error")); + await withFetchPathTest(mockFetch, async () => { + const models = await discoverDeepInfraModels(); + expect(models.length).toBeGreaterThan(0); + expect(models.some((m) => m.id === "openai/gpt-oss-120b")).toBe(true); + }); + }); + + it("falls back to static catalog on HTTP error", async () => { + const mockFetch = vi.fn().mockResolvedValue({ + ok: false, + status: 500, + }); + await withFetchPathTest(mockFetch, async () => { + const models = await discoverDeepInfraModels(); + expect(models.length).toBeGreaterThan(0); + expect(models.some((m) => m.id === "openai/gpt-oss-120b")).toBe(true); + }); + }); + + it("falls back to static catalog when response has empty data array", async () => { + const mockFetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ data: [] }), + }); + await withFetchPathTest(mockFetch, async () => { + const models = await discoverDeepInfraModels(); + expect(models.length).toBeGreaterThan(0); + expect(models.some((m) => m.id === "openai/gpt-oss-120b")).toBe(true); + }); + }); + + it("falls back to static catalog when all entries have null metadata", async () => { + const mockFetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => + Promise.resolve({ + data: [ + { id: "BAAI/bge-m3", metadata: null }, + { id: "stabilityai/sdxl", metadata: null }, + ], + }), + }); + await withFetchPathTest(mockFetch, async () => { + const models = await discoverDeepInfraModels(); + expect(models.length).toBeGreaterThan(0); + // Falls back to static catalog + expect(models.some((m) => m.id === "openai/gpt-oss-120b")).toBe(true); + }); + }); +}); diff --git a/src/agents/deepinfra-models.ts b/src/agents/deepinfra-models.ts new file mode 100644 index 00000000000..b16312b9847 --- /dev/null +++ b/src/agents/deepinfra-models.ts @@ -0,0 +1,154 @@ +import type { ModelDefinitionConfig } from "../config/types.js"; +import { createSubsystemLogger } from "../logging/subsystem.js"; +import { + DEEPINFRA_BASE_URL, + DEEPINFRA_DEFAULT_CONTEXT_WINDOW, + DEEPINFRA_DEFAULT_COST, + DEEPINFRA_DEFAULT_MAX_TOKENS, + DEEPINFRA_MODEL_CATALOG, +} from "../providers/deepinfra-shared.js"; + +const log = createSubsystemLogger("deepinfra-models"); + +export const DEEPINFRA_MODELS_URL = `${DEEPINFRA_BASE_URL}models`; + +const DISCOVERY_TIMEOUT_MS = 5000; + +// --------------------------------------------------------------------------- +// API response types (DeepInfra OpenAI-compatible /models schema) +// --------------------------------------------------------------------------- + +interface DeepInfraModelPricing { + input_tokens?: number; + output_tokens?: number; + cache_read_tokens?: number; +} + +interface DeepInfraModelMetadata { + description?: string; + context_length?: number; + max_tokens?: number; + pricing?: DeepInfraModelPricing; + /** e.g. ["vision", "reasoning_effort", "prompt_cache"] */ + tags?: string[]; +} + +interface DeepInfraModelEntry { + id: string; + object?: string; + owned_by?: string; + metadata: DeepInfraModelMetadata | null; +} + +interface DeepInfraModelsResponse { + data: DeepInfraModelEntry[]; +} + +// --------------------------------------------------------------------------- +// Model parsing +// --------------------------------------------------------------------------- + +function parseModality(metadata: DeepInfraModelMetadata): Array<"text" | "image"> { + const hasVision = metadata.tags?.includes("vision") ?? false; + return hasVision ? ["text", "image"] : ["text"]; +} + +function parseReasoning(metadata: DeepInfraModelMetadata): boolean { + return metadata.tags?.includes("reasoning_effort") ?? false; +} + +function toModelDefinition(entry: DeepInfraModelEntry): ModelDefinitionConfig { + // metadata is guaranteed non-null at call site + const meta = entry.metadata!; + return { + id: entry.id, + name: entry.id, + reasoning: parseReasoning(meta), + input: parseModality(meta), + cost: { + input: meta.pricing?.input_tokens ?? 0, + output: meta.pricing?.output_tokens ?? 0, + cacheRead: meta.pricing?.cache_read_tokens ?? 0, + cacheWrite: 0, + }, + contextWindow: meta.context_length ?? DEEPINFRA_DEFAULT_CONTEXT_WINDOW, + maxTokens: meta.max_tokens ?? DEEPINFRA_DEFAULT_MAX_TOKENS, + }; +} + +// --------------------------------------------------------------------------- +// Static fallback +// --------------------------------------------------------------------------- + +function buildStaticCatalog(): ModelDefinitionConfig[] { + return DEEPINFRA_MODEL_CATALOG.map((model) => ({ + id: model.id, + name: model.name, + reasoning: model.reasoning, + input: model.input, + cost: DEEPINFRA_DEFAULT_COST, + contextWindow: model.contextWindow ?? DEEPINFRA_DEFAULT_CONTEXT_WINDOW, + maxTokens: model.maxTokens ?? DEEPINFRA_DEFAULT_MAX_TOKENS, + })); +} + +// --------------------------------------------------------------------------- +// Discovery +// --------------------------------------------------------------------------- + +/** + * Discover models from the DeepInfra API with fallback to static catalog. + * Skips models with null metadata (embeddings, image-gen, etc.). + */ +export async function discoverDeepInfraModels(): Promise { + // Skip API discovery in test environment + if (process.env.NODE_ENV === "test" || process.env.VITEST) { + return buildStaticCatalog(); + } + + try { + const response = await fetch(DEEPINFRA_MODELS_URL, { + headers: { Accept: "application/json" }, + signal: AbortSignal.timeout(DISCOVERY_TIMEOUT_MS), + }); + + if (!response.ok) { + log.warn(`Failed to discover models: HTTP ${response.status}, using static catalog`); + return buildStaticCatalog(); + } + + const data = (await response.json()) as DeepInfraModelsResponse; + if (!Array.isArray(data.data) || data.data.length === 0) { + log.warn("No models found from DeepInfra API, using static catalog"); + return buildStaticCatalog(); + } + + const models: ModelDefinitionConfig[] = []; + const discoveredIds = new Set(); + + for (const entry of data.data) { + if (!entry || typeof entry !== "object") { + continue; + } + const id = typeof entry.id === "string" ? entry.id.trim() : ""; + if (!id || discoveredIds.has(id)) { + continue; + } + // Skip non-completion models (embeddings, image-gen, etc.) + if (entry.metadata === null) { + continue; + } + try { + models.push(toModelDefinition(entry)); + discoveredIds.add(id); + } catch (e) { + log.warn(`Skipping malformed model entry "${id}": ${String(e)}`); + } + } + + return models.length > 0 ? models : buildStaticCatalog(); + } catch (error) { + log.warn(`Discovery failed: ${String(error)}, using static catalog`); + return buildStaticCatalog(); + } +} diff --git a/src/agents/models-config.e2e-harness.ts b/src/agents/models-config.e2e-harness.ts index 81518ec9aee..f39c80c4f49 100644 --- a/src/agents/models-config.e2e-harness.ts +++ b/src/agents/models-config.e2e-harness.ts @@ -109,6 +109,7 @@ export const MODELS_CONFIG_IMPLICIT_ENV_VARS = [ "VOLCANO_ENGINE_API_KEY", "BYTEPLUS_API_KEY", "KILOCODE_API_KEY", + "DEEPINFRA_API_KEY", "KIMI_API_KEY", "KIMICODE_API_KEY", "GEMINI_API_KEY", diff --git a/src/agents/models-config.providers.deepinfra.test.ts b/src/agents/models-config.providers.deepinfra.test.ts new file mode 100644 index 00000000000..7f3cc846cf2 --- /dev/null +++ b/src/agents/models-config.providers.deepinfra.test.ts @@ -0,0 +1,66 @@ +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { captureEnv } from "../test-utils/env.ts"; +import { resolveImplicitProvidersForTest } from "./models-config.e2e-harness.ts"; +import { buildDeepInfraStaticProvider } from "./models-config.providers.ts"; + +const DEEPINFRA_MODEL_IDS = [ + "openai/gpt-oss-120b", + "minimaxai/minimax-m2.5", + "zai-org/glm-5", + "moonshotai/kimi-k2.5" +]; + +describe("DeepInfra implicit provider", () => { + it("should include deepinfra when DEEPINFRA_API_KEY is configured", async () => { + const agentDir = mkdtempSync(join(tmpdir(), "openclaw-test-")); + const envSnapshot = captureEnv(["DEEPINFRA_API_KEY"]); + process.env.DEEPINFRA_API_KEY = "test-key"; // pragma: allowlist secret + + try { + const providers = await resolveImplicitProvidersForTest({ agentDir }); + expect(providers?.deepinfra).toBeDefined(); + expect(providers?.deepinfra?.models?.length).toBeGreaterThan(0); + } finally { + envSnapshot.restore(); + } + }); + + it("should not include deepinfra when no API key is configured", async () => { + const agentDir = mkdtempSync(join(tmpdir(), "openclaw-test-")); + const envSnapshot = captureEnv(["DEEPINFRA_API_KEY"]); + delete process.env.DEEPINFRA_API_KEY; + + try { + const providers = await resolveImplicitProvidersForTest({ agentDir }); + expect(providers?.deepinfra).toBeUndefined(); + } finally { + envSnapshot.restore(); + } + }); + + it("should build deepinfra provider with correct configuration", () => { + const provider = buildDeepInfraStaticProvider(); + expect(provider.baseUrl).toBe("https://api.deepinfra.com/v1/openai/"); + expect(provider.api).toBe("openai-completions"); + expect(provider.models).toBeDefined(); + expect(provider.models.length).toBeGreaterThan(0); + }); + + it("should include the default deepinfra model", () => { + const provider = buildDeepInfraStaticProvider(); + const modelIds = provider.models.map((m) => m.id); + expect(modelIds).toContain("deepinfra/openai/gpt-oss-120b"); + }); + + it("should include the static fallback catalog", () => { + const provider = buildDeepInfraStaticProvider(); + const modelIds = provider.models.map((m) => m.id); + for (const modelId of DEEPINFRA_MODEL_IDS) { + expect(modelIds).toContain(modelId); + } + expect(provider.models).toHaveLength(DEEPINFRA_MODEL_IDS.length); + }); +}); diff --git a/src/agents/models-config.providers.discovery.ts b/src/agents/models-config.providers.discovery.ts index b138c4853d1..ee06a272ee9 100644 --- a/src/agents/models-config.providers.discovery.ts +++ b/src/agents/models-config.providers.discovery.ts @@ -19,6 +19,7 @@ import { SGLANG_DEFAULT_BASE_URL, SGLANG_PROVIDER_LABEL } from "./sglang-default import { VLLM_DEFAULT_BASE_URL, VLLM_PROVIDER_LABEL } from "./vllm-defaults.js"; export { buildHuggingfaceProvider } from "../../extensions/huggingface/provider-catalog.js"; export { buildKilocodeProviderWithDiscovery } from "../../extensions/kilocode/provider-catalog.js"; +export { buildDeepInfraProviderWithDiscovery } from "../../extensions/deepinfra/provider-catalog.js"; export { buildVeniceProvider } from "../../extensions/venice/provider-catalog.js"; export { buildVercelAiGatewayProvider } from "../../extensions/vercel-ai-gateway/provider-catalog.js"; diff --git a/src/agents/models-config.providers.static.ts b/src/agents/models-config.providers.static.ts index 71184e12286..d1cd2f51310 100644 --- a/src/agents/models-config.providers.static.ts +++ b/src/agents/models-config.providers.static.ts @@ -4,6 +4,7 @@ export { } from "../../extensions/byteplus/provider-catalog.js"; export { buildKimiCodingProvider } from "../../extensions/kimi-coding/provider-catalog.js"; export { buildKilocodeProvider } from "../../extensions/kilocode/provider-catalog.js"; +export { buildDeepInfraStaticProvider } from "../../extensions/deepinfra/provider-catalog.js"; export { buildMinimaxPortalProvider, buildMinimaxProvider, diff --git a/src/agents/pi-embedded-runner/deepinfra.test.ts b/src/agents/pi-embedded-runner/deepinfra.test.ts new file mode 100644 index 00000000000..710514637e5 --- /dev/null +++ b/src/agents/pi-embedded-runner/deepinfra.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from "vitest"; +import { isCacheTtlEligibleProvider } from "./cache-ttl.ts"; + +describe("deepinfra cache-ttl eligibility", () => { + it("is eligible when model starts with zai", () => { + expect(isCacheTtlEligibleProvider("deepinfra", "zai-org/glm-5")).toBe(true); + }); + + it("is eligible when model starts with moonshot", () => { + expect(isCacheTtlEligibleProvider("deepinfra", "moonshotai/kimi-k2.5")).toBe(true); + }); + + it("is not eligible for other models on deepinfra", () => { + expect(isCacheTtlEligibleProvider("deepinfra", "openai/gpt-oss-120b")).toBe(false); + }); + + it("is case-insensitive for provider name", () => { + expect(isCacheTtlEligibleProvider("DeepInfra", "moonshotai/kimi-k2.5")).toBe(true); + expect(isCacheTtlEligibleProvider("DEEPINFRA", "Moonshotai/kimi-k2.5")).toBe(true); + }); +}); diff --git a/src/agents/provider-capabilities.test.ts b/src/agents/provider-capabilities.test.ts index 1712f6f810e..cd560471122 100644 --- a/src/agents/provider-capabilities.test.ts +++ b/src/agents/provider-capabilities.test.ts @@ -119,6 +119,12 @@ describe("resolveProviderCapabilities", () => { modelId: "gemini-2.0-flash", }), ).toBe(true); + expect( + shouldSanitizeGeminiThoughtSignaturesForModel({ + provider: "deepinfra", + modelId: "google/gemini-2.5-pro", + }), + ).toBe(true); expect( shouldSanitizeGeminiThoughtSignaturesForModel({ provider: "opencode-go", diff --git a/src/agents/transcript-policy.test.ts b/src/agents/transcript-policy.test.ts index 7409e7a4b12..3cd8aa82a05 100644 --- a/src/agents/transcript-policy.test.ts +++ b/src/agents/transcript-policy.test.ts @@ -158,6 +158,7 @@ describe("resolveTranscriptPolicy", () => { { provider: "openrouter", modelId: "google/gemini-2.5-pro-preview" }, { provider: "opencode", modelId: "google/gemini-2.5-flash" }, { provider: "kilocode", modelId: "gemini-2.0-flash" }, + { provider: "deepinfra", modelId: "google/gemini-2.5-pro" }, ])("sanitizes Gemini thought signatures for $provider routes", ({ provider, modelId }) => { const policy = resolveTranscriptPolicy({ provider, diff --git a/src/commands/onboard-auth.config-core.deepinfra.test.ts b/src/commands/onboard-auth.config-core.deepinfra.test.ts new file mode 100644 index 00000000000..bc3c19f6b9c --- /dev/null +++ b/src/commands/onboard-auth.config-core.deepinfra.test.ts @@ -0,0 +1,197 @@ +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { resolveApiKeyForProvider, resolveEnvApiKey } from "../agents/model-auth.js"; +import type { OpenClawConfig } from "../config/config.js"; +import { resolveAgentModelPrimaryValue } from "../config/model-input.js"; +import { + DEEPINFRA_BASE_URL, + DEEPINFRA_DEFAULT_MODEL_ID, + DEEPINFRA_DEFAULT_MODEL_REF, + DEEPINFRA_DEFAULT_CONTEXT_WINDOW, + DEEPINFRA_DEFAULT_MAX_TOKENS, + DEEPINFRA_DEFAULT_COST, + DEEPINFRA_MODEL_CATALOG, +} from "../providers/deepinfra-shared.js"; +import { captureEnv } from "../test-utils/env.js"; +import { applyDeepInfraProviderConfig, applyDeepInfraConfig } from "./onboard-auth.config-core.js"; + +const emptyCfg: OpenClawConfig = {}; +const DEEPINFRA_MODEL_IDS = DEEPINFRA_MODEL_CATALOG.map((m) => m.id); + +describe("DeepInfra provider config", () => { + describe("constants", () => { + it("DEEPINFRA_BASE_URL points to DeepInfra OpenAI-compatible endpoint", () => { + expect(DEEPINFRA_BASE_URL).toBe("https://api.deepinfra.com/v1/openai/"); + }); + + it("DEEPINFRA_DEFAULT_MODEL_REF includes provider prefix", () => { + expect(DEEPINFRA_DEFAULT_MODEL_REF).toBe(`deepinfra/${DEEPINFRA_DEFAULT_MODEL_ID}`); + }); + + it("DEEPINFRA_DEFAULT_MODEL_ID is openai/gpt-oss-120b", () => { + expect(DEEPINFRA_DEFAULT_MODEL_ID).toBe("openai/gpt-oss-120b"); + }); + + it("DEEPINFRA_DEFAULT_CONTEXT_WINDOW is 128000", () => { + expect(DEEPINFRA_DEFAULT_CONTEXT_WINDOW).toBe(128000); + }); + + it("DEEPINFRA_DEFAULT_MAX_TOKENS is 8192", () => { + expect(DEEPINFRA_DEFAULT_MAX_TOKENS).toBe(8192); + }); + + it("DEEPINFRA_DEFAULT_COST has zero values", () => { + expect(DEEPINFRA_DEFAULT_COST).toEqual({ + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }); + }); + }); + + describe("applyDeepInfraProviderConfig", () => { + it("registers deepinfra provider with correct baseUrl and api", () => { + const result = applyDeepInfraProviderConfig(emptyCfg); + const provider = result.models?.providers?.deepinfra; + expect(provider).toBeDefined(); + expect(provider?.baseUrl).toBe(DEEPINFRA_BASE_URL); + expect(provider?.api).toBe("openai-completions"); + }); + + it("includes the default model in the provider model list", () => { + const result = applyDeepInfraProviderConfig(emptyCfg); + const provider = result.models?.providers?.deepinfra; + const models = provider?.models; + expect(Array.isArray(models)).toBe(true); + const modelIds = models?.map((m) => m.id) ?? []; + expect(modelIds).toContain(DEEPINFRA_DEFAULT_MODEL_ID); + }); + + it("surfaces the full DeepInfra model catalog", () => { + const result = applyDeepInfraProviderConfig(emptyCfg); + const provider = result.models?.providers?.deepinfra; + const modelIds = provider?.models?.map((m) => m.id) ?? []; + for (const modelId of DEEPINFRA_MODEL_IDS) { + expect(modelIds).toContain(modelId); + } + }); + + it("appends missing catalog models to existing DeepInfra provider config", () => { + const result = applyDeepInfraProviderConfig({ + models: { + providers: { + deepinfra: { + baseUrl: DEEPINFRA_BASE_URL, + api: "openai-completions", + models: [{ ...DEEPINFRA_MODEL_CATALOG[0], cost: DEEPINFRA_DEFAULT_COST }], + }, + }, + }, + }); + const modelIds = result.models?.providers?.deepinfra?.models?.map((m) => m.id) ?? []; + for (const modelId of DEEPINFRA_MODEL_IDS) { + expect(modelIds).toContain(modelId); + } + }); + + it("sets DeepInfra alias in agent default models", () => { + const result = applyDeepInfraProviderConfig(emptyCfg); + const agentModel = result.agents?.defaults?.models?.[DEEPINFRA_DEFAULT_MODEL_REF]; + expect(agentModel).toBeDefined(); + expect(agentModel?.alias).toBe("DeepInfra"); + }); + + it("preserves existing alias if already set", () => { + const cfg: OpenClawConfig = { + agents: { + defaults: { + models: { + [DEEPINFRA_DEFAULT_MODEL_REF]: { alias: "My Custom Alias" }, + }, + }, + }, + }; + const result = applyDeepInfraProviderConfig(cfg); + const agentModel = result.agents?.defaults?.models?.[DEEPINFRA_DEFAULT_MODEL_REF]; + expect(agentModel?.alias).toBe("My Custom Alias"); + }); + + it("does not change the default model selection", () => { + const cfg: OpenClawConfig = { + agents: { + defaults: { + model: { primary: "openai/gpt-5" }, + }, + }, + }; + const result = applyDeepInfraProviderConfig(cfg); + expect(resolveAgentModelPrimaryValue(result.agents?.defaults?.model)).toBe("openai/gpt-5"); + }); + }); + + describe("applyDeepInfraConfig", () => { + it("sets deepinfra's default model as the config's default model", () => { + const result = applyDeepInfraConfig(emptyCfg); + expect(resolveAgentModelPrimaryValue(result.agents?.defaults?.model)).toBe( + DEEPINFRA_DEFAULT_MODEL_REF, + ); + }); + + it("also registers the provider", () => { + const result = applyDeepInfraConfig(emptyCfg); + const provider = result.models?.providers?.deepinfra; + expect(provider).toBeDefined(); + expect(provider?.baseUrl).toBe(DEEPINFRA_BASE_URL); + }); + }); + + describe("env var resolution", () => { + it("resolves DEEPINFRA_API_KEY from env", () => { + const envSnapshot = captureEnv(["DEEPINFRA_API_KEY"]); + process.env.DEEPINFRA_API_KEY = "test-deepinfra-key"; // pragma: allowlist secret + + try { + const result = resolveEnvApiKey("deepinfra"); + expect(result).not.toBeNull(); + expect(result?.apiKey).toBe("test-deepinfra-key"); + expect(result?.source).toContain("DEEPINFRA_API_KEY"); + } finally { + envSnapshot.restore(); + } + }); + + it("returns null when DEEPINFRA_API_KEY is not set", () => { + const envSnapshot = captureEnv(["DEEPINFRA_API_KEY"]); + delete process.env.DEEPINFRA_API_KEY; + + try { + const result = resolveEnvApiKey("deepinfra"); + expect(result).toBeNull(); + } finally { + envSnapshot.restore(); + } + }); + + it("resolves the deepinfra api key via resolveApiKeyForProvider", async () => { + const agentDir = mkdtempSync(join(tmpdir(), "openclaw-test-")); + const envSnapshot = captureEnv(["DEEPINFRA_API_KEY"]); + process.env.DEEPINFRA_API_KEY = "deepinfra-provider-test-key"; // pragma: allowlist secret + + try { + const auth = await resolveApiKeyForProvider({ + provider: "deepinfra", + agentDir, + }); + + expect(auth.apiKey).toBe("deepinfra-provider-test-key"); + expect(auth.mode).toBe("api-key"); + expect(auth.source).toContain("DEEPINFRA_API_KEY"); + } finally { + envSnapshot.restore(); + } + }); + }); +}); diff --git a/src/commands/onboard-auth.ts b/src/commands/onboard-auth.ts new file mode 100644 index 00000000000..ab154bbc58f --- /dev/null +++ b/src/commands/onboard-auth.ts @@ -0,0 +1,141 @@ +export { + SYNTHETIC_DEFAULT_MODEL_ID, + SYNTHETIC_DEFAULT_MODEL_REF, +} from "../agents/synthetic-models.js"; +export { VENICE_DEFAULT_MODEL_ID, VENICE_DEFAULT_MODEL_REF } from "../agents/venice-models.js"; +export { + applyAuthProfileConfig, + applyCloudflareAiGatewayConfig, + applyCloudflareAiGatewayProviderConfig, + applyHuggingfaceConfig, + applyHuggingfaceProviderConfig, + applyDeepInfraConfig, + applyDeepInfraProviderConfig, + applyKilocodeConfig, + applyKilocodeProviderConfig, + applyQianfanConfig, + applyQianfanProviderConfig, + applyKimiCodeConfig, + applyKimiCodeProviderConfig, + applyLitellmConfig, + applyLitellmProviderConfig, + applyMistralConfig, + applyMistralProviderConfig, + applyMoonshotConfig, + applyMoonshotConfigCn, + applyMoonshotProviderConfig, + applyMoonshotProviderConfigCn, + applyOpenrouterConfig, + applyOpenrouterProviderConfig, + applySyntheticConfig, + applySyntheticProviderConfig, + applyTogetherConfig, + applyTogetherProviderConfig, + applyVeniceConfig, + applyVeniceProviderConfig, + applyVercelAiGatewayConfig, + applyVercelAiGatewayProviderConfig, + applyXaiConfig, + applyXaiProviderConfig, + applyXiaomiConfig, + applyXiaomiProviderConfig, + applyZaiConfig, + applyZaiProviderConfig, + applyModelStudioConfig, + applyModelStudioConfigCn, + applyModelStudioProviderConfig, + applyModelStudioProviderConfigCn, + KILOCODE_BASE_URL, +} from "./onboard-auth.config-core.js"; +export { + applyMinimaxApiConfig, + applyMinimaxApiConfigCn, + applyMinimaxApiProviderConfig, + applyMinimaxApiProviderConfigCn, + applyMinimaxConfig, + applyMinimaxHostedConfig, + applyMinimaxHostedProviderConfig, + applyMinimaxProviderConfig, +} from "./onboard-auth.config-minimax.js"; + +export { + applyOpencodeZenConfig, + applyOpencodeZenProviderConfig, +} from "./onboard-auth.config-opencode.js"; +export { + applyOpencodeGoConfig, + applyOpencodeGoProviderConfig, +} from "./onboard-auth.config-opencode-go.js"; +export { + CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF, + DEEPINFRA_DEFAULT_MODEL_REF, + KILOCODE_DEFAULT_MODEL_REF, + LITELLM_DEFAULT_MODEL_REF, + OPENROUTER_DEFAULT_MODEL_REF, + setOpenaiApiKey, + setAnthropicApiKey, + setCloudflareAiGatewayConfig, + setByteplusApiKey, + setQianfanApiKey, + setGeminiApiKey, + setDeepInfraApiKey, + setKilocodeApiKey, + setLitellmApiKey, + setKimiCodingApiKey, + setMinimaxApiKey, + setMistralApiKey, + setMoonshotApiKey, + setOpencodeGoApiKey, + setOpencodeZenApiKey, + setOpenrouterApiKey, + setSyntheticApiKey, + setTogetherApiKey, + setHuggingfaceApiKey, + setVeniceApiKey, + setVercelAiGatewayApiKey, + setXiaomiApiKey, + setVolcengineApiKey, + setZaiApiKey, + setXaiApiKey, + setModelStudioApiKey, + writeOAuthCredentials, + HUGGINGFACE_DEFAULT_MODEL_REF, + VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF, + XIAOMI_DEFAULT_MODEL_REF, + ZAI_DEFAULT_MODEL_REF, + TOGETHER_DEFAULT_MODEL_REF, + MISTRAL_DEFAULT_MODEL_REF, + XAI_DEFAULT_MODEL_REF, + MODELSTUDIO_DEFAULT_MODEL_REF, +} from "./onboard-auth.credentials.js"; +export { + buildKilocodeModelDefinition, + buildMinimaxApiModelDefinition, + buildMinimaxModelDefinition, + buildMistralModelDefinition, + buildMoonshotModelDefinition, + buildZaiModelDefinition, + DEFAULT_MINIMAX_BASE_URL, + KILOCODE_DEFAULT_MODEL_ID, + MOONSHOT_CN_BASE_URL, + QIANFAN_BASE_URL, + QIANFAN_DEFAULT_MODEL_ID, + QIANFAN_DEFAULT_MODEL_REF, + KIMI_CODING_MODEL_ID, + KIMI_CODING_MODEL_REF, + MINIMAX_API_BASE_URL, + MINIMAX_CN_API_BASE_URL, + MINIMAX_HOSTED_MODEL_ID, + MINIMAX_HOSTED_MODEL_REF, + MOONSHOT_BASE_URL, + MOONSHOT_DEFAULT_MODEL_ID, + MOONSHOT_DEFAULT_MODEL_REF, + MISTRAL_BASE_URL, + MISTRAL_DEFAULT_MODEL_ID, + resolveZaiBaseUrl, + ZAI_CODING_CN_BASE_URL, + ZAI_DEFAULT_MODEL_ID, + ZAI_CODING_GLOBAL_BASE_URL, + ZAI_CN_BASE_URL, + ZAI_GLOBAL_BASE_URL, +} from "./onboard-auth.models.js"; diff --git a/src/commands/onboard-provider-auth-flags.ts b/src/commands/onboard-provider-auth-flags.ts new file mode 100644 index 00000000000..3de6dfc322e --- /dev/null +++ b/src/commands/onboard-provider-auth-flags.ts @@ -0,0 +1,233 @@ +import type { AuthChoice, OnboardOptions } from "./onboard-types.js"; + +type OnboardProviderAuthOptionKey = keyof Pick< + OnboardOptions, + | "anthropicApiKey" + | "openaiApiKey" + | "mistralApiKey" + | "openrouterApiKey" + | "kilocodeApiKey" + | "deepinfraApiKey" + | "aiGatewayApiKey" + | "cloudflareAiGatewayApiKey" + | "moonshotApiKey" + | "kimiCodeApiKey" + | "geminiApiKey" + | "zaiApiKey" + | "xiaomiApiKey" + | "minimaxApiKey" + | "syntheticApiKey" + | "veniceApiKey" + | "togetherApiKey" + | "huggingfaceApiKey" + | "opencodeZenApiKey" + | "opencodeGoApiKey" + | "xaiApiKey" + | "litellmApiKey" + | "qianfanApiKey" + | "modelstudioApiKeyCn" + | "modelstudioApiKey" + | "volcengineApiKey" + | "byteplusApiKey" +>; + +export type OnboardProviderAuthFlag = { + optionKey: OnboardProviderAuthOptionKey; + authChoice: AuthChoice; + cliFlag: `--${string}`; + cliOption: `--${string} `; + description: string; +}; + +// Shared source for provider API-key flags used by CLI registration + non-interactive inference. +export const ONBOARD_PROVIDER_AUTH_FLAGS: ReadonlyArray = [ + { + optionKey: "anthropicApiKey", + authChoice: "apiKey", + cliFlag: "--anthropic-api-key", + cliOption: "--anthropic-api-key ", + description: "Anthropic API key", + }, + { + optionKey: "openaiApiKey", + authChoice: "openai-api-key", + cliFlag: "--openai-api-key", + cliOption: "--openai-api-key ", + description: "OpenAI API key", + }, + { + optionKey: "mistralApiKey", + authChoice: "mistral-api-key", + cliFlag: "--mistral-api-key", + cliOption: "--mistral-api-key ", + description: "Mistral API key", + }, + { + optionKey: "openrouterApiKey", + authChoice: "openrouter-api-key", + cliFlag: "--openrouter-api-key", + cliOption: "--openrouter-api-key ", + description: "OpenRouter API key", + }, + { + optionKey: "kilocodeApiKey", + authChoice: "kilocode-api-key", + cliFlag: "--kilocode-api-key", + cliOption: "--kilocode-api-key ", + description: "Kilo Gateway API key", + }, + { + optionKey: "deepinfraApiKey", + authChoice: "deepinfra-api-key", + cliFlag: "--deepinfra-api-key", + cliOption: "--deepinfra-api-key ", + description: "DeepInfra API key", + }, + { + optionKey: "aiGatewayApiKey", + authChoice: "ai-gateway-api-key", + cliFlag: "--ai-gateway-api-key", + cliOption: "--ai-gateway-api-key ", + description: "Vercel AI Gateway API key", + }, + { + optionKey: "cloudflareAiGatewayApiKey", + authChoice: "cloudflare-ai-gateway-api-key", + cliFlag: "--cloudflare-ai-gateway-api-key", + cliOption: "--cloudflare-ai-gateway-api-key ", + description: "Cloudflare AI Gateway API key", + }, + { + optionKey: "moonshotApiKey", + authChoice: "moonshot-api-key", + cliFlag: "--moonshot-api-key", + cliOption: "--moonshot-api-key ", + description: "Moonshot API key", + }, + { + optionKey: "kimiCodeApiKey", + authChoice: "kimi-code-api-key", + cliFlag: "--kimi-code-api-key", + cliOption: "--kimi-code-api-key ", + description: "Kimi Coding API key", + }, + { + optionKey: "geminiApiKey", + authChoice: "gemini-api-key", + cliFlag: "--gemini-api-key", + cliOption: "--gemini-api-key ", + description: "Gemini API key", + }, + { + optionKey: "zaiApiKey", + authChoice: "zai-api-key", + cliFlag: "--zai-api-key", + cliOption: "--zai-api-key ", + description: "Z.AI API key", + }, + { + optionKey: "xiaomiApiKey", + authChoice: "xiaomi-api-key", + cliFlag: "--xiaomi-api-key", + cliOption: "--xiaomi-api-key ", + description: "Xiaomi API key", + }, + { + optionKey: "minimaxApiKey", + authChoice: "minimax-api", + cliFlag: "--minimax-api-key", + cliOption: "--minimax-api-key ", + description: "MiniMax API key", + }, + { + optionKey: "syntheticApiKey", + authChoice: "synthetic-api-key", + cliFlag: "--synthetic-api-key", + cliOption: "--synthetic-api-key ", + description: "Synthetic API key", + }, + { + optionKey: "veniceApiKey", + authChoice: "venice-api-key", + cliFlag: "--venice-api-key", + cliOption: "--venice-api-key ", + description: "Venice API key", + }, + { + optionKey: "togetherApiKey", + authChoice: "together-api-key", + cliFlag: "--together-api-key", + cliOption: "--together-api-key ", + description: "Together AI API key", + }, + { + optionKey: "huggingfaceApiKey", + authChoice: "huggingface-api-key", + cliFlag: "--huggingface-api-key", + cliOption: "--huggingface-api-key ", + description: "Hugging Face API key (HF token)", + }, + { + optionKey: "opencodeZenApiKey", + authChoice: "opencode-zen", + cliFlag: "--opencode-zen-api-key", + cliOption: "--opencode-zen-api-key ", + description: "OpenCode API key (Zen catalog)", + }, + { + optionKey: "opencodeGoApiKey", + authChoice: "opencode-go", + cliFlag: "--opencode-go-api-key", + cliOption: "--opencode-go-api-key ", + description: "OpenCode API key (Go catalog)", + }, + { + optionKey: "xaiApiKey", + authChoice: "xai-api-key", + cliFlag: "--xai-api-key", + cliOption: "--xai-api-key ", + description: "xAI API key", + }, + { + optionKey: "litellmApiKey", + authChoice: "litellm-api-key", + cliFlag: "--litellm-api-key", + cliOption: "--litellm-api-key ", + description: "LiteLLM API key", + }, + { + optionKey: "qianfanApiKey", + authChoice: "qianfan-api-key", + cliFlag: "--qianfan-api-key", + cliOption: "--qianfan-api-key ", + description: "QIANFAN API key", + }, + { + optionKey: "modelstudioApiKeyCn", + authChoice: "modelstudio-api-key-cn", + cliFlag: "--modelstudio-api-key-cn", + cliOption: "--modelstudio-api-key-cn ", + description: "Alibaba Cloud Model Studio Coding Plan API key (China)", + }, + { + optionKey: "modelstudioApiKey", + authChoice: "modelstudio-api-key", + cliFlag: "--modelstudio-api-key", + cliOption: "--modelstudio-api-key ", + description: "Alibaba Cloud Model Studio Coding Plan API key (Global/Intl)", + }, + { + optionKey: "volcengineApiKey", + authChoice: "volcengine-api-key", + cliFlag: "--volcengine-api-key", + cliOption: "--volcengine-api-key ", + description: "Volcano Engine API key", + }, + { + optionKey: "byteplusApiKey", + authChoice: "byteplus-api-key", + cliFlag: "--byteplus-api-key", + cliOption: "--byteplus-api-key ", + description: "BytePlus API key", + }, +]; diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts index 832fae75448..6161fcafb00 100644 --- a/src/commands/onboard-types.ts +++ b/src/commands/onboard-types.ts @@ -14,6 +14,7 @@ export type BuiltInAuthChoice = | "openai-api-key" | "openrouter-api-key" | "kilocode-api-key" + | "deepinfra-api-key" | "litellm-api-key" | "ai-gateway-api-key" | "cloudflare-ai-gateway-api-key" @@ -62,6 +63,7 @@ export type BuiltInAuthChoiceGroupId = | "copilot" | "openrouter" | "kilocode" + | "deepinfra" | "litellm" | "ai-gateway" | "cloudflare-ai-gateway" @@ -119,6 +121,7 @@ export type OnboardOptions = { mistralApiKey?: string; openrouterApiKey?: string; kilocodeApiKey?: string; + deepinfraApiKey?: string; litellmApiKey?: string; aiGatewayApiKey?: string; cloudflareAiGatewayAccountId?: string; diff --git a/src/config/io.ts b/src/config/io.ts index fba17f253aa..214ac6210ec 100644 --- a/src/config/io.ts +++ b/src/config/io.ts @@ -71,6 +71,7 @@ const SHELL_ENV_EXPECTED_KEYS = [ "MODELSTUDIO_API_KEY", "SYNTHETIC_API_KEY", "KILOCODE_API_KEY", + "DEEPINFRA_API_KEY", "ELEVENLABS_API_KEY", "TELEGRAM_BOT_TOKEN", "DISCORD_BOT_TOKEN", diff --git a/src/plugins/provider-auth-storage.ts b/src/plugins/provider-auth-storage.ts index d8e15115902..97ac8ea466f 100644 --- a/src/plugins/provider-auth-storage.ts +++ b/src/plugins/provider-auth-storage.ts @@ -1,6 +1,7 @@ import { resolveOpenClawAgentDir } from "../agents/agent-paths.js"; import { upsertAuthProfile } from "../agents/auth-profiles.js"; import type { SecretInput } from "../config/types.secrets.js"; +import { DEEPINFRA_DEFAULT_MODEL_REF } from "../providers/deepinfra-shared.js"; import { KILOCODE_DEFAULT_MODEL_REF } from "../providers/kilocode-shared.js"; import { buildApiKeyCredential, @@ -12,6 +13,7 @@ import { const resolveAuthAgentDir = (agentDir?: string) => agentDir ?? resolveOpenClawAgentDir(); export { KILOCODE_DEFAULT_MODEL_REF }; +export { DEEPINFRA_DEFAULT_MODEL_REF }; export { buildApiKeyCredential, type ApiKeyStorageOptions, @@ -250,6 +252,20 @@ export async function setOpencodeGoApiKey( await setSharedOpencodeApiKey(key, agentDir, options); } +// TODO: use this to reduce the code duplication a bit. +function setApiKey( + providerId: string, + key: SecretInput, + agentDir?: string, + options?: ApiKeyStorageOptions, +) { + upsertAuthProfile({ + profileId: `${providerId}:default`, + credential: buildApiKeyCredential(providerId, key, undefined, options), + agentDir: resolveAuthAgentDir(agentDir), + }); +} + async function setSharedOpencodeApiKey( key: SecretInput, agentDir?: string, @@ -343,3 +359,11 @@ export async function setKilocodeApiKey( agentDir: resolveAuthAgentDir(agentDir), }); } + +export async function setDeepInfraApiKey( + key: SecretInput, + agentDir?: string, + options?: ApiKeyStorageOptions, +) { + setApiKey("deepinfra", key, agentDir, options); +} diff --git a/src/providers/deepinfra-shared.ts b/src/providers/deepinfra-shared.ts new file mode 100644 index 00000000000..2f788deefdc --- /dev/null +++ b/src/providers/deepinfra-shared.ts @@ -0,0 +1,61 @@ +export const DEEPINFRA_BASE_URL = "https://api.deepinfra.com/v1/openai/"; +export const DEEPINFRA_DEFAULT_MODEL_ID = "openai/gpt-oss-120b"; +export const DEEPINFRA_DEFAULT_MODEL_REF = `deepinfra/${DEEPINFRA_DEFAULT_MODEL_ID}`; +export const DEEPINFRA_DEFAULT_MODEL_NAME = "gpt-oss-120b"; +export type DeepInfraModelCatalogEntry = { + id: string; + name: string; + reasoning: boolean; + input: Array<"text" | "image">; + contextWindow: number; + maxTokens: number; +}; + +/** + * Static fallback catalog used by the sync onboarding path and as a + * fallback when dynamic model discovery from the gateway API fails. + * The full model list is fetched dynamically by {@link discoverDeepInfraModels} + * in `src/agents/deepinfra-models.ts`. + */ +export const DEEPINFRA_MODEL_CATALOG: DeepInfraModelCatalogEntry[] = [ + { + id: DEEPINFRA_DEFAULT_MODEL_ID, + name: DEEPINFRA_DEFAULT_MODEL_NAME, + reasoning: true, + input: ["text", "image"], + contextWindow: 131072, + maxTokens: 131072, + }, + { + id: "minimaxai/minimax-m2.5", + name: "MiniMax M2.5", + reasoning: false, + input: ["text"], + contextWindow: 196608, + maxTokens: 196608, + }, + { + id: "zai-org/glm-5", + name: "GLM 5", + reasoning: false, + input: ["text"], + contextWindow: 202752, + maxTokens: 202752, + }, + { + id: "moonshotai/kimi-k2.5", + name: "Kimi K2.5", + reasoning: false, + input: ["text", "image"], + contextWindow: 262144, + maxTokens: 262144, + }, +]; +export const DEEPINFRA_DEFAULT_CONTEXT_WINDOW = 128000; +export const DEEPINFRA_DEFAULT_MAX_TOKENS = 8192; +export const DEEPINFRA_DEFAULT_COST = { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, +} as const; diff --git a/src/secrets/provider-env-vars.ts b/src/secrets/provider-env-vars.ts index 269b37d836a..d0cd661a999 100644 --- a/src/secrets/provider-env-vars.ts +++ b/src/secrets/provider-env-vars.ts @@ -7,6 +7,7 @@ const CORE_PROVIDER_AUTH_ENV_VAR_CANDIDATES = { deepgram: ["DEEPGRAM_API_KEY"], cerebras: ["CEREBRAS_API_KEY"], litellm: ["LITELLM_API_KEY"], + deepinfra: ["DEEPINFRA_API_KEY"], } as const; const CORE_PROVIDER_SETUP_ENV_VAR_OVERRIDES = {