Implement an integration for provider DeepInfra
This commit is contained in:
parent
192f859325
commit
f1ae6265e4
@ -245,4 +245,4 @@
|
||||
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.3.8-beta.1/OpenClaw-2026.3.8-beta.1.zip" length="23407015" type="application/octet-stream" sparkle:edSignature="KCqhSmu4b0tHf55RqcQOHorsc55CgBI5BUmK/NTizxNq04INn/7QvsamHYQou9DbB2IW6B2nawBC4nn4au5yDA=="/>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
</rss>
|
||||
|
||||
@ -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 <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`)
|
||||
|
||||
@ -1135,6 +1135,7 @@
|
||||
"providers/cloudflare-ai-gateway",
|
||||
"providers/claude-max-api-proxy",
|
||||
"providers/deepgram",
|
||||
"providers/deepinfra",
|
||||
"providers/github-copilot",
|
||||
"providers/google",
|
||||
"providers/groq",
|
||||
|
||||
62
docs/providers/deepinfra.md
Normal file
62
docs/providers/deepinfra.md
Normal file
@ -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 <key>
|
||||
```
|
||||
|
||||
Or set the environment variable:
|
||||
|
||||
```bash
|
||||
export DEEPINFRA_API_KEY="<your-deepinfra-api-key>" # pragma: allowlist secret
|
||||
```
|
||||
|
||||
## Config snippet
|
||||
|
||||
```json5
|
||||
{
|
||||
env: { DEEPINFRA_API_KEY: "<your-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/<provider>/<model>` (e.g., `deepinfra/qwen/qwen3-max`).
|
||||
- Default model: `deepinfra/openai/gpt-oss-120b`
|
||||
- Base URL: `https://api.deepinfra.com/v1/openai/`
|
||||
@ -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)
|
||||
|
||||
0
extensions/deepinfra/index.ts
Normal file
0
extensions/deepinfra/index.ts
Normal file
34
extensions/deepinfra/provider-catalog.ts
Normal file
34
extensions/deepinfra/provider-catalog.ts
Normal file
@ -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<ProviderConfig> {
|
||||
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,
|
||||
})),
|
||||
};
|
||||
}
|
||||
277
src/agents/deepinfra-models.test.ts
Normal file
277
src/agents/deepinfra-models.test.ts
Normal file
@ -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<string, unknown> = {}) {
|
||||
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<string, unknown> = {}) {
|
||||
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<typeof vi.fn>,
|
||||
runAssertions: () => Promise<void>,
|
||||
) {
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
154
src/agents/deepinfra-models.ts
Normal file
154
src/agents/deepinfra-models.ts
Normal file
@ -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<ModelDefinitionConfig[]> {
|
||||
// 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<string>();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -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",
|
||||
|
||||
66
src/agents/models-config.providers.deepinfra.test.ts
Normal file
66
src/agents/models-config.providers.deepinfra.test.ts
Normal file
@ -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);
|
||||
});
|
||||
});
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
21
src/agents/pi-embedded-runner/deepinfra.test.ts
Normal file
21
src/agents/pi-embedded-runner/deepinfra.test.ts
Normal file
@ -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);
|
||||
});
|
||||
});
|
||||
@ -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",
|
||||
|
||||
@ -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,
|
||||
|
||||
197
src/commands/onboard-auth.config-core.deepinfra.test.ts
Normal file
197
src/commands/onboard-auth.config-core.deepinfra.test.ts
Normal file
@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
141
src/commands/onboard-auth.ts
Normal file
141
src/commands/onboard-auth.ts
Normal file
@ -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";
|
||||
233
src/commands/onboard-provider-auth-flags.ts
Normal file
233
src/commands/onboard-provider-auth-flags.ts
Normal file
@ -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} <key>`;
|
||||
description: string;
|
||||
};
|
||||
|
||||
// Shared source for provider API-key flags used by CLI registration + non-interactive inference.
|
||||
export const ONBOARD_PROVIDER_AUTH_FLAGS: ReadonlyArray<OnboardProviderAuthFlag> = [
|
||||
{
|
||||
optionKey: "anthropicApiKey",
|
||||
authChoice: "apiKey",
|
||||
cliFlag: "--anthropic-api-key",
|
||||
cliOption: "--anthropic-api-key <key>",
|
||||
description: "Anthropic API key",
|
||||
},
|
||||
{
|
||||
optionKey: "openaiApiKey",
|
||||
authChoice: "openai-api-key",
|
||||
cliFlag: "--openai-api-key",
|
||||
cliOption: "--openai-api-key <key>",
|
||||
description: "OpenAI API key",
|
||||
},
|
||||
{
|
||||
optionKey: "mistralApiKey",
|
||||
authChoice: "mistral-api-key",
|
||||
cliFlag: "--mistral-api-key",
|
||||
cliOption: "--mistral-api-key <key>",
|
||||
description: "Mistral API key",
|
||||
},
|
||||
{
|
||||
optionKey: "openrouterApiKey",
|
||||
authChoice: "openrouter-api-key",
|
||||
cliFlag: "--openrouter-api-key",
|
||||
cliOption: "--openrouter-api-key <key>",
|
||||
description: "OpenRouter API key",
|
||||
},
|
||||
{
|
||||
optionKey: "kilocodeApiKey",
|
||||
authChoice: "kilocode-api-key",
|
||||
cliFlag: "--kilocode-api-key",
|
||||
cliOption: "--kilocode-api-key <key>",
|
||||
description: "Kilo Gateway API key",
|
||||
},
|
||||
{
|
||||
optionKey: "deepinfraApiKey",
|
||||
authChoice: "deepinfra-api-key",
|
||||
cliFlag: "--deepinfra-api-key",
|
||||
cliOption: "--deepinfra-api-key <key>",
|
||||
description: "DeepInfra API key",
|
||||
},
|
||||
{
|
||||
optionKey: "aiGatewayApiKey",
|
||||
authChoice: "ai-gateway-api-key",
|
||||
cliFlag: "--ai-gateway-api-key",
|
||||
cliOption: "--ai-gateway-api-key <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 <key>",
|
||||
description: "Cloudflare AI Gateway API key",
|
||||
},
|
||||
{
|
||||
optionKey: "moonshotApiKey",
|
||||
authChoice: "moonshot-api-key",
|
||||
cliFlag: "--moonshot-api-key",
|
||||
cliOption: "--moonshot-api-key <key>",
|
||||
description: "Moonshot API key",
|
||||
},
|
||||
{
|
||||
optionKey: "kimiCodeApiKey",
|
||||
authChoice: "kimi-code-api-key",
|
||||
cliFlag: "--kimi-code-api-key",
|
||||
cliOption: "--kimi-code-api-key <key>",
|
||||
description: "Kimi Coding API key",
|
||||
},
|
||||
{
|
||||
optionKey: "geminiApiKey",
|
||||
authChoice: "gemini-api-key",
|
||||
cliFlag: "--gemini-api-key",
|
||||
cliOption: "--gemini-api-key <key>",
|
||||
description: "Gemini API key",
|
||||
},
|
||||
{
|
||||
optionKey: "zaiApiKey",
|
||||
authChoice: "zai-api-key",
|
||||
cliFlag: "--zai-api-key",
|
||||
cliOption: "--zai-api-key <key>",
|
||||
description: "Z.AI API key",
|
||||
},
|
||||
{
|
||||
optionKey: "xiaomiApiKey",
|
||||
authChoice: "xiaomi-api-key",
|
||||
cliFlag: "--xiaomi-api-key",
|
||||
cliOption: "--xiaomi-api-key <key>",
|
||||
description: "Xiaomi API key",
|
||||
},
|
||||
{
|
||||
optionKey: "minimaxApiKey",
|
||||
authChoice: "minimax-api",
|
||||
cliFlag: "--minimax-api-key",
|
||||
cliOption: "--minimax-api-key <key>",
|
||||
description: "MiniMax API key",
|
||||
},
|
||||
{
|
||||
optionKey: "syntheticApiKey",
|
||||
authChoice: "synthetic-api-key",
|
||||
cliFlag: "--synthetic-api-key",
|
||||
cliOption: "--synthetic-api-key <key>",
|
||||
description: "Synthetic API key",
|
||||
},
|
||||
{
|
||||
optionKey: "veniceApiKey",
|
||||
authChoice: "venice-api-key",
|
||||
cliFlag: "--venice-api-key",
|
||||
cliOption: "--venice-api-key <key>",
|
||||
description: "Venice API key",
|
||||
},
|
||||
{
|
||||
optionKey: "togetherApiKey",
|
||||
authChoice: "together-api-key",
|
||||
cliFlag: "--together-api-key",
|
||||
cliOption: "--together-api-key <key>",
|
||||
description: "Together AI API key",
|
||||
},
|
||||
{
|
||||
optionKey: "huggingfaceApiKey",
|
||||
authChoice: "huggingface-api-key",
|
||||
cliFlag: "--huggingface-api-key",
|
||||
cliOption: "--huggingface-api-key <key>",
|
||||
description: "Hugging Face API key (HF token)",
|
||||
},
|
||||
{
|
||||
optionKey: "opencodeZenApiKey",
|
||||
authChoice: "opencode-zen",
|
||||
cliFlag: "--opencode-zen-api-key",
|
||||
cliOption: "--opencode-zen-api-key <key>",
|
||||
description: "OpenCode API key (Zen catalog)",
|
||||
},
|
||||
{
|
||||
optionKey: "opencodeGoApiKey",
|
||||
authChoice: "opencode-go",
|
||||
cliFlag: "--opencode-go-api-key",
|
||||
cliOption: "--opencode-go-api-key <key>",
|
||||
description: "OpenCode API key (Go catalog)",
|
||||
},
|
||||
{
|
||||
optionKey: "xaiApiKey",
|
||||
authChoice: "xai-api-key",
|
||||
cliFlag: "--xai-api-key",
|
||||
cliOption: "--xai-api-key <key>",
|
||||
description: "xAI API key",
|
||||
},
|
||||
{
|
||||
optionKey: "litellmApiKey",
|
||||
authChoice: "litellm-api-key",
|
||||
cliFlag: "--litellm-api-key",
|
||||
cliOption: "--litellm-api-key <key>",
|
||||
description: "LiteLLM API key",
|
||||
},
|
||||
{
|
||||
optionKey: "qianfanApiKey",
|
||||
authChoice: "qianfan-api-key",
|
||||
cliFlag: "--qianfan-api-key",
|
||||
cliOption: "--qianfan-api-key <key>",
|
||||
description: "QIANFAN API key",
|
||||
},
|
||||
{
|
||||
optionKey: "modelstudioApiKeyCn",
|
||||
authChoice: "modelstudio-api-key-cn",
|
||||
cliFlag: "--modelstudio-api-key-cn",
|
||||
cliOption: "--modelstudio-api-key-cn <key>",
|
||||
description: "Alibaba Cloud Model Studio Coding Plan API key (China)",
|
||||
},
|
||||
{
|
||||
optionKey: "modelstudioApiKey",
|
||||
authChoice: "modelstudio-api-key",
|
||||
cliFlag: "--modelstudio-api-key",
|
||||
cliOption: "--modelstudio-api-key <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 <key>",
|
||||
description: "Volcano Engine API key",
|
||||
},
|
||||
{
|
||||
optionKey: "byteplusApiKey",
|
||||
authChoice: "byteplus-api-key",
|
||||
cliFlag: "--byteplus-api-key",
|
||||
cliOption: "--byteplus-api-key <key>",
|
||||
description: "BytePlus API key",
|
||||
},
|
||||
];
|
||||
@ -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;
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
61
src/providers/deepinfra-shared.ts
Normal file
61
src/providers/deepinfra-shared.ts
Normal file
@ -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;
|
||||
@ -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 = {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user