Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled

Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11),
free to diverge. Tree copied sans upstream .git; upstream remote added for
future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19.
Preserves docs/ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
2026-07-05 09:36:54 +00:00
parent 3216769225
commit bedb527145
21108 changed files with 6010766 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
# OpenClaw DeepInfra Provider
Official OpenClaw provider plugin for DeepInfra.
Install from OpenClaw:
```bash
openclaw plugins install @openclaw/deepinfra-provider
openclaw gateway restart
```
See <https://docs.openclaw.ai/providers/deepinfra> for setup and configuration.

View File

@@ -0,0 +1,9 @@
// Deepinfra API module exposes the plugin public contract.
export { buildDeepInfraProvider, buildStaticDeepInfraProvider } from "./provider-catalog.js";
export { applyDeepInfraConfig } from "./onboard.js";
export { DEEPINFRA_DEFAULT_MODEL_REF } from "./provider-models.js";
export { buildDeepInfraImageGenerationProvider } from "./image-generation-provider.js";
export { deepinfraMediaUnderstandingProvider } from "./media-understanding-provider.js";
export { deepinfraMemoryEmbeddingProviderAdapter } from "./memory-embedding-adapter.js";
export { buildDeepInfraSpeechProvider } from "./speech-provider.js";
export { buildDeepInfraVideoGenerationProvider } from "./video-generation-provider.js";

View File

@@ -0,0 +1,121 @@
// Deepinfra tests cover cache wrapper plugin behavior.
import { describe, expect, it } from "vitest";
import { createDeepInfraAnthropicCacheWrapper } from "./cache-wrapper.js";
type StreamFn = Parameters<typeof createDeepInfraAnthropicCacheWrapper>[0];
function capturePayload(params: { modelId: string; initialPayload: Record<string, unknown> }): {
captured: Record<string, unknown>;
baseCalls: number;
} {
let captured: Record<string, unknown> = {};
let baseCalls = 0;
const baseStreamFn: StreamFn = (_model, _context, options) => {
baseCalls += 1;
const payload = structuredClone(params.initialPayload);
options?.onPayload?.(payload, _model);
captured = payload;
return {} as ReturnType<StreamFn>;
};
const wrapped = createDeepInfraAnthropicCacheWrapper(baseStreamFn);
void wrapped(
{
api: "openai-completions",
provider: "deepinfra",
id: params.modelId,
reasoning: false,
} as Parameters<StreamFn>[0],
{ messages: [] } as Parameters<StreamFn>[1],
{} as never,
);
return { captured, baseCalls };
}
describe("createDeepInfraAnthropicCacheWrapper", () => {
it("injects ephemeral cache_control markers on the system message for anthropic/* models", () => {
const { captured, baseCalls } = capturePayload({
modelId: "anthropic/claude-sonnet-4-6",
initialPayload: {
messages: [
{ role: "system", content: "You are a helpful assistant." },
{ role: "user", content: "Hi" },
],
},
});
expect(baseCalls).toBe(1);
expect(captured.messages).toEqual([
{
role: "system",
content: [
{
type: "text",
text: "You are a helpful assistant.",
cache_control: { type: "ephemeral" },
},
],
},
{ role: "user", content: "Hi" },
]);
});
it("tags the last block of an array-shaped system message", () => {
const { captured } = capturePayload({
modelId: "anthropic/claude-haiku-4-5",
initialPayload: {
messages: [
{
role: "system",
content: [
{ type: "text", text: "Block one" },
{ type: "text", text: "Block two" },
],
},
{ role: "user", content: "Hi" },
],
},
});
const messages = captured.messages as Array<{ role: string; content: unknown }>;
expect(messages[0]?.content).toEqual([
{ type: "text", text: "Block one" },
{
type: "text",
text: "Block two",
cache_control: { type: "ephemeral" },
},
]);
});
it("matches the anthropic/ prefix case-insensitively", () => {
const { captured } = capturePayload({
modelId: "Anthropic/Claude-Sonnet-4-6",
initialPayload: {
messages: [{ role: "system", content: "sys" }],
},
});
const messages = captured.messages as Array<{ content: unknown }>;
expect(messages[0]?.content).toEqual([
{ type: "text", text: "sys", cache_control: { type: "ephemeral" } },
]);
});
it("does not mutate payloads for non-anthropic model ids", () => {
const initialPayload = {
messages: [
{ role: "system", content: "sys" },
{ role: "user", content: "Hi" },
],
};
const { captured, baseCalls } = capturePayload({
modelId: "deepseek-ai/DeepSeek-V4-Flash",
initialPayload,
});
expect(baseCalls).toBe(1);
expect(captured).toEqual(initialPayload);
});
});

View File

@@ -0,0 +1,26 @@
// Deepinfra plugin module implements cache wrapper behavior.
import {
applyAnthropicEphemeralCacheControlMarkers,
streamWithPayloadPatch,
} from "openclaw/plugin-sdk/provider-stream";
// StreamFn isn't re-exported via the plugin SDK; derive it from a helper that
// accepts it so we stay on the SDK boundary.
type StreamFn = Parameters<typeof streamWithPayloadPatch>[0];
// Inject Anthropic ephemeral cache_control markers for anthropic/* models on
// DeepInfra. The OpenRouter equivalent short-circuits on a provider/endpoint
// check, so DeepInfra advertises isCacheTtlEligible but the payload patch
// never fires. Gating on the model id instead fixes that.
export function createDeepInfraAnthropicCacheWrapper(baseStreamFn: StreamFn): StreamFn {
return ((model, context, options) => {
const modelIdRaw = (model as { id?: unknown }).id;
const modelId = typeof modelIdRaw === "string" ? modelIdRaw.toLowerCase() : "";
if (!modelId.startsWith("anthropic/")) {
return baseStreamFn(model, context, options);
}
return streamWithPayloadPatch(baseStreamFn, model, context, options, (payload) => {
applyAnthropicEphemeralCacheControlMarkers(payload);
});
}) as StreamFn;
}

View File

@@ -0,0 +1,35 @@
// Deepinfra provider module implements model/runtime integration.
import {
createRemoteEmbeddingProvider,
resolveRemoteEmbeddingClient,
type MemoryEmbeddingProviderCreateOptions,
type MemoryEmbeddingProviderCreateResult,
} from "openclaw/plugin-sdk/memory-core-host-engine-embeddings";
import {
DEEPINFRA_BASE_URL,
DEEPINFRA_EMBED_FALLBACK_MODELS,
normalizeDeepInfraModelRef,
} from "./media-models.js";
export const DEFAULT_DEEPINFRA_EMBEDDING_MODEL = DEEPINFRA_EMBED_FALLBACK_MODELS[0];
export async function createDeepInfraEmbeddingProvider(
options: MemoryEmbeddingProviderCreateOptions & { defaultModel?: string },
): Promise<MemoryEmbeddingProviderCreateResult & { client: { model: string } }> {
const defaultModel = options.defaultModel ?? DEFAULT_DEEPINFRA_EMBEDDING_MODEL;
const client = await resolveRemoteEmbeddingClient({
provider: "deepinfra",
options: {
...options,
model: normalizeDeepInfraModelRef(options.model, defaultModel),
},
defaultBaseUrl: DEEPINFRA_BASE_URL,
normalizeModel: (model) => normalizeDeepInfraModelRef(model, defaultModel),
});
const provider = createRemoteEmbeddingProvider({
id: "deepinfra",
client,
errorPrefix: "DeepInfra embeddings API error",
});
return { provider, client };
}

View File

@@ -0,0 +1,234 @@
// Deepinfra tests cover image generation provider plugin behavior.
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
import { buildDeepInfraImageGenerationProvider } from "./image-generation-provider.js";
const {
assertOkOrThrowHttpErrorMock,
postJsonRequestMock,
postMultipartRequestMock,
resolveApiKeyForProviderMock,
resolveProviderHttpRequestConfigMock,
createProviderOperationDeadlineMock,
resolveProviderOperationTimeoutMsMock,
} = vi.hoisted(() => ({
assertOkOrThrowHttpErrorMock: vi.fn(async () => {}),
postJsonRequestMock: vi.fn(),
postMultipartRequestMock: vi.fn(),
resolveApiKeyForProviderMock: vi.fn(async () => ({ apiKey: "deepinfra-key" })),
createProviderOperationDeadlineMock: vi.fn((params: Record<string, unknown>) => params),
resolveProviderOperationTimeoutMsMock: vi.fn(
(params: Record<string, unknown>) => params.defaultTimeoutMs,
),
resolveProviderHttpRequestConfigMock: vi.fn((params: Record<string, unknown>) => ({
baseUrl: params.baseUrl ?? params.defaultBaseUrl ?? "https://api.deepinfra.com/v1/openai",
allowPrivateNetwork: false,
headers: new Headers(params.defaultHeaders as HeadersInit | undefined),
dispatcherPolicy: undefined,
})),
}));
vi.mock("openclaw/plugin-sdk/provider-auth-runtime", () => ({
resolveApiKeyForProvider: resolveApiKeyForProviderMock,
}));
vi.mock("openclaw/plugin-sdk/provider-http", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/provider-http")>(
"openclaw/plugin-sdk/provider-http",
);
return {
assertOkOrThrowHttpError: assertOkOrThrowHttpErrorMock,
createProviderOperationDeadline: createProviderOperationDeadlineMock,
postJsonRequest: postJsonRequestMock,
postMultipartRequest: postMultipartRequestMock,
readProviderJsonResponse: actual.readProviderJsonResponse,
resolveProviderHttpRequestConfig: resolveProviderHttpRequestConfigMock,
resolveProviderOperationTimeoutMs: resolveProviderOperationTimeoutMsMock,
sanitizeConfiguredModelProviderRequest: vi.fn((request) => request),
};
});
afterAll(() => {
vi.doUnmock("openclaw/plugin-sdk/provider-auth-runtime");
vi.doUnmock("openclaw/plugin-sdk/provider-http");
vi.resetModules();
});
function requireFirstMockArg(mock: ReturnType<typeof vi.fn>, label: string): unknown {
const [call] = mock.mock.calls;
if (!call) {
throw new Error(`expected ${label}`);
}
return call[0];
}
function requireFirstMockObjectArg(mock: ReturnType<typeof vi.fn>, label: string): object {
const value = requireFirstMockArg(mock, label);
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new Error(`expected ${label}`);
}
return value;
}
function jsonResponse(payload: unknown): Response {
return new Response(JSON.stringify(payload), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
describe("deepinfra image generation provider", () => {
afterEach(() => {
assertOkOrThrowHttpErrorMock.mockClear();
postJsonRequestMock.mockReset();
postMultipartRequestMock.mockReset();
resolveApiKeyForProviderMock.mockClear();
resolveProviderHttpRequestConfigMock.mockClear();
});
it("declares generation and single-reference edit support", () => {
const provider = buildDeepInfraImageGenerationProvider();
expect(provider.id).toBe("deepinfra");
expect(provider.defaultModel).toBe("black-forest-labs/FLUX-1-schnell");
expect(provider.capabilities.generate.maxCount).toBe(4);
expect(provider.capabilities.edit.enabled).toBe(true);
expect(provider.capabilities.edit.maxInputImages).toBe(1);
});
it("sends OpenAI-compatible image generation requests and sniffs JPEG output", async () => {
const release = vi.fn(async () => {});
const jpegBytes = Buffer.from([0xff, 0xd8, 0xff, 0x00]);
postJsonRequestMock.mockResolvedValue({
response: jsonResponse({
data: [{ b64_json: jpegBytes.toString("base64"), revised_prompt: "red square" }],
}),
release,
});
const provider = buildDeepInfraImageGenerationProvider();
const result = await provider.generateImage({
provider: "deepinfra",
model: "deepinfra/black-forest-labs/FLUX-1-schnell",
prompt: "red square",
count: 2,
size: "512x512",
timeoutMs: 12_345,
cfg: {
models: {
providers: {
deepinfra: {
baseUrl: "https://api.deepinfra.com/v1/openai/",
},
},
},
} as never,
});
expect(resolveProviderHttpRequestConfigMock.mock.calls).toEqual([
[
{
baseUrl: "https://api.deepinfra.com/v1/openai",
defaultBaseUrl: "https://api.deepinfra.com/v1/openai",
allowPrivateNetwork: false,
request: undefined,
defaultHeaders: {
Authorization: "Bearer deepinfra-key",
},
provider: "deepinfra",
capability: "image",
transport: "http",
},
],
]);
expect(postJsonRequestMock).toHaveBeenCalledOnce();
const jsonRequest = requireFirstMockArg(postJsonRequestMock, "DeepInfra JSON image request");
const jsonRequestHeaders = Reflect.get(jsonRequest ?? {}, "headers");
expect(jsonRequestHeaders).toBeInstanceOf(Headers);
expect(Object.fromEntries((jsonRequestHeaders as Headers).entries())).toEqual({
authorization: "Bearer deepinfra-key",
"content-type": "application/json",
});
expect(jsonRequest).toEqual({
url: "https://api.deepinfra.com/v1/openai/images/generations",
headers: jsonRequestHeaders,
timeoutMs: 12_345,
body: {
model: "black-forest-labs/FLUX-1-schnell",
prompt: "red square",
n: 2,
size: "512x512",
response_format: "b64_json",
},
fetchFn: fetch,
allowPrivateNetwork: false,
dispatcherPolicy: undefined,
});
expect(result.images).toHaveLength(1);
const [firstImage] = result.images;
if (!firstImage) {
throw new Error("Expected generated DeepInfra image");
}
expect(firstImage).toEqual({
buffer: jpegBytes,
mimeType: "image/jpeg",
fileName: "image-1.jpg",
revisedPrompt: "red square",
});
expect(release).toHaveBeenCalledOnce();
});
it("sends image edits as multipart OpenAI-compatible requests", async () => {
postMultipartRequestMock.mockResolvedValue({
response: jsonResponse({
data: [
{
b64_json: Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]).toString(
"base64",
),
},
],
}),
release: vi.fn(async () => {}),
});
const provider = buildDeepInfraImageGenerationProvider();
const result = await provider.generateImage({
provider: "deepinfra",
model: "black-forest-labs/FLUX-1-schnell",
prompt: "make it neon",
inputImages: [{ buffer: Buffer.from("source"), mimeType: "image/png" }],
cfg: {} as never,
});
expect(postMultipartRequestMock).toHaveBeenCalledOnce();
const multipartRequest = requireFirstMockObjectArg(
postMultipartRequestMock,
"DeepInfra multipart image request",
);
const multipartHeaders = Reflect.get(multipartRequest, "headers");
expect(multipartHeaders).toBeInstanceOf(Headers);
expect(Object.fromEntries((multipartHeaders as Headers).entries())).toEqual({
authorization: "Bearer deepinfra-key",
});
const form = Reflect.get(multipartRequest, "body") as FormData;
expect(multipartRequest).toEqual({
url: "https://api.deepinfra.com/v1/openai/images/edits",
headers: multipartHeaders,
body: form,
timeoutMs: undefined,
fetchFn: fetch,
allowPrivateNetwork: false,
dispatcherPolicy: undefined,
});
expect(form.get("model")).toBe("black-forest-labs/FLUX-1-schnell");
expect(form.get("prompt")).toBe("make it neon");
expect(form.get("response_format")).toBe("b64_json");
expect(form.get("image")).toBeInstanceOf(File);
expect(result.images).toHaveLength(1);
const [image] = result.images;
if (!image) {
throw new Error("Expected edited DeepInfra image");
}
expect(image.mimeType).toBe("image/png");
});
});

View File

@@ -0,0 +1,99 @@
// Deepinfra provider module implements model/runtime integration.
import {
createOpenAiCompatibleImageGenerationProvider,
imageSourceUploadFileName,
type ImageGenerationProvider,
} from "openclaw/plugin-sdk/image-generation";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
DEEPINFRA_BASE_URL,
DEEPINFRA_IMAGE_FALLBACK_MODELS,
DEFAULT_DEEPINFRA_IMAGE_SIZE,
normalizeDeepInfraBaseUrl,
normalizeDeepInfraModelRef,
} from "./media-models.js";
import type { DeepInfraSurfaceModel } from "./provider-models.js";
const DEEPINFRA_IMAGE_SIZES = ["512x512", "1024x1024", "1024x1792", "1792x1024"] as const;
const MAX_DEEPINFRA_INPUT_IMAGES = 1;
// First entry of imageGenModels is the default; rest fill the allowlist.
// No catalog supplied -> DEEPINFRA_IMAGE_FALLBACK_MODELS.
export function buildDeepInfraImageGenerationProvider(options?: {
imageGenModels?: readonly DeepInfraSurfaceModel[];
}): ImageGenerationProvider {
const ids =
options?.imageGenModels && options.imageGenModels.length > 0
? options.imageGenModels.map((model) => model.id)
: [...DEEPINFRA_IMAGE_FALLBACK_MODELS];
const defaultModel = ids[0] ?? DEEPINFRA_IMAGE_FALLBACK_MODELS[0];
return createOpenAiCompatibleImageGenerationProvider({
id: "deepinfra",
label: "DeepInfra",
defaultModel,
models: ids,
capabilities: {
generate: {
maxCount: 4,
supportsSize: true,
supportsAspectRatio: false,
supportsResolution: false,
},
edit: {
enabled: true,
maxCount: 1,
maxInputImages: MAX_DEEPINFRA_INPUT_IMAGES,
supportsSize: true,
supportsAspectRatio: false,
supportsResolution: false,
},
geometry: {
sizes: [...DEEPINFRA_IMAGE_SIZES],
},
},
defaultBaseUrl: DEEPINFRA_BASE_URL,
normalizeModel: normalizeDeepInfraModelRef,
resolveBaseUrl: ({ providerConfig }) =>
normalizeDeepInfraBaseUrl(providerConfig?.baseUrl, DEEPINFRA_BASE_URL),
resolveAllowPrivateNetwork: () => false,
useConfiguredRequest: true,
resolveCount: ({ req, mode }) => (mode === "edit" ? 1 : (req.count ?? 1)),
buildGenerateRequest: ({ req, model, count }) => ({
kind: "json",
body: {
model,
prompt: req.prompt,
n: count,
size: normalizeOptionalString(req.size) ?? DEFAULT_DEEPINFRA_IMAGE_SIZE,
response_format: "b64_json",
},
}),
buildEditRequest: ({ req, inputImages, model, count }) => {
const image = inputImages[0];
if (!image) {
throw new Error("DeepInfra image edit missing reference image.");
}
const form = new FormData();
form.set("model", model);
form.set("prompt", req.prompt);
form.set("n", String(count));
form.set("size", normalizeOptionalString(req.size) ?? DEFAULT_DEEPINFRA_IMAGE_SIZE);
form.set("response_format", "b64_json");
const mimeType = normalizeOptionalString(image.mimeType) ?? "image/png";
form.append(
"image",
new Blob([new Uint8Array(image.buffer)], { type: mimeType }),
imageSourceUploadFileName({ image, index: 0 }),
);
return { kind: "multipart", form };
},
response: { defaultMimeType: "image/jpeg", sniffMimeType: true },
tooManyInputImagesError: "DeepInfra image editing supports one reference image.",
missingApiKeyError: "DeepInfra API key missing",
emptyResponseError: "DeepInfra image response did not include generated image data",
failureLabels: {
generate: "DeepInfra image generation failed",
edit: "DeepInfra image edit failed",
},
});
}

View File

@@ -0,0 +1,309 @@
// Deepinfra tests cover index plugin behavior.
import {
createCapturedPluginRegistration,
registerSingleProviderPlugin,
} from "openclaw/plugin-sdk/plugin-test-runtime";
import type { ProviderCatalogContext } from "openclaw/plugin-sdk/provider-catalog-shared";
import { describe, expect, it, vi } from "vitest";
import deepinfraPlugin from "./index.js";
import {
DEEPINFRA_MODEL_CATALOG,
DEEPINFRA_MODELS_URL,
resetDeepInfraModelCacheForTest,
} from "./provider-models.js";
function buildSyntheticDeepInfraEntries(count: number) {
return Array.from({ length: count }, (_unused, index) => ({
provider: "deepinfra",
id: `synthetic/model-${index}`,
name: `synthetic/model-${index}`,
}));
}
function buildDeepInfraCatalogContext(): ProviderCatalogContext {
return {
config: {},
env: {},
agentDir: "/tmp/openclaw-agent",
resolveProviderApiKey: () => ({ apiKey: "profile-key" }),
resolveProviderAuth: () => ({
apiKey: "profile-key",
mode: "api_key",
source: "profile",
}),
};
}
function makeAgentModelEntry(id = "profile/live-model") {
return {
id,
object: "model",
owned_by: "deepinfra",
metadata: {
description: id,
context_length: 32768,
max_tokens: 4096,
pricing: { input_tokens: 1, output_tokens: 2 },
tags: ["chat"],
},
};
}
function jsonResponse(payload: unknown, init: ResponseInit = {}): Response {
return new Response(JSON.stringify(payload), {
status: 200,
headers: { "Content-Type": "application/json" },
...init,
});
}
async function withLiveDiscoveryTestEnv(
mockFetch: ReturnType<typeof vi.fn>,
runAssertions: () => Promise<void>,
) {
const env = { ...process.env };
delete process.env.NODE_ENV;
delete process.env.VITEST;
delete process.env.DEEPINFRA_API_KEY;
vi.stubGlobal("fetch", mockFetch);
try {
await runAssertions();
} finally {
for (const key of ["NODE_ENV", "VITEST", "DEEPINFRA_API_KEY"]) {
if (env[key] === undefined) {
delete process.env[key];
} else {
process.env[key] = env[key];
}
}
vi.unstubAllGlobals();
}
}
describe("deepinfra augmentModelCatalog", () => {
it("returns the discovered (static under VITEST) catalog when nothing is configured", async () => {
resetDeepInfraModelCacheForTest();
const provider = await registerSingleProviderPlugin(deepinfraPlugin);
const entries = (await provider.augmentModelCatalog?.({ entries: [] } as never)) ?? [];
expect(entries.map((entry) => entry.id)).toEqual(
DEEPINFRA_MODEL_CATALOG.map((model) => model.id),
);
for (const entry of entries) {
expect(entry.provider).toBe("deepinfra");
}
});
it("preserves configured entries and appends discovered entries that are not already configured", async () => {
resetDeepInfraModelCacheForTest();
const provider = await registerSingleProviderPlugin(deepinfraPlugin);
const entries =
(await provider.augmentModelCatalog?.({
entries: [],
config: {
models: {
providers: {
deepinfra: {
models: [
{
id: "zai-org/GLM-5.1",
name: "GLM-5.1 custom",
input: ["text"],
reasoning: true,
contextWindow: 202752,
},
],
},
},
},
},
} as never)) ?? [];
const glmEntry = entries.find((entry) => entry.id === "zai-org/GLM-5.1");
expect(glmEntry?.name).toBe("GLM-5.1 custom");
expect(entries.filter((entry) => entry.id === "zai-org/GLM-5.1")).toHaveLength(1);
expect(entries.length).toBe(DEEPINFRA_MODEL_CATALOG.length);
});
it("uses config-backed API keys to enable live model catalog augmentation", async () => {
resetDeepInfraModelCacheForTest();
const mockFetch = vi
.fn()
.mockResolvedValue(jsonResponse({ data: [makeAgentModelEntry("config/live-model")] }));
const provider = await registerSingleProviderPlugin(deepinfraPlugin);
await withLiveDiscoveryTestEnv(mockFetch, async () => {
const entries =
(await provider.augmentModelCatalog?.({
entries: [],
env: {},
config: {
models: {
providers: {
deepinfra: {
apiKey: { source: "env", provider: "default", id: "CUSTOM_DEEPINFRA_KEY" },
},
},
},
},
} as never)) ?? [];
expect(mockFetch).toHaveBeenCalledOnce();
expect(entries.map((entry) => entry.id)).toContain("config/live-model");
});
});
it("still runs live discovery when ctx.entries includes custom DeepInfra rows", async () => {
resetDeepInfraModelCacheForTest();
const mockFetch = vi
.fn()
.mockResolvedValue(jsonResponse({ data: [makeAgentModelEntry("custom/live-model")] }));
const provider = await registerSingleProviderPlugin(deepinfraPlugin);
const seededDeepInfraCount = DEEPINFRA_MODEL_CATALOG.length + 5;
await withLiveDiscoveryTestEnv(mockFetch, async () => {
const entries =
(await provider.augmentModelCatalog?.({
entries: [
...buildSyntheticDeepInfraEntries(seededDeepInfraCount),
{ provider: "openai", id: "noise", name: "noise" },
],
config: {
models: {
providers: {
deepinfra: {
apiKey: "sk-test",
models: [
{
id: "zai-org/GLM-5.1",
name: "configured override",
input: ["text"],
reasoning: true,
contextWindow: 202752,
},
],
},
},
},
},
} as never)) ?? [];
expect(mockFetch).toHaveBeenCalledOnce();
expect(entries[0]).toEqual({
provider: "deepinfra",
id: "zai-org/GLM-5.1",
name: "configured override",
input: ["text"],
reasoning: true,
contextWindow: 202752,
});
expect(entries.map((entry) => entry.id)).toContain("custom/live-model");
});
});
it("still fetches when ctx.entries has exactly the static catalog length (static-fallback case)", async () => {
resetDeepInfraModelCacheForTest();
const provider = await registerSingleProviderPlugin(deepinfraPlugin);
const entries =
(await provider.augmentModelCatalog?.({
entries: buildSyntheticDeepInfraEntries(DEEPINFRA_MODEL_CATALOG.length),
} as never)) ?? [];
expect(entries.map((entry) => entry.id)).toEqual(
DEEPINFRA_MODEL_CATALOG.map((model) => model.id),
);
});
});
describe("deepinfra capability registration", () => {
it("registers all DeepInfra-backed OpenClaw provider surfaces", () => {
const captured = createCapturedPluginRegistration();
deepinfraPlugin.register(captured.api);
expect(captured.providers.map((provider) => provider.id)).toEqual(["deepinfra"]);
expect(captured.imageGenerationProviders.map((provider) => provider.id)).toEqual(["deepinfra"]);
expect(captured.mediaUnderstandingProviders.map((provider) => provider.id)).toEqual([
"deepinfra",
]);
expect(captured.memoryEmbeddingProviders.map((provider) => provider.id)).toEqual(["deepinfra"]);
expect(captured.speechProviders.map((provider) => provider.id)).toEqual(["deepinfra"]);
expect(captured.videoGenerationProviders.map((provider) => provider.id)).toEqual(["deepinfra"]);
});
it("uses profile-resolved API keys for live text catalog discovery", async () => {
resetDeepInfraModelCacheForTest();
const mockFetch = vi.fn().mockResolvedValue(jsonResponse({ data: [makeAgentModelEntry()] }));
const captured = createCapturedPluginRegistration();
deepinfraPlugin.register(captured.api);
const provider = captured.providers[0];
if (!provider?.catalog) {
throw new Error("expected DeepInfra provider registration");
}
const catalog = provider.catalog;
await withLiveDiscoveryTestEnv(mockFetch, async () => {
const result = await catalog.run(buildDeepInfraCatalogContext());
if (!result || !("provider" in result)) {
throw new Error("expected single-provider DeepInfra catalog result");
}
expect(mockFetch).toHaveBeenCalledOnce();
expect(mockFetch.mock.calls[0]?.[0]).toBe(DEEPINFRA_MODELS_URL);
expect(result?.provider.apiKey).toBe("profile-key");
expect(result.provider.models.map((model) => model.id)).toEqual([
"profile/live-model",
...DEEPINFRA_MODEL_CATALOG.map((model) => model.id),
]);
});
});
});
describe("deepinfra isCacheTtlEligible", () => {
it("returns true for anthropic/* proxied models", async () => {
const provider = await registerSingleProviderPlugin(deepinfraPlugin);
expect(
provider.isCacheTtlEligible?.({
provider: "deepinfra",
modelId: "anthropic/claude-4-sonnet",
}),
).toBe(true);
});
// Locked to case-insensitive to stay consistent with the shared proxy cache
// wrapper, which lowercases the modelId before the "anthropic/" prefix check.
it("returns true regardless of modelId case", async () => {
const provider = await registerSingleProviderPlugin(deepinfraPlugin);
expect(
provider.isCacheTtlEligible?.({
provider: "deepinfra",
modelId: "Anthropic/Claude-4-Sonnet",
}),
).toBe(true);
expect(
provider.isCacheTtlEligible?.({
provider: "deepinfra",
modelId: "ANTHROPIC/claude-4-sonnet",
}),
).toBe(true);
});
it("returns false for non-anthropic models", async () => {
const provider = await registerSingleProviderPlugin(deepinfraPlugin);
expect(
provider.isCacheTtlEligible?.({
provider: "deepinfra",
modelId: "meta-llama/Llama-4-Scout-17B-16E-Instruct",
}),
).toBe(false);
expect(
provider.isCacheTtlEligible?.({
provider: "deepinfra",
modelId: "zai-org/GLM-5.1",
}),
).toBe(false);
});
});

View File

@@ -0,0 +1,156 @@
// Deepinfra plugin entrypoint registers its OpenClaw integration.
import {
type ProviderCatalogContext,
type ConfiguredProviderCatalogEntry,
readConfiguredProviderCatalogEntries,
} from "openclaw/plugin-sdk/provider-catalog-shared";
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
import { PASSTHROUGH_GEMINI_REPLAY_HOOKS } from "openclaw/plugin-sdk/provider-model-shared";
import {
createOpenRouterWrapper,
isProxyReasoningUnsupported,
} from "openclaw/plugin-sdk/provider-stream";
import { createDeepInfraAnthropicCacheWrapper } from "./cache-wrapper.js";
import { buildDeepInfraImageGenerationProvider } from "./image-generation-provider.js";
import { buildDeepInfraMediaUnderstandingProvider } from "./media-understanding-provider.js";
import { buildDeepInfraMemoryEmbeddingAdapter } from "./memory-embedding-adapter.js";
import { applyDeepInfraConfig } from "./onboard.js";
import { buildDeepInfraApiKeyCatalog, buildStaticDeepInfraProvider } from "./provider-catalog.js";
import {
DEEPINFRA_DEFAULT_MODEL_REF,
discoverDeepInfraModels,
getDeepInfraSurfaceFallbackCatalog,
hasDeepInfraApiKey,
} from "./provider-models.js";
import { buildDeepInfraSpeechProvider } from "./speech-provider.js";
import {
listDeepInfraImageGenCatalog,
listDeepInfraVideoGenCatalog,
} from "./surface-model-catalogs.js";
import { buildDeepInfraVideoGenerationProvider } from "./video-generation-provider.js";
const PROVIDER_ID = "deepinfra";
export default defineSingleProviderPluginEntry({
id: PROVIDER_ID,
name: "DeepInfra Provider",
description: "Bundled DeepInfra provider plugin",
provider: {
label: "DeepInfra",
docsPath: "/providers/deepinfra",
auth: [
{
methodId: "api-key",
label: "DeepInfra API key",
hint: "Unified API for open source models",
optionKey: "deepinfraApiKey",
flagName: "--deepinfra-api-key",
envVar: "DEEPINFRA_API_KEY",
promptMessage: "Enter DeepInfra API key",
noteTitle: "DeepInfra",
noteMessage: [
"DeepInfra provides an OpenAI-compatible API for open source and frontier models.",
"Get your API key at: https://deepinfra.com/dash/api_keys",
].join("\n"),
defaultModel: DEEPINFRA_DEFAULT_MODEL_REF,
applyConfig: (cfg) => applyDeepInfraConfig(cfg),
wizard: {
choiceId: "deepinfra-api-key",
choiceLabel: "DeepInfra API key",
choiceHint: "Unified API for open source models",
groupId: PROVIDER_ID,
groupLabel: "DeepInfra",
groupHint: "Unified API for open source models",
},
},
],
catalog: {
order: "simple",
run: (ctx: ProviderCatalogContext) => buildDeepInfraApiKeyCatalog(ctx),
staticRun: async () => ({ provider: buildStaticDeepInfraProvider() }),
},
augmentModelCatalog: async ({ config, env, agentDir }) => {
const configured = readConfiguredProviderCatalogEntries({
config,
providerId: PROVIDER_ID,
});
// Gate dynamic discovery on the user having configured a DeepInfra API
// key (env var, config SecretInput, or auth-profile store).
// Pre-auth flows keep the curated manifest fallback so the model picker
// stays tight and startup stays offline-friendly.
const hasApiKey = hasDeepInfraApiKey({ env, agentDir, config });
const seen = new Set(configured.map((entry) => entry.id));
const discovered = await discoverDeepInfraModels({ hasApiKey, env, agentDir });
const merged: ConfiguredProviderCatalogEntry[] = [...configured];
for (const model of discovered) {
if (seen.has(model.id)) {
continue;
}
seen.add(model.id);
const input = model.input;
merged.push({
provider: PROVIDER_ID,
id: model.id,
name: model.name ?? model.id,
...(typeof model.contextWindow === "number" && model.contextWindow > 0
? { contextWindow: model.contextWindow }
: {}),
...(typeof model.reasoning === "boolean" ? { reasoning: model.reasoning } : {}),
...(input && input.length > 0 ? { input } : {}),
});
}
return merged;
},
normalizeConfig: ({ providerConfig }) => providerConfig,
normalizeTransport: ({ api, baseUrl }) =>
baseUrl === "https://api.deepinfra.com/v1/openai" ? { api, baseUrl } : undefined,
...PASSTHROUGH_GEMINI_REPLAY_HOOKS,
wrapStreamFn: (ctx) => {
const thinkingLevel = isProxyReasoningUnsupported(ctx.modelId)
? undefined
: ctx.thinkingLevel;
// OpenRouter wrapper handles reasoning normalization for proxy-style
// providers; layer DeepInfra's anthropic cache-marker wrapper on top so
// anthropic/* requests carry the ephemeral cache_control markers that
// the upstream OpenRouter-only wrapper skips.
return createDeepInfraAnthropicCacheWrapper(
createOpenRouterWrapper(ctx.streamFn, thinkingLevel),
);
},
isModernModelRef: () => true,
isCacheTtlEligible: (ctx) => ctx.modelId.toLowerCase().startsWith("anthropic/"),
},
register(api) {
// Single source for media defaults at register time; image-gen and
// video-gen also get a live registerModelCatalogProvider that refreshes
// from the agent endpoint when a key is configured (OpenRouter pattern).
// TTS/STT/VLM/embed stay static until UnifiedModelCatalogKind covers them.
const catalog = getDeepInfraSurfaceFallbackCatalog();
api.registerImageGenerationProvider(
buildDeepInfraImageGenerationProvider({ imageGenModels: catalog.imageGen }),
);
api.registerModelCatalogProvider({
provider: PROVIDER_ID,
kinds: ["image_generation"],
liveCatalog: listDeepInfraImageGenCatalog,
});
api.registerMediaUnderstandingProvider(
buildDeepInfraMediaUnderstandingProvider({
vlmModels: catalog.vlm,
sttModels: catalog.stt,
}),
);
api.registerMemoryEmbeddingProvider(
buildDeepInfraMemoryEmbeddingAdapter({ embedModels: catalog.embed }),
);
api.registerSpeechProvider(buildDeepInfraSpeechProvider({ ttsModels: catalog.tts }));
api.registerVideoGenerationProvider(
buildDeepInfraVideoGenerationProvider({ videoGenModels: catalog.videoGen }),
);
api.registerModelCatalogProvider({
provider: PROVIDER_ID,
kinds: ["video_generation"],
liveCatalog: listDeepInfraVideoGenCatalog,
});
},
});

View File

@@ -0,0 +1,55 @@
// Deepinfra plugin module implements media models behavior.
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { DEEPINFRA_BASE_URL } from "./provider-models.js";
export { DEEPINFRA_BASE_URL };
export const DEEPINFRA_NATIVE_BASE_URL = "https://api.deepinfra.com/v1/inference";
// Structural capability shapes — not model IDs.
export const DEFAULT_DEEPINFRA_IMAGE_SIZE = "1024x1024";
export const DEFAULT_DEEPINFRA_TTS_VOICE = "af_bella";
export const DEEPINFRA_VIDEO_ASPECT_RATIOS = ["16:9", "4:3", "1:1", "3:4", "9:16"] as const;
export const DEEPINFRA_VIDEO_DURATIONS = [5, 8] as const;
// Per-surface fallback lists — used when no discovered/static catalog is
// supplied. First entry is the default. Prefer discoverDeepInfraSurfaces().
export const DEEPINFRA_IMAGE_FALLBACK_MODELS = [
"black-forest-labs/FLUX-1-schnell",
"run-diffusion/Juggernaut-Lightning-Flux",
"black-forest-labs/FLUX-1-dev",
"Qwen/Qwen-Image-Max",
"stabilityai/sdxl-turbo",
] as const;
export const DEEPINFRA_TTS_FALLBACK_MODELS = [
"hexgrad/Kokoro-82M",
"Qwen/Qwen3-TTS",
"ResembleAI/chatterbox-turbo",
"sesame/csm-1b",
] as const;
export const DEEPINFRA_VIDEO_FALLBACK_MODELS = [
"Pixverse/Pixverse-T2V",
"Pixverse/Pixverse-T2V-HD",
"Wan-AI/Wan2.6-T2V",
"google/veo-3.1-fast",
] as const;
export const DEEPINFRA_STT_FALLBACK_MODELS = [
"openai/whisper-large-v3-turbo",
"openai/whisper-large-v3",
] as const;
export const DEEPINFRA_EMBED_FALLBACK_MODELS = ["BAAI/bge-m3"] as const;
export const DEEPINFRA_VLM_FALLBACK_MODELS = ["moonshotai/Kimi-K2.5"] as const;
export function normalizeDeepInfraModelRef(model: string | undefined, fallback: string): string {
const value = normalizeOptionalString(model) ?? fallback;
return value.startsWith("deepinfra/") ? value.slice("deepinfra/".length) : value;
}
export function normalizeDeepInfraBaseUrl(value: unknown, fallback = DEEPINFRA_BASE_URL): string {
return (normalizeOptionalString(value) ?? fallback).replace(/\/+$/u, "");
}

View File

@@ -0,0 +1,74 @@
// Deepinfra tests cover media understanding provider plugin behavior.
import {
describeImagesWithModel,
describeImageWithModel,
} from "openclaw/plugin-sdk/media-understanding";
import { afterAll, describe, expect, it, vi } from "vitest";
import {
deepinfraMediaUnderstandingProvider,
transcribeDeepInfraAudio,
} from "./media-understanding-provider.js";
const { transcribeOpenAiCompatibleAudioMock } = vi.hoisted(() => ({
transcribeOpenAiCompatibleAudioMock: vi.fn(async () => ({ text: "hello", model: "whisper" })),
}));
vi.mock("openclaw/plugin-sdk/media-understanding", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/media-understanding")>(
"openclaw/plugin-sdk/media-understanding",
);
return {
...actual,
transcribeOpenAiCompatibleAudio: transcribeOpenAiCompatibleAudioMock,
};
});
afterAll(() => {
vi.doUnmock("openclaw/plugin-sdk/media-understanding");
vi.resetModules();
});
describe("deepinfra media understanding provider", () => {
it("declares image and audio defaults", () => {
expect(deepinfraMediaUnderstandingProvider).toEqual({
id: "deepinfra",
capabilities: ["image", "audio"],
defaultModels: {
image: "moonshotai/Kimi-K2.5",
audio: "openai/whisper-large-v3-turbo",
},
autoPriority: {
image: 45,
audio: 45,
},
transcribeAudio: transcribeDeepInfraAudio,
describeImage: describeImageWithModel,
describeImages: describeImagesWithModel,
});
});
it("routes audio transcription through the OpenAI-compatible DeepInfra endpoint", async () => {
const buffer = Buffer.from("audio");
const result = await transcribeDeepInfraAudio({
buffer,
fileName: "clip.mp3",
apiKey: "deepinfra-key",
timeoutMs: 30_000,
});
expect(result).toEqual({ text: "hello", model: "whisper" });
expect(transcribeOpenAiCompatibleAudioMock.mock.calls).toEqual([
[
{
buffer,
fileName: "clip.mp3",
apiKey: "deepinfra-key",
timeoutMs: 30_000,
provider: "deepinfra",
defaultBaseUrl: "https://api.deepinfra.com/v1/openai",
defaultModel: "openai/whisper-large-v3-turbo",
},
],
]);
});
});

View File

@@ -0,0 +1,57 @@
// Deepinfra provider module implements model/runtime integration.
import {
describeImageWithModel,
describeImagesWithModel,
transcribeOpenAiCompatibleAudio,
type AudioTranscriptionRequest,
type MediaUnderstandingProvider,
} from "openclaw/plugin-sdk/media-understanding";
import {
DEEPINFRA_BASE_URL,
DEEPINFRA_STT_FALLBACK_MODELS,
DEEPINFRA_VLM_FALLBACK_MODELS,
} from "./media-models.js";
import type { DeepInfraSurfaceModel } from "./provider-models.js";
function resolveDefault(
surfaceModels: readonly DeepInfraSurfaceModel[] | undefined,
fallback: readonly string[],
): string {
const first = surfaceModels?.[0]?.id;
return first ?? fallback[0] ?? "";
}
export async function transcribeDeepInfraAudio(params: AudioTranscriptionRequest) {
return await transcribeOpenAiCompatibleAudio({
...params,
provider: "deepinfra",
defaultBaseUrl: DEEPINFRA_BASE_URL,
defaultModel: resolveDefault(undefined, DEEPINFRA_STT_FALLBACK_MODELS),
});
}
// First entries of vlmModels / sttModels become the image / audio defaults.
export function buildDeepInfraMediaUnderstandingProvider(options?: {
vlmModels?: readonly DeepInfraSurfaceModel[];
sttModels?: readonly DeepInfraSurfaceModel[];
}): MediaUnderstandingProvider {
return {
id: "deepinfra",
capabilities: ["image", "audio"],
defaultModels: {
image: resolveDefault(options?.vlmModels, DEEPINFRA_VLM_FALLBACK_MODELS),
audio: resolveDefault(options?.sttModels, DEEPINFRA_STT_FALLBACK_MODELS),
},
autoPriority: {
image: 45,
audio: 45,
},
transcribeAudio: transcribeDeepInfraAudio,
describeImage: describeImageWithModel,
describeImages: describeImagesWithModel,
};
}
// Back-compat const for callers not yet on the builder. Static fallback only.
export const deepinfraMediaUnderstandingProvider: MediaUnderstandingProvider =
buildDeepInfraMediaUnderstandingProvider();

View File

@@ -0,0 +1,32 @@
// Deepinfra tests cover memory embedding adapter plugin behavior.
import { isMissingEmbeddingApiKeyError } from "openclaw/plugin-sdk/memory-core-host-engine-embeddings";
import { describe, expect, it } from "vitest";
import { DEFAULT_DEEPINFRA_EMBEDDING_MODEL } from "./embedding-provider.js";
import { deepinfraMemoryEmbeddingProviderAdapter } from "./memory-embedding-adapter.js";
describe("deepinfra memory embedding adapter", () => {
it("declares a remote auth-backed embedding provider", () => {
expect(Object.keys(deepinfraMemoryEmbeddingProviderAdapter)).toEqual([
"id",
"defaultModel",
"transport",
"authProviderId",
"autoSelectPriority",
"allowExplicitWhenConfiguredAuto",
"shouldContinueAutoSelection",
"create",
]);
expect(deepinfraMemoryEmbeddingProviderAdapter.id).toBe("deepinfra");
expect(deepinfraMemoryEmbeddingProviderAdapter.defaultModel).toBe(
DEFAULT_DEEPINFRA_EMBEDDING_MODEL,
);
expect(deepinfraMemoryEmbeddingProviderAdapter.transport).toBe("remote");
expect(deepinfraMemoryEmbeddingProviderAdapter.authProviderId).toBe("deepinfra");
expect(deepinfraMemoryEmbeddingProviderAdapter.autoSelectPriority).toBe(55);
expect(deepinfraMemoryEmbeddingProviderAdapter.allowExplicitWhenConfiguredAuto).toBe(true);
expect(deepinfraMemoryEmbeddingProviderAdapter.shouldContinueAutoSelection).toBe(
isMissingEmbeddingApiKeyError,
);
expect(deepinfraMemoryEmbeddingProviderAdapter.create).toBeTypeOf("function");
});
});

View File

@@ -0,0 +1,48 @@
// Deepinfra plugin module implements memory embedding adapter behavior.
import {
isMissingEmbeddingApiKeyError,
type MemoryEmbeddingProviderAdapter,
} from "openclaw/plugin-sdk/memory-core-host-engine-embeddings";
import {
createDeepInfraEmbeddingProvider,
DEFAULT_DEEPINFRA_EMBEDDING_MODEL,
} from "./embedding-provider.js";
import type { DeepInfraSurfaceModel } from "./provider-models.js";
// First entry of embedModels becomes the default embedding model.
export function buildDeepInfraMemoryEmbeddingAdapter(options?: {
embedModels?: readonly DeepInfraSurfaceModel[];
}): MemoryEmbeddingProviderAdapter {
const defaultModel = options?.embedModels?.[0]?.id ?? DEFAULT_DEEPINFRA_EMBEDDING_MODEL;
return {
id: "deepinfra",
defaultModel,
transport: "remote",
authProviderId: "deepinfra",
autoSelectPriority: 55,
allowExplicitWhenConfiguredAuto: true,
shouldContinueAutoSelection: isMissingEmbeddingApiKeyError,
create: async (createOptions) => {
const { provider, client } = await createDeepInfraEmbeddingProvider({
...createOptions,
provider: "deepinfra",
fallback: "none",
defaultModel,
});
return {
provider,
runtime: {
id: "deepinfra",
cacheKeyData: {
provider: "deepinfra",
model: client.model,
},
},
};
},
};
}
// Back-compat const for callers not yet on the builder.
export const deepinfraMemoryEmbeddingProviderAdapter: MemoryEmbeddingProviderAdapter =
buildDeepInfraMemoryEmbeddingAdapter();

12
extensions/deepinfra/npm-shrinkwrap.json generated Normal file
View File

@@ -0,0 +1,12 @@
{
"name": "@openclaw/deepinfra-provider",
"version": "2026.6.11",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openclaw/deepinfra-provider",
"version": "2026.6.11"
}
}
}

View File

@@ -0,0 +1,141 @@
// Deepinfra tests cover onboard plugin behavior.
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import * as providerAuth from "openclaw/plugin-sdk/provider-auth-runtime";
import {
type OpenClawConfig,
resolveAgentModelPrimaryValue,
} from "openclaw/plugin-sdk/provider-onboard";
import { captureEnv } from "openclaw/plugin-sdk/test-env";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
applyDeepInfraConfig,
DEEPINFRA_BASE_URL,
DEEPINFRA_DEFAULT_MODEL_REF,
} from "./onboard.js";
import { DEEPINFRA_DEFAULT_MODEL_ID } from "./provider-models.js";
const { resolveEnvApiKey } = providerAuth;
const emptyCfg: OpenClawConfig = {};
describe("DeepInfra provider config", () => {
describe("constants", () => {
it("DEEPINFRA_BASE_URL points to deepinfra openai 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/deepseek-ai/DeepSeek-V4-Flash");
});
it("DEEPINFRA_DEFAULT_MODEL_ID is deepseek-ai/DeepSeek-V4-Flash", () => {
expect(DEEPINFRA_DEFAULT_MODEL_ID).toBe("deepseek-ai/DeepSeek-V4-Flash");
});
});
describe("applyDeepInfraConfig", () => {
it("sets the provided model ref as the primary default", () => {
const result = applyDeepInfraConfig(emptyCfg, DEEPINFRA_DEFAULT_MODEL_REF);
expect(resolveAgentModelPrimaryValue(result.agents?.defaults?.model)).toBe(
DEEPINFRA_DEFAULT_MODEL_REF,
);
});
it("sets the DeepInfra alias on the provided ref", () => {
const result = applyDeepInfraConfig(emptyCfg, DEEPINFRA_DEFAULT_MODEL_REF);
const agentModel = result.agents?.defaults?.models?.[DEEPINFRA_DEFAULT_MODEL_REF];
expect(agentModel?.alias).toBe("DeepInfra");
});
it("honors a fallback ref when discovery picked a non-default model", () => {
const fallbackRef = "deepinfra/other/awesome-model";
const result = applyDeepInfraConfig(emptyCfg, fallbackRef);
expect(resolveAgentModelPrimaryValue(result.agents?.defaults?.model)).toBe(fallbackRef);
expect(result.agents?.defaults?.models?.[fallbackRef]?.alias).toBe("DeepInfra");
});
it("preserves an existing alias on the selected model", () => {
const cfg: OpenClawConfig = {
agents: {
defaults: {
models: {
[DEEPINFRA_DEFAULT_MODEL_REF]: { alias: "My Custom Alias" },
},
},
},
};
const result = applyDeepInfraConfig(cfg, DEEPINFRA_DEFAULT_MODEL_REF);
expect(result.agents?.defaults?.models?.[DEEPINFRA_DEFAULT_MODEL_REF]?.alias).toBe(
"My Custom Alias",
);
});
});
describe("env var resolution", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("resolves DEEPINFRA_API_KEY from env", () => {
const envSnapshot = captureEnv(["DEEPINFRA_API_KEY"]);
process.env.DEEPINFRA_API_KEY = "test-deepinfra-key";
try {
const result = resolveEnvApiKey("deepinfra");
expect(result?.apiKey).toBe("test-deepinfra-key");
expect(result?.source.endsWith("DEEPINFRA_API_KEY")).toBe(true);
} 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";
const spy = vi.spyOn(providerAuth, "resolveApiKeyForProvider").mockResolvedValue({
apiKey: "deepinfra-provider-test-key",
source: "env: DEEPINFRA_API_KEY",
mode: "api-key",
});
try {
const auth = await providerAuth.resolveApiKeyForProvider({
provider: "deepinfra",
agentDir,
});
expect(spy.mock.calls).toEqual([
[
{
provider: "deepinfra",
agentDir,
},
],
]);
expect(auth).toEqual({
apiKey: "deepinfra-provider-test-key",
source: "env: DEEPINFRA_API_KEY",
mode: "api-key",
});
} finally {
envSnapshot.restore();
}
});
});
});

View File

@@ -0,0 +1,30 @@
// Deepinfra setup module handles plugin onboarding behavior.
import {
applyAgentDefaultModelPrimary,
type OpenClawConfig,
} from "openclaw/plugin-sdk/provider-onboard";
import { DEEPINFRA_BASE_URL, DEEPINFRA_DEFAULT_MODEL_REF } from "./provider-models.js";
export { DEEPINFRA_BASE_URL, DEEPINFRA_DEFAULT_MODEL_REF };
export function applyDeepInfraConfig(
cfg: OpenClawConfig,
modelRef: string = DEEPINFRA_DEFAULT_MODEL_REF,
): OpenClawConfig {
const models = { ...cfg.agents?.defaults?.models };
models[modelRef] = {
...models[modelRef],
alias: models[modelRef]?.alias ?? "DeepInfra",
};
return applyAgentDefaultModelPrimary({
...cfg,
agents: {
...cfg.agents,
defaults: {
...cfg.agents?.defaults,
models,
},
},
}, modelRef);
}

View File

@@ -0,0 +1,224 @@
{
"id": "deepinfra",
"activation": {
"onStartup": false
},
"enabledByDefault": true,
"providers": ["deepinfra"],
"providerCatalogEntry": "./provider-discovery.ts",
"providerAuthEnvVars": {
"deepinfra": ["DEEPINFRA_API_KEY"]
},
"providerEndpoints": [
{
"endpointClass": "deepinfra-native",
"hosts": ["api.deepinfra.com"]
}
],
"providerRequest": {
"providers": {
"deepinfra": {
"family": "deepinfra"
}
}
},
"setup": {
"providers": [
{
"id": "deepinfra",
"authMethods": ["api-key"],
"envVars": ["DEEPINFRA_API_KEY"]
}
]
},
"modelCatalog": {
"providers": {
"deepinfra": {
"baseUrl": "https://api.deepinfra.com/v1/openai",
"api": "openai-completions",
"models": [
{
"id": "deepseek-ai/DeepSeek-V4-Flash",
"name": "DeepSeek V4 Flash",
"reasoning": true,
"input": ["text"],
"contextWindow": 1048576,
"maxTokens": 1048576,
"cost": {
"input": 0.1,
"output": 0.2,
"cacheRead": 0.02,
"cacheWrite": 0
},
"compat": {
"supportsUsageInStreaming": true
}
},
{
"id": "deepseek-ai/DeepSeek-V3.2",
"name": "DeepSeek V3.2",
"reasoning": false,
"input": ["text"],
"contextWindow": 163840,
"maxTokens": 163840,
"cost": {
"input": 0.26,
"output": 0.38,
"cacheRead": 0.13,
"cacheWrite": 0
},
"compat": {
"supportsUsageInStreaming": true
}
},
{
"id": "zai-org/GLM-5.1",
"name": "GLM-5.1",
"reasoning": true,
"input": ["text"],
"contextWindow": 202752,
"maxTokens": 202752,
"cost": {
"input": 1.05,
"output": 3.5,
"cacheRead": 0.205000005,
"cacheWrite": 0
},
"compat": {
"supportsUsageInStreaming": true
}
},
{
"id": "stepfun-ai/Step-3.5-Flash",
"name": "Step 3.5 Flash",
"reasoning": false,
"input": ["text"],
"contextWindow": 262144,
"maxTokens": 262144,
"cost": {
"input": 0.1,
"output": 0.3,
"cacheRead": 0.02,
"cacheWrite": 0
},
"compat": {
"supportsUsageInStreaming": true
}
},
{
"id": "MiniMaxAI/MiniMax-M2.5",
"name": "MiniMax M2.5",
"reasoning": true,
"input": ["text"],
"contextWindow": 196608,
"maxTokens": 196608,
"cost": {
"input": 0.15,
"output": 1.15,
"cacheRead": 0.03,
"cacheWrite": 0
},
"compat": {
"supportsUsageInStreaming": true
}
},
{
"id": "moonshotai/Kimi-K2.5",
"name": "Kimi K2.5",
"reasoning": true,
"input": ["text", "image"],
"contextWindow": 262144,
"maxTokens": 262144,
"cost": {
"input": 0.45,
"output": 2.25,
"cacheRead": 0.070000002,
"cacheWrite": 0
},
"compat": {
"supportsUsageInStreaming": true
}
},
{
"id": "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B",
"name": "NVIDIA Nemotron 3 Super 120B A12B",
"reasoning": true,
"input": ["text"],
"contextWindow": 262144,
"maxTokens": 262144,
"cost": {
"input": 0.1,
"output": 0.5,
"cacheRead": 0,
"cacheWrite": 0
},
"compat": {
"supportsUsageInStreaming": true
}
},
{
"id": "meta-llama/Llama-3.3-70B-Instruct-Turbo",
"name": "Llama 3.3 70B Instruct Turbo",
"reasoning": false,
"input": ["text"],
"contextWindow": 131072,
"maxTokens": 131072,
"cost": {
"input": 0.1,
"output": 0.32,
"cacheRead": 0,
"cacheWrite": 0
},
"compat": {
"supportsUsageInStreaming": true
}
}
]
}
},
"discovery": {
"deepinfra": "refreshable"
}
},
"providerAuthChoices": [
{
"provider": "deepinfra",
"method": "api-key",
"choiceId": "deepinfra-api-key",
"choiceLabel": "DeepInfra API key",
"choiceHint": "Unified API for open source models",
"groupId": "deepinfra",
"groupLabel": "DeepInfra",
"groupHint": "Unified API for open source models",
"optionKey": "deepinfraApiKey",
"cliFlag": "--deepinfra-api-key",
"cliOption": "--deepinfra-api-key <key>",
"cliDescription": "DeepInfra API key"
}
],
"contracts": {
"mediaUnderstandingProviders": ["deepinfra"],
"memoryEmbeddingProviders": ["deepinfra"],
"imageGenerationProviders": ["deepinfra"],
"speechProviders": ["deepinfra"],
"videoGenerationProviders": ["deepinfra"]
},
"mediaUnderstandingProviderMetadata": {
"deepinfra": {
"capabilities": ["image", "audio"],
"defaultModels": {
"image": "moonshotai/Kimi-K2.5",
"audio": "openai/whisper-large-v3-turbo"
},
"autoPriority": {
"image": 45,
"audio": 45
}
}
},
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {}
}
}

View File

@@ -0,0 +1,35 @@
{
"name": "@openclaw/deepinfra-provider",
"version": "2026.6.11",
"description": "OpenClaw DeepInfra provider plugin.",
"repository": {
"type": "git",
"url": "https://github.com/openclaw/openclaw"
},
"type": "module",
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"
},
"openclaw": {
"extensions": [
"./index.ts"
],
"install": {
"clawhubSpec": "clawhub:@openclaw/deepinfra-provider",
"npmSpec": "@openclaw/deepinfra-provider",
"defaultChoice": "npm",
"minHostVersion": ">=2026.6.8"
},
"compat": {
"pluginApi": ">=2026.6.11"
},
"build": {
"openclawVersion": "2026.6.11",
"bundledDist": false
},
"release": {
"publishToClawHub": true,
"publishToNpm": true
}
}
}

View File

@@ -0,0 +1,51 @@
// Deepinfra provider module implements model/runtime integration.
import {
buildSingleProviderApiKeyCatalog,
type ProviderCatalogContext,
type ProviderCatalogResult,
} from "openclaw/plugin-sdk/provider-catalog-shared";
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared";
import {
DEEPINFRA_BASE_URL,
DEEPINFRA_MODEL_CATALOG,
buildDeepInfraModelDefinition,
discoverDeepInfraModels,
} from "./provider-models.js";
export function buildStaticDeepInfraProvider(): ModelProviderConfig {
return {
baseUrl: DEEPINFRA_BASE_URL,
api: "openai-completions",
models: DEEPINFRA_MODEL_CATALOG.map(buildDeepInfraModelDefinition),
};
}
export async function buildDeepInfraProvider(options?: {
hasApiKey?: boolean;
env?: NodeJS.ProcessEnv;
agentDir?: string;
}): Promise<ModelProviderConfig> {
const models = await discoverDeepInfraModels(options);
return {
baseUrl: DEEPINFRA_BASE_URL,
api: "openai-completions",
models,
};
}
export function buildDeepInfraApiKeyCatalog(
ctx: ProviderCatalogContext,
): Promise<ProviderCatalogResult> {
return buildSingleProviderApiKeyCatalog({
ctx,
providerId: "deepinfra",
// The shared API-key helper already resolved env/profile credentials.
// Pass that fact into discovery so profile-only setups get the live catalog.
buildProvider: () =>
buildDeepInfraProvider({
hasApiKey: true,
env: ctx.env,
agentDir: ctx.agentDir,
}),
});
}

View File

@@ -0,0 +1,25 @@
// Deepinfra provider module implements model/runtime integration.
import type { ProviderCatalogContext } from "openclaw/plugin-sdk/provider-catalog-shared";
import type { ProviderPlugin } from "openclaw/plugin-sdk/provider-model-shared";
import { buildDeepInfraApiKeyCatalog, buildStaticDeepInfraProvider } from "./provider-catalog.js";
const PROVIDER_ID = "deepinfra";
const deepinfraProviderDiscovery: ProviderPlugin = {
id: PROVIDER_ID,
label: "DeepInfra",
docsPath: "/providers/deepinfra",
auth: [],
catalog: {
order: "simple",
run: (ctx: ProviderCatalogContext) => buildDeepInfraApiKeyCatalog(ctx),
},
staticCatalog: {
order: "simple",
run: async () => ({
provider: buildStaticDeepInfraProvider(),
}),
},
};
export default deepinfraProviderDiscovery;

View File

@@ -0,0 +1,522 @@
// Deepinfra tests cover provider models plugin behavior.
import { beforeEach, describe, expect, it, vi } from "vitest";
const isProviderApiKeyConfiguredMock = vi.hoisted(() => vi.fn<(p: unknown) => boolean>());
vi.mock("openclaw/plugin-sdk/provider-auth", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/provider-auth")>(
"openclaw/plugin-sdk/provider-auth",
);
return {
...actual,
isProviderApiKeyConfigured: isProviderApiKeyConfiguredMock,
};
});
import {
DEEPINFRA_MODELS_URL,
DEEPINFRA_DEFAULT_MODEL_REF,
DEEPINFRA_MODEL_CATALOG,
discoverDeepInfraModels,
discoverDeepInfraSurfaces,
hasDeepInfraApiKey,
resetDeepInfraModelCacheForTest,
} from "./provider-models.js";
beforeEach(() => {
resetDeepInfraModelCacheForTest();
isProviderApiKeyConfiguredMock.mockReset();
isProviderApiKeyConfiguredMock.mockReturnValue(false);
});
function makeAgentModelEntry(overrides: Record<string, unknown> = {}) {
return {
id: "openai/gpt-oss-120b",
object: "model",
owned_by: "deepinfra",
metadata: {
description: "gpt-oss-120b",
context_length: 131072,
max_tokens: 65536,
pricing: {
input_tokens: 3,
output_tokens: 15,
cache_read_tokens: 0.3,
},
tags: ["chat", "vlm", "vision", "reasoning_effort", "prompt_cache", "reasoning"],
},
...overrides,
};
}
function jsonResponse(payload: unknown, init: ResponseInit = {}): Response {
return new Response(JSON.stringify(payload), {
status: 200,
headers: { "Content-Type": "application/json" },
...init,
});
}
function expectedStaticChatCatalog() {
return DEEPINFRA_MODEL_CATALOG.map((model) => {
const compat = Object.assign({}, model.compat, {
supportsUsageInStreaming: model.compat?.supportsUsageInStreaming ?? true,
});
return Object.assign({}, model, { compat });
});
}
function expectedLiveChatCatalog(liveModels: ReturnType<typeof expectedStaticChatCatalog>) {
const liveIds = new Set(liveModels.map((model) => model.id));
return [...liveModels, ...expectedStaticChatCatalog().filter((model) => !liveIds.has(model.id))];
}
async function withFetchPathTest(
mockFetch: ReturnType<typeof vi.fn>,
envOverrides: Record<string, string | undefined>,
runAssertions: () => Promise<void>,
) {
const env = { ...process.env };
delete process.env.NODE_ENV;
delete process.env.VITEST;
for (const [key, value] of Object.entries(envOverrides)) {
if (value === undefined) {
delete process.env[key];
} else {
Reflect.set(process.env, key, value);
}
}
vi.stubGlobal("fetch", mockFetch);
try {
await runAssertions();
} finally {
for (const key of Object.keys(envOverrides)) {
if (env[key] === undefined) {
delete process.env[key];
} else {
Reflect.set(process.env, key, env[key]);
}
}
if (env.NODE_ENV !== undefined) {
process.env.NODE_ENV = env.NODE_ENV;
}
if (env.VITEST !== undefined) {
process.env.VITEST = env.VITEST;
}
vi.unstubAllGlobals();
}
}
function requireFirstFetchCall(mockFetch: ReturnType<typeof vi.fn>): [unknown, unknown] {
const [call] = mockFetch.mock.calls;
if (!call) {
throw new Error("expected DeepInfra models fetch call");
}
return call as [unknown, unknown];
}
describe("DEEPINFRA_MODELS_URL", () => {
it("points at /v1/openai/models with the openclaw sort + filter=with_meta gate", () => {
expect(DEEPINFRA_MODELS_URL).toBe(
"https://api.deepinfra.com/v1/openai/models?sort_by=openclaw&filter=with_meta",
);
});
});
describe("hasDeepInfraApiKey", () => {
it("returns true via env var, false on missing / blank", () => {
expect(hasDeepInfraApiKey({ env: { DEEPINFRA_API_KEY: "sk-x" } })).toBe(true);
expect(hasDeepInfraApiKey({ env: { DEEPINFRA_API_KEY: "" } })).toBe(false);
expect(hasDeepInfraApiKey({ env: { DEEPINFRA_API_KEY: " " } })).toBe(false);
expect(hasDeepInfraApiKey({ env: {} })).toBe(false);
});
it("falls back to the auth-profile store when no env var is set", () => {
isProviderApiKeyConfiguredMock.mockReturnValue(true);
expect(hasDeepInfraApiKey({ env: {}, agentDir: "/tmp/openclaw-agent" })).toBe(true);
expect(isProviderApiKeyConfiguredMock).toHaveBeenCalledTimes(1);
expect(isProviderApiKeyConfiguredMock).toHaveBeenCalledWith({
provider: "deepinfra",
agentDir: "/tmp/openclaw-agent",
});
});
it("accepts config-backed provider API keys before probing the profile store", () => {
expect(
hasDeepInfraApiKey({
env: {},
agentDir: "/tmp/openclaw-agent",
config: {
models: {
providers: {
deepinfra: {
apiKey: { source: "env", provider: "default", id: "CUSTOM_DEEPINFRA_KEY" },
},
},
},
},
}),
).toBe(true);
expect(isProviderApiKeyConfiguredMock).not.toHaveBeenCalled();
});
it("short-circuits on env var and skips the profile-store probe", () => {
isProviderApiKeyConfiguredMock.mockReturnValue(true);
expect(
hasDeepInfraApiKey({
env: { DEEPINFRA_API_KEY: "sk-x" },
agentDir: "/tmp/openclaw-agent",
}),
).toBe(true);
expect(isProviderApiKeyConfiguredMock).not.toHaveBeenCalled();
});
it("returns false when env is empty and the auth-profile store has no deepinfra profile", () => {
isProviderApiKeyConfiguredMock.mockReturnValue(false);
expect(hasDeepInfraApiKey({ env: {}, agentDir: "/tmp/openclaw-agent" })).toBe(false);
expect(isProviderApiKeyConfiguredMock).toHaveBeenCalledWith({
provider: "deepinfra",
agentDir: "/tmp/openclaw-agent",
});
});
});
describe("discoverDeepInfraModels (chat-only shim)", () => {
it("returns static catalog in test environment", async () => {
const models = await discoverDeepInfraModels();
const modelIds = models.map((m) => m.id);
const streamingUsageIncompatibleModelIds = models
.filter((m) => !m.compat?.supportsUsageInStreaming)
.map((m) => m.id);
expect(DEEPINFRA_DEFAULT_MODEL_REF).toBe("deepinfra/deepseek-ai/DeepSeek-V4-Flash");
expect(models).toStrictEqual(expectedStaticChatCatalog());
expect(modelIds).toStrictEqual(expectedStaticChatCatalog().map((model) => model.id));
expect(streamingUsageIncompatibleModelIds).toStrictEqual([]);
});
it("fetches the openclaw-projection endpoint and parses chat-surface entries when an API key is configured", async () => {
const mockFetch = vi.fn().mockResolvedValue(jsonResponse({ data: [makeAgentModelEntry()] }));
await withFetchPathTest(mockFetch, { DEEPINFRA_API_KEY: "sk-test" }, async () => {
const models = await discoverDeepInfraModels();
expect(mockFetch).toHaveBeenCalledOnce();
const [fetchUrl, fetchInit] = requireFirstFetchCall(mockFetch);
const fetchSignal = Reflect.get(fetchInit ?? {}, "signal");
const fetchHeaders = Reflect.get(fetchInit ?? {}, "headers");
expect(fetchUrl).toBe(DEEPINFRA_MODELS_URL);
expect(fetchSignal).toBeInstanceOf(AbortSignal);
expect(fetchHeaders).toBeInstanceOf(Headers);
expect((fetchHeaders as Headers).get("Accept")).toBe("application/json");
expect(models).toEqual(
expectedLiveChatCatalog([
{
id: "openai/gpt-oss-120b",
name: "openai/gpt-oss-120b",
reasoning: true,
input: ["text", "image"],
contextWindow: 131072,
maxTokens: 65536,
cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 0 },
compat: { supportsUsageInStreaming: true },
},
]),
);
});
});
it("skips entries with no metadata or no surface tag, and deduplicates ids", async () => {
const mockFetch = vi.fn().mockResolvedValue(
jsonResponse({
data: [
{ id: "BAAI/bge-m3", object: "model", metadata: null },
makeAgentModelEntry({
id: "untagged/model",
metadata: { context_length: 1, max_tokens: 1, pricing: {}, tags: [] },
}),
makeAgentModelEntry(),
makeAgentModelEntry(),
],
}),
);
await withFetchPathTest(mockFetch, { DEEPINFRA_API_KEY: "sk-test" }, async () => {
const models = await discoverDeepInfraModels();
expect(models.map((m) => m.id)).toEqual(
expectedLiveChatCatalog([
{
id: "openai/gpt-oss-120b",
name: "openai/gpt-oss-120b",
reasoning: true,
input: ["text", "image"],
contextWindow: 131072,
maxTokens: 65536,
cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 0 },
compat: { supportsUsageInStreaming: true },
},
]).map((model) => model.id),
);
});
});
it("falls back to the static catalog when no API key is configured (skips network entirely)", async () => {
const mockFetch = vi.fn();
await withFetchPathTest(mockFetch, { DEEPINFRA_API_KEY: undefined }, async () => {
const models = await discoverDeepInfraModels();
expect(mockFetch).not.toHaveBeenCalled();
expect(models.map((m) => m.id)).toEqual(expectedStaticChatCatalog().map((model) => model.id));
});
});
it("falls back to the static catalog on network errors", async () => {
const mockFetch = vi.fn().mockRejectedValue(new Error("network error"));
await withFetchPathTest(mockFetch, { DEEPINFRA_API_KEY: "sk-test" }, async () => {
const models = await discoverDeepInfraModels();
expect(models.map((m) => m.id)).toEqual(expectedStaticChatCatalog().map((model) => model.id));
});
});
it("falls back to the static catalog on non-2xx HTTP responses", async () => {
const mockFetch = vi.fn().mockResolvedValue(new Response("", { status: 503 }));
await withFetchPathTest(mockFetch, { DEEPINFRA_API_KEY: "sk-test" }, async () => {
const models = await discoverDeepInfraModels();
expect(models.map((m) => m.id)).toEqual(expectedStaticChatCatalog().map((model) => model.id));
});
});
it("falls back without caching malformed successful model list payloads", async () => {
const mockFetch = vi
.fn()
.mockResolvedValueOnce(jsonResponse({ data: {} }))
.mockResolvedValueOnce(
jsonResponse({ data: [makeAgentModelEntry({ id: "recovered/model" })] }),
);
await withFetchPathTest(mockFetch, { DEEPINFRA_API_KEY: "sk-test" }, async () => {
expect((await discoverDeepInfraModels()).map((m) => m.id)).toEqual(
expectedStaticChatCatalog().map((model) => model.id),
);
expect((await discoverDeepInfraModels()).map((m) => m.id)).toEqual(
expectedLiveChatCatalog([
{
id: "recovered/model",
name: "recovered/model",
reasoning: true,
input: ["text", "image"],
contextWindow: 131072,
maxTokens: 65536,
cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 0 },
compat: { supportsUsageInStreaming: true },
},
]).map((model) => model.id),
);
expect(mockFetch).toHaveBeenCalledTimes(2);
});
});
it("caches successful discovery responses only", async () => {
const mockFetch = vi
.fn()
.mockResolvedValueOnce(jsonResponse({ data: [makeAgentModelEntry({ id: "first/model" })] }))
.mockResolvedValueOnce(jsonResponse({ data: [makeAgentModelEntry({ id: "second/model" })] }));
await withFetchPathTest(mockFetch, { DEEPINFRA_API_KEY: "sk-test" }, async () => {
const expectedIds = expectedLiveChatCatalog([
{
id: "first/model",
name: "first/model",
reasoning: true,
input: ["text", "image"],
contextWindow: 131072,
maxTokens: 65536,
cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 0 },
compat: { supportsUsageInStreaming: true },
},
]).map((model) => model.id);
expect((await discoverDeepInfraModels()).map((m) => m.id)).toEqual(expectedIds);
expect((await discoverDeepInfraModels()).map((m) => m.id)).toEqual(expectedIds);
expect(mockFetch).toHaveBeenCalledTimes(1);
});
});
it("does not cache successful responses that produce no live catalog rows", async () => {
const mockFetch = vi
.fn()
.mockResolvedValueOnce(jsonResponse({ data: [] }))
.mockResolvedValueOnce(
jsonResponse({ data: [makeAgentModelEntry({ id: "recovered/model" })] }),
);
await withFetchPathTest(mockFetch, { DEEPINFRA_API_KEY: "sk-test" }, async () => {
expect((await discoverDeepInfraModels()).map((m) => m.id)).toEqual(
expectedStaticChatCatalog().map((model) => model.id),
);
expect((await discoverDeepInfraModels()).map((m) => m.id)).toEqual(
expectedLiveChatCatalog([
{
id: "recovered/model",
name: "recovered/model",
reasoning: true,
input: ["text", "image"],
contextWindow: 131072,
maxTokens: 65536,
cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 0 },
compat: { supportsUsageInStreaming: true },
},
]).map((model) => model.id),
);
expect(mockFetch).toHaveBeenCalledTimes(2);
});
});
});
describe("discoverDeepInfraSurfaces (per-surface bucketing)", () => {
it("buckets dynamic entries by short-alias surface tag", async () => {
const mockFetch = vi.fn().mockResolvedValue(
jsonResponse({
data: [
makeAgentModelEntry({
id: "anthropic/claude-sonnet-4-6",
metadata: {
description: "claude sonnet 4.6",
context_length: 200000,
max_tokens: 8192,
pricing: { input_tokens: 3, output_tokens: 15 },
tags: ["chat", "vlm", "vision", "prompt_cache"],
},
}),
makeAgentModelEntry({
id: "BAAI/bge-m3",
metadata: {
description: "bge-m3",
pricing: { input_tokens: 0.01 },
tags: ["embed"],
},
}),
makeAgentModelEntry({
id: "black-forest-labs/FLUX-1-schnell",
metadata: {
description: "FLUX schnell",
pricing: { per_image_unit: 0.003 },
tags: ["image-gen"],
default_width: 1024,
default_height: 1024,
default_iterations: 4,
},
}),
makeAgentModelEntry({
id: "Wan-AI/Wan2.6-T2V",
metadata: {
description: "Wan T2V",
pricing: { output_seconds: 0.05 },
tags: ["video-gen"],
},
}),
makeAgentModelEntry({
id: "Qwen/Qwen3-TTS",
metadata: {
description: "Qwen3 TTS",
pricing: { input_characters: 0.65 },
tags: ["tts"],
},
}),
makeAgentModelEntry({
id: "openai/whisper-large-v3-turbo",
metadata: {
description: "whisper",
pricing: { input_seconds: 0.00004 },
tags: ["stt"],
},
}),
],
}),
);
await withFetchPathTest(mockFetch, { DEEPINFRA_API_KEY: "sk-test" }, async () => {
const catalog = await discoverDeepInfraSurfaces();
expect(catalog.live).toBe(true);
expect(catalog.chat.map((m) => m.id)).toEqual(["anthropic/claude-sonnet-4-6"]);
expect(catalog.vlm.map((m) => m.id)).toEqual(["anthropic/claude-sonnet-4-6"]);
expect(catalog.embed.map((m) => m.id)).toEqual(["BAAI/bge-m3"]);
expect(catalog.imageGen.map((m) => m.id)).toEqual(["black-forest-labs/FLUX-1-schnell"]);
expect(catalog.imageGen[0]?.defaultWidth).toBe(1024);
expect(catalog.imageGen[0]?.pricing.per_image_unit).toBe(0.003);
expect(catalog.videoGen.map((m) => m.id)).toEqual(["Wan-AI/Wan2.6-T2V"]);
expect(catalog.tts.map((m) => m.id)).toEqual(["Qwen/Qwen3-TTS"]);
expect(catalog.stt.map((m) => m.id)).toEqual(["openai/whisper-large-v3-turbo"]);
});
});
it("drops malformed live numeric metadata", async () => {
const mockFetch = vi.fn().mockResolvedValue(
jsonResponse({
data: [
makeAgentModelEntry({
id: "bad/chat",
metadata: {
description: "bad chat",
context_length: -1,
max_tokens: 1.5,
pricing: { input_tokens: 3, output_tokens: 15 },
tags: ["chat"],
},
}),
makeAgentModelEntry({
id: "bad/image",
metadata: {
description: "bad image",
pricing: { per_image_unit: 0.003 },
tags: ["image-gen"],
default_width: Number.POSITIVE_INFINITY,
default_height: 1024.5,
default_iterations: 0,
},
}),
],
}),
);
await withFetchPathTest(mockFetch, { DEEPINFRA_API_KEY: "sk-test" }, async () => {
const catalog = await discoverDeepInfraSurfaces();
expect(catalog.chat[0]).toMatchObject({ id: "bad/chat" });
expect(catalog.chat[0]?.contextWindow).toBeUndefined();
expect(catalog.chat[0]?.maxTokens).toBeUndefined();
expect(catalog.imageGen[0]).toMatchObject({ id: "bad/image" });
expect(catalog.imageGen[0]?.defaultWidth).toBeUndefined();
expect(catalog.imageGen[0]?.defaultHeight).toBeUndefined();
expect(catalog.imageGen[0]?.defaultIterations).toBeUndefined();
});
});
it("returns the manifest static fallback (live=false) when no API key is configured", async () => {
const mockFetch = vi.fn();
await withFetchPathTest(mockFetch, { DEEPINFRA_API_KEY: undefined }, async () => {
const catalog = await discoverDeepInfraSurfaces();
expect(catalog.live).toBe(false);
expect(catalog.chat.length).toBeGreaterThan(0);
// Non-chat surfaces in the static fallback live in TS constants because
// the manifest schema only validates chat-shaped rows.
expect(catalog.imageGen.map((m) => m.id)).toContain("black-forest-labs/FLUX-1-schnell");
expect(catalog.tts.map((m) => m.id)).toContain("Qwen/Qwen3-TTS");
expect(catalog.stt.map((m) => m.id)).toContain("openai/whisper-large-v3-turbo");
// No static video-gen fallback — live discovery picks up text-to-video
// models when the backend tags them. The live-discovery test above
// covers the video-gen bucketing path.
expect(catalog.videoGen).toEqual([]);
expect(catalog.embed.map((m) => m.id)).toContain("BAAI/bge-m3");
expect(mockFetch).not.toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,475 @@
// Deepinfra provider module implements model/runtime integration.
import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth";
import {
clearLiveCatalogCacheForTests,
getCachedLiveProviderModelRows,
LiveModelCatalogHttpError,
} from "openclaw/plugin-sdk/provider-catalog-live-runtime";
import { buildManifestModelProviderConfig } from "openclaw/plugin-sdk/provider-catalog-shared";
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared";
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
import { hasConfiguredSecretInput } from "openclaw/plugin-sdk/secret-input";
import { asPositiveSafeInteger } from "openclaw/plugin-sdk/string-coerce-runtime";
import manifest from "./openclaw.plugin.json" with { type: "json" };
const log = createSubsystemLogger("deepinfra-models");
const DEEPINFRA_MANIFEST_PROVIDER = buildManifestModelProviderConfig({
providerId: "deepinfra",
catalog: manifest.modelCatalog.providers.deepinfra,
});
export const DEEPINFRA_BASE_URL = DEEPINFRA_MANIFEST_PROVIDER.baseUrl;
export const DEEPINFRA_MODELS_URL = `${DEEPINFRA_BASE_URL}/models?sort_by=openclaw&filter=with_meta`;
export const DEEPINFRA_DEFAULT_MODEL_ID = "deepseek-ai/DeepSeek-V4-Flash";
export const DEEPINFRA_DEFAULT_MODEL_REF = `deepinfra/${DEEPINFRA_DEFAULT_MODEL_ID}`;
const DEEPINFRA_DEFAULT_CONTEXT_WINDOW = 128000;
const DEEPINFRA_DEFAULT_MAX_TOKENS = 8192;
export const DEEPINFRA_MODEL_CATALOG: ModelDefinitionConfig[] = DEEPINFRA_MANIFEST_PROVIDER.models;
const DISCOVERY_TIMEOUT_MS = 5000;
const DISCOVERY_CACHE_TTL_MS = 5 * 60 * 1000;
type DeepInfraAuthConfig = {
secrets?: { defaults?: { env?: string; file?: string; exec?: string } };
models?: { providers?: Record<string, { apiKey?: unknown } | undefined> };
};
// Wire format — mirrors deepapi/agent_models_api.AgentOpenAIModelsOut.
interface DeepInfraAgentModelPricing {
// chat / vlm / embed
input_tokens?: number;
output_tokens?: number;
cache_read_tokens?: number;
// image-gen
per_image_unit?: number;
// video-gen
output_seconds?: number;
// tts
input_characters?: number;
// stt
input_seconds?: number;
}
interface DeepInfraAgentModelMetadata {
description?: string;
context_length?: number | null;
max_tokens?: number | null;
pricing?: DeepInfraAgentModelPricing;
tags?: string[];
default_width?: number | null;
default_height?: number | null;
default_iterations?: number | null;
}
interface DeepInfraAgentModelEntry {
id: string;
metadata: DeepInfraAgentModelMetadata | null;
}
export type DeepInfraSurface = "chat" | "vlm" | "embed" | "image-gen" | "video-gen" | "tts" | "stt";
export interface DeepInfraSurfaceModel {
id: string;
name: string;
description?: string;
tags: string[];
contextWindow?: number;
maxTokens?: number;
pricing: DeepInfraAgentModelPricing;
defaultWidth?: number;
defaultHeight?: number;
defaultIterations?: number;
}
export interface DeepInfraDiscoveredCatalog {
chat: DeepInfraSurfaceModel[];
vlm: DeepInfraSurfaceModel[];
embed: DeepInfraSurfaceModel[];
imageGen: DeepInfraSurfaceModel[];
videoGen: DeepInfraSurfaceModel[];
tts: DeepInfraSurfaceModel[];
stt: DeepInfraSurfaceModel[];
/** True iff served from a successful live fetch; false for the static fallback. */
live: boolean;
}
export function resetDeepInfraModelCacheForTest(): void {
clearLiveCatalogCacheForTests();
}
const SURFACE_FOR_TAG: Record<string, DeepInfraSurface> = {
chat: "chat",
vlm: "vlm",
embed: "embed",
"image-gen": "image-gen",
"video-gen": "video-gen",
tts: "tts",
stt: "stt",
};
function entryToSurfaceModel(entry: DeepInfraAgentModelEntry): DeepInfraSurfaceModel | null {
const id = typeof entry?.id === "string" ? entry.id.trim() : "";
if (!id) {
return null;
}
const metadata = entry.metadata;
if (!metadata) {
return null;
}
const tags = Array.isArray(metadata.tags)
? metadata.tags.filter((t): t is string => typeof t === "string")
: [];
const pricing: DeepInfraAgentModelPricing = metadata.pricing ?? {};
return {
id,
name: id,
description: metadata.description ?? undefined,
tags,
contextWindow: asPositiveSafeInteger(metadata.context_length),
maxTokens: asPositiveSafeInteger(metadata.max_tokens),
pricing,
defaultWidth: asPositiveSafeInteger(metadata.default_width),
defaultHeight: asPositiveSafeInteger(metadata.default_height),
defaultIterations: asPositiveSafeInteger(metadata.default_iterations),
};
}
function bucketBySurface(models: DeepInfraSurfaceModel[]): DeepInfraDiscoveredCatalog {
const catalog: DeepInfraDiscoveredCatalog = {
chat: [],
vlm: [],
embed: [],
imageGen: [],
videoGen: [],
tts: [],
stt: [],
live: true,
};
const buckets: Record<DeepInfraSurface, DeepInfraSurfaceModel[]> = {
chat: catalog.chat,
vlm: catalog.vlm,
embed: catalog.embed,
"image-gen": catalog.imageGen,
"video-gen": catalog.videoGen,
tts: catalog.tts,
stt: catalog.stt,
};
for (const model of models) {
const seen = new Set<DeepInfraSurface>();
for (const tag of model.tags) {
const surface = SURFACE_FOR_TAG[tag];
if (surface && !seen.has(surface)) {
seen.add(surface);
buckets[surface].push(model);
}
}
}
return catalog;
}
function hasDeepInfraSurfaceModelRows(rows: readonly unknown[]): boolean {
return rows.some((entry) => entryToSurfaceModel(entry as DeepInfraAgentModelEntry) !== null);
}
// Static fallback. Chat rows live in openclaw.plugin.json (manifest-validated);
// non-chat surfaces live below because the manifest validator only accepts
// chat-shaped rows. These are used pre-auth / offline; live discovery
// overrides once a key is configured.
interface ManifestChatModelEntry {
id: string;
name?: string;
contextWindow?: number;
maxTokens?: number;
reasoning?: boolean;
input?: Array<"text" | "image">;
cost?: { input?: number; output?: number; cacheRead?: number };
}
function manifestChatEntryToSurfaceModel(entry: ManifestChatModelEntry): DeepInfraSurfaceModel {
const cost = entry.cost ?? {};
const pricing: DeepInfraAgentModelPricing = {};
if (typeof cost.input === "number") {
pricing.input_tokens = cost.input;
}
if (typeof cost.output === "number") {
pricing.output_tokens = cost.output;
}
if (typeof cost.cacheRead === "number" && cost.cacheRead > 0) {
pricing.cache_read_tokens = cost.cacheRead;
}
const tags: string[] = ["chat"];
if (entry.input?.includes("image")) {
tags.push("vlm");
}
if (entry.reasoning) {
tags.push("reasoning");
}
return {
id: entry.id,
name: entry.name ?? entry.id,
tags,
contextWindow: entry.contextWindow,
maxTokens: entry.maxTokens,
pricing,
};
}
// Per-surface static fallback used only when no API key is configured or
// live discovery fails. Kept deliberately minimal: the dynamic
// `/v1/openai/models?sort_by=openclaw&filter=with_meta` projection is the
// real source of truth (140 tagged rows today), so every retired model
// removed from the DeepInfra catalog disappears here automatically the
// next time discovery runs. Newer entries — additional image-gen models,
// video-gen models, additional TTS voices — arrive through discovery
// without a code change.
//
// Every entry below is verified against the live catalog at the time of
// addition; entries are not pinned to historical shipped models if the
// upstream provider has retired them (e.g. `run-diffusion/Juggernaut-
// Lightning-Flux` was removed from DeepInfra and is therefore not listed
// even though earlier main releases shipped it as a fallback).
const STATIC_NON_CHAT_FALLBACK: DeepInfraSurfaceModel[] = [
// image-gen — representative subset of currently-served models.
{
id: "black-forest-labs/FLUX-1-schnell",
name: "black-forest-labs/FLUX-1-schnell",
tags: ["image-gen"],
pricing: { per_image_unit: 0.003 },
defaultWidth: 1024,
defaultHeight: 1024,
defaultIterations: 4,
},
{
id: "black-forest-labs/FLUX-1-dev",
name: "black-forest-labs/FLUX-1-dev",
tags: ["image-gen"],
pricing: { per_image_unit: 0.025 },
defaultWidth: 1024,
defaultHeight: 1024,
defaultIterations: 28,
},
{
id: "Qwen/Qwen-Image-Max",
name: "Qwen/Qwen-Image-Max",
tags: ["image-gen"],
pricing: { per_image_unit: 0.075 },
defaultWidth: 1024,
defaultHeight: 1024,
defaultIterations: 28,
},
{
id: "stabilityai/sdxl-turbo",
name: "stabilityai/sdxl-turbo",
tags: ["image-gen"],
pricing: { per_image_unit: 0.0002 },
defaultWidth: 1024,
defaultHeight: 1024,
defaultIterations: 4,
},
// video-gen — DeepInfra has no live video-gen catalog rows today;
// intentionally empty here. Live discovery picks up text-to-video
// models as soon as the backend tags them, no static row required.
// tts — Kokoro first so the shipped default voice (af_bella) pairs with
// the chosen default model; the rest are alternative TTS providers
// currently served by DeepInfra. Qwen3-TTS / chatterbox-turbo / csm-1b
// each require their own voice; they ship as discoverable alternatives,
// not the implicit default.
{
id: "hexgrad/Kokoro-82M",
name: "hexgrad/Kokoro-82M",
tags: ["tts"],
pricing: { input_characters: 0.65 },
},
{
id: "Qwen/Qwen3-TTS",
name: "Qwen/Qwen3-TTS",
tags: ["tts"],
pricing: { input_characters: 0.65 },
},
{
id: "ResembleAI/chatterbox-turbo",
name: "ResembleAI/chatterbox-turbo",
tags: ["tts"],
pricing: { input_characters: 1 },
},
{
id: "sesame/csm-1b",
name: "sesame/csm-1b",
tags: ["tts"],
pricing: { input_characters: 7 },
},
// stt
{
id: "openai/whisper-large-v3-turbo",
name: "openai/whisper-large-v3-turbo",
tags: ["stt"],
pricing: { input_seconds: 0.00004 },
},
// embed
{
id: "BAAI/bge-m3",
name: "BAAI/bge-m3",
tags: ["embed"],
pricing: { input_tokens: 0.01 },
maxTokens: 8192,
contextWindow: 8192,
},
];
function manifestFallbackCatalog(): DeepInfraDiscoveredCatalog {
const rawChat = (manifest.modelCatalog.providers.deepinfra.models ??
[]) as ManifestChatModelEntry[];
const chatModels = rawChat.map(manifestChatEntryToSurfaceModel);
const catalog = bucketBySurface([...chatModels, ...STATIC_NON_CHAT_FALLBACK]);
catalog.live = false;
return catalog;
}
// Sync per-surface fallback for the (sync) register callback. Media providers
// register with these defaults; live discovery feeds the chat surface via
// augmentModelCatalog and the catalog seams for image/video-gen.
export function getDeepInfraSurfaceFallbackCatalog(): DeepInfraDiscoveredCatalog {
return manifestFallbackCatalog();
}
export function buildDeepInfraModelDefinition(model: ModelDefinitionConfig): ModelDefinitionConfig {
return {
...model,
compat: {
...model.compat,
supportsUsageInStreaming: model.compat?.supportsUsageInStreaming ?? true,
},
};
}
function chatSurfaceModelToModelDefinition(model: DeepInfraSurfaceModel): ModelDefinitionConfig {
const input: Array<"text" | "image"> = model.tags.includes("vlm") ? ["text", "image"] : ["text"];
const reasoning = model.tags.includes("reasoning") || model.tags.includes("reasoning_effort");
return buildDeepInfraModelDefinition({
id: model.id,
name: model.name,
reasoning,
input,
contextWindow: model.contextWindow ?? DEEPINFRA_DEFAULT_CONTEXT_WINDOW,
maxTokens: model.maxTokens ?? DEEPINFRA_DEFAULT_MAX_TOKENS,
cost: {
input: model.pricing.input_tokens ?? 0,
output: model.pricing.output_tokens ?? 0,
cacheRead: model.pricing.cache_read_tokens ?? 0,
cacheWrite: 0,
},
});
}
// Gate dynamic discovery on key presence: pre-auth keeps the picker tight and
// avoids a useless network call. The endpoint itself is unauthenticated.
// Accepts env-var keys and auth-profile-store keys via the shared
// `isProviderApiKeyConfigured` helper (covers SecretRef / `OPENCLAW_LIVE_*`
// indirection too).
export function hasDeepInfraApiKey(options?: {
env?: NodeJS.ProcessEnv;
agentDir?: string;
config?: DeepInfraAuthConfig;
}): boolean {
const env = options?.env ?? process.env;
const fromEnv = env.DEEPINFRA_API_KEY;
if (typeof fromEnv === "string" && fromEnv.trim() !== "") {
return true;
}
const providers = options?.config?.models?.providers;
for (const [providerId, provider] of Object.entries(providers ?? {})) {
if (
providerId.trim().toLowerCase() === "deepinfra" &&
hasConfiguredSecretInput(provider?.apiKey, options?.config?.secrets?.defaults)
) {
return true;
}
}
return isProviderApiKeyConfigured({ provider: "deepinfra", agentDir: options?.agentDir });
}
// Discover the per-surface catalog. Falls back to the static manifest when
// no key, fetch fails, or running under Vitest. 5-minute cache on success.
export async function discoverDeepInfraSurfaces(options?: {
hasApiKey?: boolean;
env?: NodeJS.ProcessEnv;
agentDir?: string;
}): Promise<DeepInfraDiscoveredCatalog> {
if (process.env.NODE_ENV === "test" || process.env.VITEST) {
return manifestFallbackCatalog();
}
const env = options?.env ?? process.env;
const hasKey = options?.hasApiKey ?? hasDeepInfraApiKey({ env, agentDir: options?.agentDir });
if (!hasKey) {
return manifestFallbackCatalog();
}
try {
const data = await getCachedLiveProviderModelRows({
providerId: "deepinfra",
endpoint: DEEPINFRA_MODELS_URL,
timeoutMs: DISCOVERY_TIMEOUT_MS,
ttlMs: DISCOVERY_CACHE_TTL_MS,
buildRequestHeaders: () => ({ Accept: "application/json" }),
auditContext: "deepinfra-model-discovery",
shouldCacheRows: hasDeepInfraSurfaceModelRows,
});
if (data.length === 0) {
log.warn("No models found from DeepInfra agent-projection endpoint, using static catalog");
return manifestFallbackCatalog();
}
const seenIds = new Set<string>();
const surfaceModels: DeepInfraSurfaceModel[] = [];
for (const entry of data) {
const model = entryToSurfaceModel(entry as DeepInfraAgentModelEntry);
if (!model || seenIds.has(model.id)) {
continue;
}
seenIds.add(model.id);
surfaceModels.push(model);
}
if (surfaceModels.length === 0) {
return manifestFallbackCatalog();
}
return bucketBySurface(surfaceModels);
} catch (error) {
if (error instanceof LiveModelCatalogHttpError) {
log.warn(`Failed to discover models: HTTP ${error.status}, using static catalog`);
return manifestFallbackCatalog();
}
log.warn(`Discovery failed: ${String(error)}, using static catalog`);
return manifestFallbackCatalog();
}
}
// Chat-only shim for callers that haven't migrated to the per-surface catalog
// (provider-catalog.ts, augmentModelCatalog).
export async function discoverDeepInfraModels(options?: {
hasApiKey?: boolean;
env?: NodeJS.ProcessEnv;
agentDir?: string;
}): Promise<ModelDefinitionConfig[]> {
const catalog = await discoverDeepInfraSurfaces(options);
const chatModels = catalog.chat.length > 0 ? catalog.chat : [...catalog.chat, ...catalog.vlm];
if (chatModels.length === 0) {
// True empty (no manifest entries either) — keep behavior stable.
return DEEPINFRA_MODEL_CATALOG.map(buildDeepInfraModelDefinition);
}
const liveModels = chatModels.map(chatSurfaceModelToModelDefinition);
const seen = new Set(liveModels.map((model) => model.id));
const manifestModels = DEEPINFRA_MODEL_CATALOG.map(buildDeepInfraModelDefinition).filter(
(model) => {
if (seen.has(model.id)) {
return false;
}
seen.add(model.id);
return true;
},
);
return [...liveModels, ...manifestModels];
}

View File

@@ -0,0 +1,42 @@
// Deepinfra tests cover provider policy api plugin behavior.
import type {
ModelDefinitionConfig,
ModelProviderConfig,
} from "openclaw/plugin-sdk/provider-model-types";
import { describe, expect, it } from "vitest";
import { normalizeConfig } from "./provider-policy-api.js";
function createModel(id: string): ModelDefinitionConfig {
return {
id,
name: id,
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128_000,
maxTokens: 8_192,
};
}
describe("deepinfra provider policy public artifact", () => {
it("preserves the DeepInfra mid-path /v1 baseUrl without appending another /v1", () => {
const providerConfig: ModelProviderConfig = {
baseUrl: "https://api.deepinfra.com/v1/openai",
api: "openai-completions",
models: [createModel("zai-org/GLM-5")],
};
const normalized = normalizeConfig({ provider: "deepinfra", providerConfig });
expect(normalized.baseUrl).toBe("https://api.deepinfra.com/v1/openai");
expect(normalized.baseUrl).not.toMatch(/\/v1\/openai\/v1$/);
});
it("returns the providerConfig unchanged (referentially equal)", () => {
const providerConfig = {
baseUrl: "https://api.deepinfra.com/v1/openai",
models: [],
};
expect(normalizeConfig({ provider: "deepinfra", providerConfig })).toBe(providerConfig);
});
});

View File

@@ -0,0 +1,22 @@
// Deepinfra API module exposes the plugin public contract.
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-types";
/**
* Passthrough normalization for DeepInfra provider config.
*
* DeepInfra's OpenAI-compatible base URL is `https://api.deepinfra.com/v1/openai`
* with the `/v1` segment mid-path, not at the end. The generic
* openai-completions config normalizer strips a trailing `/v1` and re-appends
* one, which is idempotent for providers like OpenRouter (`.../api/v1`) but
* doubles to `.../v1/openai/v1` here and breaks inference (404).
*
* Shipping this bundled policy surface short-circuits the fallback normalizer
* chain (see `src/plugins/provider-runtime.ts:normalizeProviderConfigWithPlugin`)
* and preserves the DeepInfra-declared baseUrl as-is.
*/
export function normalizeConfig(params: {
provider: string;
providerConfig: ModelProviderConfig;
}): ModelProviderConfig {
return params.providerConfig;
}

View File

@@ -0,0 +1,4 @@
// Deepinfra tests cover provider.contract plugin behavior.
import { describeProviderContracts } from "openclaw/plugin-sdk/provider-test-contracts";
describeProviderContracts("deepinfra");

View File

@@ -0,0 +1,170 @@
// Deepinfra tests cover speech provider plugin behavior.
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
import { buildDeepInfraSpeechProvider } from "./speech-provider.js";
const {
assertOkOrThrowHttpErrorMock,
postJsonRequestMock,
readProviderBinaryResponseMock,
resolveProviderHttpRequestConfigMock,
} = vi.hoisted(() => ({
assertOkOrThrowHttpErrorMock: vi.fn(async () => {}),
postJsonRequestMock: vi.fn(),
readProviderBinaryResponseMock: vi.fn(async (response: Response) => {
return new Uint8Array(await response.arrayBuffer());
}),
resolveProviderHttpRequestConfigMock: vi.fn((params: Record<string, unknown>) => ({
baseUrl: params.baseUrl ?? params.defaultBaseUrl ?? "https://api.deepinfra.com/v1/openai",
allowPrivateNetwork: false,
headers: new Headers(params.defaultHeaders as HeadersInit | undefined),
dispatcherPolicy: undefined,
})),
}));
vi.mock("openclaw/plugin-sdk/provider-http", () => ({
assertOkOrThrowHttpError: assertOkOrThrowHttpErrorMock,
postJsonRequest: postJsonRequestMock,
readProviderBinaryResponse: readProviderBinaryResponseMock,
resolveProviderHttpRequestConfig: resolveProviderHttpRequestConfigMock,
}));
afterAll(() => {
vi.doUnmock("openclaw/plugin-sdk/provider-http");
vi.resetModules();
});
function requireFirstPostJsonRequest(): unknown {
const [call] = postJsonRequestMock.mock.calls;
if (!call) {
throw new Error("expected DeepInfra speech request");
}
return call[0];
}
describe("deepinfra speech provider", () => {
afterEach(() => {
assertOkOrThrowHttpErrorMock.mockClear();
postJsonRequestMock.mockReset();
readProviderBinaryResponseMock.mockClear();
resolveProviderHttpRequestConfigMock.mockClear();
vi.unstubAllEnvs();
});
it("normalizes provider-owned speech config", () => {
const provider = buildDeepInfraSpeechProvider();
const resolved = provider.resolveConfig?.({
cfg: {} as never,
timeoutMs: 30_000,
rawConfig: {
providers: {
deepinfra: {
apiKey: "sk-test",
baseUrl: "https://api.deepinfra.com/v1/openai/",
modelId: "deepinfra/hexgrad/Kokoro-82M",
voiceId: "af_bella",
speed: 1.1,
responseFormat: " MP3 ",
},
},
},
});
expect(resolved).toEqual({
apiKey: "sk-test",
baseUrl: "https://api.deepinfra.com/v1/openai",
model: "hexgrad/Kokoro-82M",
voice: "af_bella",
speed: 1.1,
responseFormat: "mp3",
extraBody: undefined,
});
});
it("synthesizes OpenAI-compatible speech through DeepInfra", async () => {
const release = vi.fn(async () => {});
postJsonRequestMock.mockResolvedValue({
response: new Response(new Uint8Array([1, 2, 3]), { status: 200 }),
release,
});
const provider = buildDeepInfraSpeechProvider();
const result = await provider.synthesize({
text: "hello",
cfg: {
models: {
providers: {
deepinfra: {
apiKey: "sk-deepinfra",
baseUrl: "https://api.deepinfra.com/v1/openai/",
},
},
},
} as never,
providerConfig: {
model: "hexgrad/Kokoro-82M",
voice: "af_bella",
speed: 1.2,
},
target: "voice-note",
timeoutMs: 12_345,
});
expect(resolveProviderHttpRequestConfigMock.mock.calls).toEqual([
[
{
baseUrl: "https://api.deepinfra.com/v1/openai",
defaultBaseUrl: "https://api.deepinfra.com/v1/openai",
allowPrivateNetwork: false,
defaultHeaders: {
Authorization: "Bearer sk-deepinfra",
"Content-Type": "application/json",
},
provider: "deepinfra",
capability: "audio",
transport: "http",
},
],
]);
expect(postJsonRequestMock).toHaveBeenCalledOnce();
const postRequest = requireFirstPostJsonRequest();
const postRequestHeaders = Reflect.get(postRequest ?? {}, "headers");
expect(postRequestHeaders).toBeInstanceOf(Headers);
expect(Object.fromEntries((postRequestHeaders as Headers).entries())).toEqual({
authorization: "Bearer sk-deepinfra",
"content-type": "application/json",
});
expect(postRequest).toEqual({
url: "https://api.deepinfra.com/v1/openai/audio/speech",
headers: postRequestHeaders,
timeoutMs: 12_345,
body: {
model: "hexgrad/Kokoro-82M",
input: "hello",
voice: "af_bella",
response_format: "mp3",
speed: 1.2,
},
fetchFn: fetch,
allowPrivateNetwork: false,
dispatcherPolicy: undefined,
});
expect(result.audioBuffer).toEqual(Buffer.from([1, 2, 3]));
expect(result.outputFormat).toBe("mp3");
expect(result.fileExtension).toBe(".mp3");
expect(result.voiceCompatible).toBe(true);
expect(release).toHaveBeenCalledOnce();
});
it("uses DEEPINFRA_API_KEY when provider config omits apiKey", () => {
vi.stubEnv("DEEPINFRA_API_KEY", "sk-env");
const provider = buildDeepInfraSpeechProvider();
expect(
provider.isConfigured({
cfg: {} as never,
providerConfig: {},
timeoutMs: 30_000,
}),
).toBe(true);
});
});

View File

@@ -0,0 +1,50 @@
// Deepinfra provider module implements model/runtime integration.
import {
asObject,
createOpenAiCompatibleSpeechProvider,
type SpeechProviderPlugin,
} from "openclaw/plugin-sdk/speech";
import {
DEEPINFRA_BASE_URL,
DEEPINFRA_TTS_FALLBACK_MODELS,
DEFAULT_DEEPINFRA_TTS_VOICE,
normalizeDeepInfraModelRef,
} from "./media-models.js";
import type { DeepInfraSurfaceModel } from "./provider-models.js";
const DEEPINFRA_TTS_RESPONSE_FORMATS = ["mp3", "opus", "flac", "wav", "pcm"] as const;
type DeepInfraTtsExtraConfig = {
extraBody?: Record<string, unknown>;
};
// First entry of ttsModels is the default; rest fill the allowlist.
export function buildDeepInfraSpeechProvider(options?: {
ttsModels?: readonly DeepInfraSurfaceModel[];
}): SpeechProviderPlugin {
const ids =
options?.ttsModels && options.ttsModels.length > 0
? options.ttsModels.map((model) => model.id)
: [...DEEPINFRA_TTS_FALLBACK_MODELS];
const defaultModel = ids[0] ?? DEEPINFRA_TTS_FALLBACK_MODELS[0];
return createOpenAiCompatibleSpeechProvider<DeepInfraTtsExtraConfig>({
id: "deepinfra",
label: "DeepInfra",
autoSelectOrder: 45,
models: ids,
voices: [DEFAULT_DEEPINFRA_TTS_VOICE],
defaultModel,
defaultVoice: DEFAULT_DEEPINFRA_TTS_VOICE,
defaultBaseUrl: DEEPINFRA_BASE_URL,
envKey: "DEEPINFRA_API_KEY",
responseFormats: DEEPINFRA_TTS_RESPONSE_FORMATS,
defaultResponseFormat: "mp3",
voiceCompatibleResponseFormats: ["mp3", "opus"],
baseUrlPolicy: { kind: "trim-trailing-slash" },
normalizeModel: normalizeDeepInfraModelRef,
apiErrorLabel: "DeepInfra TTS API error",
missingApiKeyError: "DeepInfra API key missing",
readExtraConfig: (raw) => ({ extraBody: asObject(raw?.extraBody) }),
extraJsonBodyFields: [{ configKey: "extraBody", requestKey: "extra_body" }],
});
}

View File

@@ -0,0 +1,273 @@
// Deepinfra tests cover surface model catalogs plugin behavior.
import { beforeEach, describe, expect, it, vi } from "vitest";
import { resetDeepInfraModelCacheForTest } from "./provider-models.js";
import {
listDeepInfraImageGenCatalog,
listDeepInfraVideoGenCatalog,
resolveDeepInfraVideoModelCapabilities,
} from "./surface-model-catalogs.js";
beforeEach(() => {
resetDeepInfraModelCacheForTest();
});
function makeCtx(overrides: Partial<Parameters<typeof listDeepInfraImageGenCatalog>[0]> = {}) {
return {
config: {},
env: { ...process.env },
resolveProviderApiKey: (_id?: string) => ({
apiKey: undefined,
discoveryApiKey: undefined,
}),
resolveProviderAuth: () => ({
apiKey: undefined,
mode: "none" as const,
source: "none" as const,
}),
...overrides,
} as Parameters<typeof listDeepInfraImageGenCatalog>[0];
}
function withKeyCtx(): Parameters<typeof listDeepInfraImageGenCatalog>[0] {
return makeCtx({
resolveProviderApiKey: () => ({
apiKey: "sk-test",
discoveryApiKey: "sk-test",
}),
});
}
const surfaceEntry = (id: string, surfaceTag: string, extra: Record<string, unknown> = {}) => ({
id,
object: "model" as const,
owned_by: "deepinfra",
metadata: {
description: id,
tags: [surfaceTag],
pricing: {},
...extra,
},
});
function jsonResponse(payload: unknown, init: ResponseInit = {}): Response {
return new Response(JSON.stringify(payload), {
status: 200,
headers: { "Content-Type": "application/json" },
...init,
});
}
async function withLiveFetch(mockFetch: ReturnType<typeof vi.fn>, run: () => Promise<void>) {
const env = { ...process.env };
delete process.env.NODE_ENV;
delete process.env.VITEST;
process.env.DEEPINFRA_API_KEY = "sk-test";
vi.stubGlobal("fetch", mockFetch);
try {
await run();
} finally {
if (env.NODE_ENV !== undefined) {
process.env.NODE_ENV = env.NODE_ENV;
} else {
delete process.env.NODE_ENV;
}
if (env.VITEST !== undefined) {
process.env.VITEST = env.VITEST;
} else {
delete process.env.VITEST;
}
if (env.DEEPINFRA_API_KEY !== undefined) {
process.env.DEEPINFRA_API_KEY = env.DEEPINFRA_API_KEY;
} else {
delete process.env.DEEPINFRA_API_KEY;
}
vi.unstubAllGlobals();
}
}
describe("DeepInfra generation catalogs", () => {
it("return null when no discoveryApiKey is configured", async () => {
await expect(listDeepInfraImageGenCatalog(makeCtx())).resolves.toBeNull();
await expect(listDeepInfraVideoGenCatalog(makeCtx())).resolves.toBeNull();
});
});
describe("listDeepInfraImageGenCatalog", () => {
it("returns null when live discovery succeeds but the response has zero image-gen entries", async () => {
const mockFetch = vi.fn().mockResolvedValue(
jsonResponse({
data: [
surfaceEntry("anthropic/claude-sonnet-4-6", "chat", {
context_length: 200000,
max_tokens: 8192,
pricing: { input_tokens: 3, output_tokens: 15 },
}),
],
}),
);
await withLiveFetch(mockFetch, async () => {
const result = await listDeepInfraImageGenCatalog(withKeyCtx());
expect(result).toBeNull();
});
});
it("returns null under VITEST even with a key (static fallback owns offline)", async () => {
// The default VITEST env path makes discoverDeepInfraSurfaces emit the
// manifest fallback (live=false), and the catalog provider rejects
// non-live results so it cannot serve stale offline data as "live".
const result = await listDeepInfraImageGenCatalog(withKeyCtx());
expect(result).toBeNull();
});
it("projects discovered image-gen entries when a key is configured and discovery is live", async () => {
const mockFetch = vi.fn().mockResolvedValue(
jsonResponse({
data: [
surfaceEntry("black-forest-labs/FLUX-2-pro", "image-gen", {
pricing: { per_image_unit: 0.08 },
default_width: 1024,
default_height: 1024,
default_iterations: 28,
}),
surfaceEntry("ByteDance/Seedream-4", "image-gen", {
pricing: { per_image_unit: 0.03 },
}),
surfaceEntry("anthropic/claude-sonnet-4-6", "chat", {
context_length: 200000,
max_tokens: 8192,
pricing: { input_tokens: 3, output_tokens: 15 },
}),
],
}),
);
await withLiveFetch(mockFetch, async () => {
const result = await listDeepInfraImageGenCatalog(withKeyCtx());
expect(result).not.toBeNull();
expect(result?.map((e) => e.model)).toEqual([
"black-forest-labs/FLUX-2-pro",
"ByteDance/Seedream-4",
]);
for (const entry of result ?? []) {
expect(entry.kind).toBe("image_generation");
expect(entry.provider).toBe("deepinfra");
expect(entry.source).toBe("live");
}
});
});
});
describe("listDeepInfraVideoGenCatalog", () => {
it("returns null when live discovery succeeds but the response has zero video-gen entries", async () => {
// Current production state: TTS/STT/T2V models lack the OPENAI tag the
// backend filter requires, so a key-authenticated discovery still
// produces zero video-gen entries. We must return null so the registered
// provider's static fallback list is consulted instead of an empty
// "live" answer.
const mockFetch = vi.fn().mockResolvedValue(
jsonResponse({
data: [
surfaceEntry("anthropic/claude-sonnet-4-6", "chat", {
context_length: 200000,
max_tokens: 8192,
pricing: { input_tokens: 3, output_tokens: 15 },
}),
surfaceEntry("black-forest-labs/FLUX-2-pro", "image-gen", {
pricing: { per_image_unit: 0.08 },
}),
],
}),
);
await withLiveFetch(mockFetch, async () => {
const result = await listDeepInfraVideoGenCatalog(withKeyCtx());
expect(result).toBeNull();
});
});
it("projects discovered video-gen entries with capability shape", async () => {
const mockFetch = vi.fn().mockResolvedValue(
jsonResponse({
data: [
surfaceEntry("Wan-AI/Wan2.6-T2V", "video-gen", {
pricing: { output_seconds: 0.05 },
}),
surfaceEntry("ByteDance/Seedance-2.0", "video-gen", {
pricing: { output_seconds: 0.08 },
}),
],
}),
);
await withLiveFetch(mockFetch, async () => {
const result = await listDeepInfraVideoGenCatalog(withKeyCtx());
expect(result).not.toBeNull();
expect(result?.map((e) => e.model)).toEqual(["Wan-AI/Wan2.6-T2V", "ByteDance/Seedance-2.0"]);
const first = result?.[0];
expect(first?.kind).toBe("video_generation");
expect(first?.capabilities?.generate?.supportsAspectRatio).toBe(true);
expect(first?.capabilities?.generate?.supportedDurationSeconds).toEqual([5, 8]);
});
});
});
describe("resolveDeepInfraVideoModelCapabilities", () => {
it("returns capabilities for a discovered video-gen model", async () => {
const mockFetch = vi.fn().mockResolvedValue(
jsonResponse({
data: [
surfaceEntry("Wan-AI/Wan2.6-T2V", "video-gen", {
pricing: { output_seconds: 0.05 },
}),
],
}),
);
await withLiveFetch(mockFetch, async () => {
const caps = await resolveDeepInfraVideoModelCapabilities({
model: "Wan-AI/Wan2.6-T2V",
} as Parameters<typeof resolveDeepInfraVideoModelCapabilities>[0]);
expect(caps).toBeDefined();
expect(caps?.generate?.supportsAspectRatio).toBe(true);
});
});
it("strips the deepinfra/ prefix when matching", async () => {
const mockFetch = vi.fn().mockResolvedValue(
jsonResponse({
data: [
surfaceEntry("Wan-AI/Wan2.6-T2V", "video-gen", {
pricing: { output_seconds: 0.05 },
}),
],
}),
);
await withLiveFetch(mockFetch, async () => {
const caps = await resolveDeepInfraVideoModelCapabilities({
model: "deepinfra/Wan-AI/Wan2.6-T2V",
} as Parameters<typeof resolveDeepInfraVideoModelCapabilities>[0]);
expect(caps).toBeDefined();
});
});
it("returns undefined for an unknown model", async () => {
const mockFetch = vi.fn().mockResolvedValue(
jsonResponse({
data: [
surfaceEntry("Wan-AI/Wan2.6-T2V", "video-gen", {
pricing: { output_seconds: 0.05 },
}),
],
}),
);
await withLiveFetch(mockFetch, async () => {
const caps = await resolveDeepInfraVideoModelCapabilities({
model: "ByteDance/Seedance-2.0",
} as Parameters<typeof resolveDeepInfraVideoModelCapabilities>[0]);
expect(caps).toBeUndefined();
});
});
});

View File

@@ -0,0 +1,115 @@
// Deepinfra plugin module implements surface model catalogs behavior.
import type {
UnifiedModelCatalogEntry,
UnifiedModelCatalogProviderContext,
} from "openclaw/plugin-sdk/plugin-entry";
import type {
VideoGenerationModelCapabilitiesContext,
VideoGenerationProviderCapabilities,
} from "openclaw/plugin-sdk/video-generation";
import { DEEPINFRA_VIDEO_ASPECT_RATIOS, DEEPINFRA_VIDEO_DURATIONS } from "./media-models.js";
import { discoverDeepInfraSurfaces, type DeepInfraSurfaceModel } from "./provider-models.js";
const PROVIDER_ID = "deepinfra";
// Live catalog providers (registered via api.registerModelCatalogProvider).
// Mirrors extensions/openrouter/video-model-catalog.ts: auth-gated (returns
// null without a key so the static fallback wins), and reuses the cached
// discoverDeepInfraSurfaces call so chat/image-gen/video-gen share one fetch.
function surfaceModelToImageGenEntry(model: DeepInfraSurfaceModel): UnifiedModelCatalogEntry {
return {
kind: "image_generation",
provider: PROVIDER_ID,
model: model.id,
source: "live",
...(model.name ? { label: model.name } : {}),
};
}
function surfaceModelToVideoGenEntry(
model: DeepInfraSurfaceModel,
): UnifiedModelCatalogEntry<VideoGenerationProviderCapabilities> {
return {
kind: "video_generation",
provider: PROVIDER_ID,
model: model.id,
source: "live",
...(model.name ? { label: model.name } : {}),
capabilities: buildDeepInfraVideoModelCapabilities(),
};
}
// Canonical DeepInfra-wide video-gen shape. Wire per-model hints
// (metadata.supported_durations etc.) in here once the backend emits them.
function buildDeepInfraVideoModelCapabilities(): VideoGenerationProviderCapabilities {
return {
providerOptions: {
seed: "number",
negative_prompt: "string",
negativePrompt: "string",
style: "string",
guidance_scale: "number",
guidanceScale: "number",
},
generate: {
maxVideos: 1,
maxDurationSeconds: 8,
supportedDurationSeconds: [...DEEPINFRA_VIDEO_DURATIONS],
supportsAspectRatio: true,
aspectRatios: [...DEEPINFRA_VIDEO_ASPECT_RATIOS],
},
imageToVideo: { enabled: false },
videoToVideo: { enabled: false },
};
}
export async function listDeepInfraImageGenCatalog(
ctx: UnifiedModelCatalogProviderContext,
): Promise<readonly UnifiedModelCatalogEntry[] | null> {
const { discoveryApiKey } = ctx.resolveProviderApiKey(PROVIDER_ID);
if (!discoveryApiKey) {
return null;
}
const catalog = await discoverDeepInfraSurfaces({ hasApiKey: true, env: ctx.env });
// Bail on non-live (static fallback owns offline) and on empty surface
// (returning [] would starve the unified catalog instead of falling back).
if (!catalog.live || catalog.imageGen.length === 0) {
return null;
}
return catalog.imageGen.map(surfaceModelToImageGenEntry);
}
export async function listDeepInfraVideoGenCatalog(
ctx: UnifiedModelCatalogProviderContext,
): Promise<readonly UnifiedModelCatalogEntry<VideoGenerationProviderCapabilities>[] | null> {
const { discoveryApiKey } = ctx.resolveProviderApiKey(PROVIDER_ID);
if (!discoveryApiKey) {
return null;
}
const catalog = await discoverDeepInfraSurfaces({ hasApiKey: true, env: ctx.env });
if (!catalog.live || catalog.videoGen.length === 0) {
return null;
}
return catalog.videoGen.map(surfaceModelToVideoGenEntry);
}
// VideoGenerationProvider.resolveModelCapabilities hook. Returns the
// capability shape per-request when the model is live; provider static caps
// are the fallback.
export async function resolveDeepInfraVideoModelCapabilities(
ctx: VideoGenerationModelCapabilitiesContext,
): Promise<VideoGenerationProviderCapabilities | undefined> {
// Model id may arrive bare or `deepinfra/`-prefixed.
const rawId = typeof ctx.model === "string" ? ctx.model : "";
const normalized = rawId.startsWith(`${PROVIDER_ID}/`)
? rawId.slice(PROVIDER_ID.length + 1)
: rawId;
const catalog = await discoverDeepInfraSurfaces({
env: process.env,
});
const entry =
catalog.videoGen.find((m) => m.id === normalized) ??
catalog.videoGen.find((m) => m.id === rawId);
return entry ? buildDeepInfraVideoModelCapabilities() : undefined;
}

View File

@@ -0,0 +1,16 @@
{
"extends": "../tsconfig.package-boundary.base.json",
"compilerOptions": {
"rootDir": "."
},
"include": ["./*.ts", "./src/**/*.ts"],
"exclude": [
"./**/*.test.ts",
"./dist/**",
"./node_modules/**",
"./src/test-support/**",
"./src/**/*test-helpers.ts",
"./src/**/*test-harness.ts",
"./src/**/*test-support.ts"
]
}

View File

@@ -0,0 +1,296 @@
// Deepinfra tests cover video generation provider plugin behavior.
import {
getProviderHttpMocks,
installProviderHttpMockCleanup,
} from "openclaw/plugin-sdk/provider-http-test-mocks";
import { expectExplicitVideoGenerationCapabilities } from "openclaw/plugin-sdk/provider-test-contracts";
import { beforeAll, describe, expect, it, vi } from "vitest";
const { postJsonRequestMock, resolveProviderHttpRequestConfigMock } = getProviderHttpMocks();
let buildDeepInfraVideoGenerationProvider: typeof import("./video-generation-provider.js").buildDeepInfraVideoGenerationProvider;
beforeAll(async () => {
({ buildDeepInfraVideoGenerationProvider } = await import("./video-generation-provider.js"));
});
installProviderHttpMockCleanup();
function requireFirstPostJsonRequest(): unknown {
const [call] = postJsonRequestMock.mock.calls;
if (!call) {
throw new Error("expected DeepInfra video request");
}
return call[0];
}
describe("deepinfra video generation provider", () => {
it("declares explicit mode capabilities", () => {
expectExplicitVideoGenerationCapabilities(buildDeepInfraVideoGenerationProvider());
});
it("uses the current DeepInfra text-to-video fallback model first", () => {
const provider = buildDeepInfraVideoGenerationProvider();
expect(provider.defaultModel).toBe("Pixverse/Pixverse-T2V");
expect(provider.models?.slice(0, 3)).toEqual([
"Pixverse/Pixverse-T2V",
"Pixverse/Pixverse-T2V-HD",
"Wan-AI/Wan2.6-T2V",
]);
});
it("creates native text-to-video requests and returns the hosted output URL", async () => {
const release = vi.fn(async () => {});
postJsonRequestMock.mockResolvedValue({
response: {
json: async () => ({
video_url: "/generated/video.mp4",
request_id: "req_123",
seed: 42,
inference_status: { status: "succeeded" },
}),
},
release,
});
const provider = buildDeepInfraVideoGenerationProvider();
const result = await provider.generateVideo({
provider: "deepinfra",
model: "deepinfra/Pixverse/Pixverse-T2V",
prompt: "A bicycle weaving through a rainy neon street",
cfg: {},
aspectRatio: "16:9",
durationSeconds: 8,
providerOptions: {
seed: 42,
negative_prompt: "blur",
style: "anime",
},
});
expect(resolveProviderHttpRequestConfigMock.mock.calls).toEqual([
[
{
baseUrl: "https://api.deepinfra.com/v1/inference",
defaultBaseUrl: "https://api.deepinfra.com/v1/inference",
allowPrivateNetwork: false,
defaultHeaders: {
Authorization: "Bearer provider-key",
"Content-Type": "application/json",
},
provider: "deepinfra",
capability: "video",
transport: "http",
},
],
]);
expect(postJsonRequestMock).toHaveBeenCalledOnce();
const postRequest = requireFirstPostJsonRequest();
const postRequestHeaders = Reflect.get(postRequest ?? {}, "headers");
expect(postRequestHeaders).toBeInstanceOf(Headers);
expect(Object.fromEntries((postRequestHeaders as Headers).entries())).toEqual({
authorization: "Bearer provider-key",
"content-type": "application/json",
});
expect(postRequest).toEqual({
url: "https://api.deepinfra.com/v1/inference/Pixverse/Pixverse-T2V",
headers: postRequestHeaders,
body: {
prompt: "A bicycle weaving through a rainy neon street",
aspect_ratio: "16:9",
duration: 8,
seed: 42,
negative_prompt: "blur",
style: "anime",
},
timeoutMs: undefined,
fetchFn: fetch,
allowPrivateNetwork: false,
dispatcherPolicy: undefined,
});
expect(result.videos).toEqual([
{
url: "https://api.deepinfra.com/generated/video.mp4",
mimeType: "video/mp4",
fileName: "video-1.mp4",
},
]);
expect(result.metadata).toEqual({
requestId: "req_123",
seed: 42,
status: "succeeded",
});
expect(release).toHaveBeenCalledOnce();
});
it("does not forward malformed video seed values", async () => {
postJsonRequestMock.mockResolvedValue({
response: {
json: async () => ({
video_url: "/generated/video.mp4",
request_id: "req_seed",
inference_status: { status: "succeeded" },
}),
},
release: vi.fn(async () => {}),
});
const provider = buildDeepInfraVideoGenerationProvider();
await provider.generateVideo({
provider: "deepinfra",
model: "deepinfra/Pixverse/Pixverse-T2V",
prompt: "A bicycle weaving through a rainy neon street",
cfg: {},
providerOptions: {
seed: 1.5,
},
});
expect(postJsonRequestMock).toHaveBeenCalledOnce();
const postRequest = requireFirstPostJsonRequest();
expect(Reflect.get(Reflect.get(postRequest ?? {}, "body") ?? {}, "seed")).toBeUndefined();
});
it("drops malformed response seed metadata", async () => {
postJsonRequestMock.mockResolvedValue({
response: {
json: async () => ({
video_url: "/generated/video.mp4",
request_id: "req_bad_seed",
seed: 1.5,
inference_status: { status: "succeeded" },
}),
},
release: vi.fn(async () => {}),
});
const provider = buildDeepInfraVideoGenerationProvider();
const result = await provider.generateVideo({
provider: "deepinfra",
model: "deepinfra/Pixverse/Pixverse-T2V",
prompt: "A bicycle weaving through a rainy neon street",
cfg: {},
});
expect(result.metadata).toEqual({
requestId: "req_bad_seed",
seed: undefined,
status: "succeeded",
});
});
it("reports malformed native video JSON as a provider error", async () => {
const release = vi.fn(async () => {});
postJsonRequestMock.mockResolvedValue({
response: {
json: async () => {
throw new SyntaxError("Unexpected token");
},
},
release,
});
const provider = buildDeepInfraVideoGenerationProvider();
await expect(
provider.generateVideo({
provider: "deepinfra",
model: "deepinfra/Pixverse/Pixverse-T2V",
prompt: "A bicycle weaving through a rainy neon street",
cfg: {},
}),
).rejects.toThrow("DeepInfra video generation failed: malformed JSON response");
expect(release).toHaveBeenCalledOnce();
});
it("names base64 WebM data URL outputs from the MIME type", async () => {
postJsonRequestMock.mockResolvedValue({
response: {
json: async () => ({
video_url: `data:video/webm;base64,${Buffer.from("webm-data").toString("base64")}`,
request_id: "req_webm",
inference_status: { status: "succeeded" },
}),
},
release: vi.fn(async () => {}),
});
const provider = buildDeepInfraVideoGenerationProvider();
const result = await provider.generateVideo({
provider: "deepinfra",
model: "deepinfra/Pixverse/Pixverse-T2V",
prompt: "A WebM data URL",
cfg: {},
});
expect(result.videos).toHaveLength(1);
const [video] = result.videos;
if (!video) {
throw new Error("Expected generated DeepInfra video");
}
expect(video).toEqual({
buffer: Buffer.from("webm-data"),
mimeType: "video/webm",
fileName: "video-1.webm",
});
});
it("accepts DeepInfra video array responses", async () => {
postJsonRequestMock.mockResolvedValue({
response: {
json: async () => ({
videos: [{ url: "/generated/video-array.mp4" }],
request_id: "req_array",
status: "succeeded",
}),
},
release: vi.fn(async () => {}),
});
const provider = buildDeepInfraVideoGenerationProvider();
const result = await provider.generateVideo({
provider: "deepinfra",
model: "deepinfra/google/veo-3.1-fast",
prompt: "A videos array response",
cfg: {},
});
expect(result.videos).toEqual([
{
url: "https://api.deepinfra.com/generated/video-array.mp4",
mimeType: "video/mp4",
fileName: "video-1.mp4",
},
]);
expect(result.metadata).toEqual({
requestId: "req_array",
seed: undefined,
status: "succeeded",
});
});
it("rejects malformed base64 data URL video outputs", async () => {
const release = vi.fn(async () => undefined);
postJsonRequestMock.mockResolvedValue({
response: {
json: async () => ({
video_url: "data:video/webm;base64,not-base64!",
request_id: "req_bad_base64",
inference_status: { status: "succeeded" },
}),
},
release,
});
const provider = buildDeepInfraVideoGenerationProvider();
await expect(
provider.generateVideo({
provider: "deepinfra",
model: "deepinfra/Pixverse/Pixverse-T2V",
prompt: "A malformed WebM data URL",
cfg: {},
}),
).rejects.toThrow("DeepInfra video response returned malformed data URL base64");
expect(release).toHaveBeenCalledOnce();
});
});

View File

@@ -0,0 +1,297 @@
// Deepinfra provider module implements model/runtime integration.
import { extensionForMime } from "openclaw/plugin-sdk/media-mime";
import { canonicalizeBase64 } from "openclaw/plugin-sdk/media-runtime";
import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth";
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
import {
assertOkOrThrowHttpError,
postJsonRequest,
readProviderJsonResponse,
resolveProviderHttpRequestConfig,
} from "openclaw/plugin-sdk/provider-http";
import {
asFiniteNumber,
asSafeIntegerInRange,
normalizeOptionalString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import type {
GeneratedVideoAsset,
VideoGenerationProvider,
VideoGenerationRequest,
} from "openclaw/plugin-sdk/video-generation";
import {
DEEPINFRA_NATIVE_BASE_URL,
DEEPINFRA_VIDEO_ASPECT_RATIOS,
DEEPINFRA_VIDEO_DURATIONS,
DEEPINFRA_VIDEO_FALLBACK_MODELS,
normalizeDeepInfraBaseUrl,
normalizeDeepInfraModelRef,
} from "./media-models.js";
import type { DeepInfraSurfaceModel } from "./provider-models.js";
import { resolveDeepInfraVideoModelCapabilities } from "./surface-model-catalogs.js";
type DeepInfraVideoStatus = {
status?: string;
runtime_ms?: number;
};
type DeepInfraVideoResponse = {
video_url?: string;
video?: string;
videos?: Array<string | { url?: string; video_url?: string }>;
status?: string;
seed?: number;
request_id?: string;
inference_status?: DeepInfraVideoStatus;
};
function encodeDeepInfraModelPath(model: string): string {
return model.split("/").map(encodeURIComponent).join("/");
}
function resolveDeepInfraNativeBaseUrl(req: VideoGenerationRequest): string {
const providerConfig = req.cfg?.models?.providers?.deepinfra as
| (Record<string, unknown> & { baseUrl?: unknown })
| undefined;
const nativeBaseUrl = normalizeOptionalString(providerConfig?.nativeBaseUrl);
if (nativeBaseUrl) {
return normalizeDeepInfraBaseUrl(nativeBaseUrl, DEEPINFRA_NATIVE_BASE_URL);
}
const configuredBaseUrl = normalizeOptionalString(providerConfig?.baseUrl);
if (configuredBaseUrl?.includes("/v1/inference")) {
return normalizeDeepInfraBaseUrl(configuredBaseUrl, DEEPINFRA_NATIVE_BASE_URL);
}
return DEEPINFRA_NATIVE_BASE_URL;
}
function normalizeDeepInfraVideoUrl(url: string): string {
if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("data:")) {
return url;
}
return new URL(url, "https://api.deepinfra.com").href;
}
function parseVideoDataUrl(url: string): GeneratedVideoAsset | undefined {
const match = /^data:([^;,]+);base64,(.+)$/u.exec(url);
if (!match) {
return undefined;
}
const mimeType = match[1] ?? "video/mp4";
const ext = extensionForMime(mimeType)?.slice(1) ?? "mp4";
const canonicalBase64 = canonicalizeBase64(match[2] ?? "");
if (!canonicalBase64) {
throw new Error("DeepInfra video response returned malformed data URL base64");
}
return {
buffer: Buffer.from(canonicalBase64, "base64"),
mimeType,
fileName: `video-1.${ext}`,
};
}
function resolveDurationSeconds(value: number | undefined): number | undefined {
if (typeof value !== "number" || !Number.isFinite(value)) {
return undefined;
}
return value <= 6.5 ? 5 : 8;
}
function resolveSeed(value: unknown): number | undefined {
return asSafeIntegerInRange(value, { min: 0, max: 4_294_967_295 });
}
function buildDeepInfraVideoBody(
req: VideoGenerationRequest,
model: string,
): Record<string, unknown> {
const options = req.providerOptions ?? {};
const body: Record<string, unknown> = {
prompt: req.prompt,
};
const aspectRatio = normalizeOptionalString(req.aspectRatio);
if (aspectRatio) {
body.aspect_ratio = aspectRatio;
}
const duration = resolveDurationSeconds(req.durationSeconds);
if (duration) {
body.duration = duration;
}
const seed = resolveSeed(options.seed);
if (seed != null) {
body.seed = seed;
}
const negativePrompt =
normalizeOptionalString(options.negative_prompt) ??
normalizeOptionalString(options.negativePrompt);
if (negativePrompt) {
body.negative_prompt = negativePrompt;
}
const style = normalizeOptionalString(options.style);
if (style) {
body.style = style;
}
const guidanceScale =
asFiniteNumber(options.guidance_scale) ?? asFiniteNumber(options.guidanceScale);
if (guidanceScale != null && model.startsWith("Wan-AI/")) {
body.guidance_scale = guidanceScale;
}
return body;
}
function firstDeepInfraVideoUrl(payload: DeepInfraVideoResponse): string | undefined {
const direct =
normalizeOptionalString(payload.video_url) ?? normalizeOptionalString(payload.video);
if (direct) {
return direct;
}
for (const entry of payload.videos ?? []) {
const videoUrl =
typeof entry === "string"
? normalizeOptionalString(entry)
: (normalizeOptionalString(entry.url) ?? normalizeOptionalString(entry.video_url));
if (videoUrl) {
return videoUrl;
}
}
return undefined;
}
function extractDeepInfraVideoAsset(payload: DeepInfraVideoResponse): GeneratedVideoAsset {
const videoUrl = firstDeepInfraVideoUrl(payload);
if (!videoUrl) {
throw new Error("DeepInfra video response missing video URL");
}
const normalizedUrl = normalizeDeepInfraVideoUrl(videoUrl);
const dataAsset = parseVideoDataUrl(normalizedUrl);
if (dataAsset) {
return dataAsset;
}
return {
url: normalizedUrl,
mimeType: "video/mp4",
fileName: "video-1.mp4",
};
}
function failureMessage(payload: DeepInfraVideoResponse): string | undefined {
const status = (
normalizeOptionalString(payload.inference_status?.status) ??
normalizeOptionalString(payload.status)
)?.toLowerCase();
if (status === "failed" || status === "error") {
return "DeepInfra video generation failed";
}
return undefined;
}
// First entry of videoGenModels is the default; rest fill the allowlist.
export function buildDeepInfraVideoGenerationProvider(options?: {
videoGenModels?: readonly DeepInfraSurfaceModel[];
}): VideoGenerationProvider {
const ids =
options?.videoGenModels && options.videoGenModels.length > 0
? options.videoGenModels.map((model) => model.id)
: [...DEEPINFRA_VIDEO_FALLBACK_MODELS];
const defaultModel = ids[0] ?? DEEPINFRA_VIDEO_FALLBACK_MODELS[0];
return {
id: "deepinfra",
label: "DeepInfra",
defaultModel,
models: ids,
resolveModelCapabilities: resolveDeepInfraVideoModelCapabilities,
isConfigured: ({ agentDir }) =>
isProviderApiKeyConfigured({
provider: "deepinfra",
agentDir,
}),
capabilities: {
generate: {
maxVideos: 1,
maxDurationSeconds: 8,
supportedDurationSeconds: [...DEEPINFRA_VIDEO_DURATIONS],
supportsAspectRatio: true,
aspectRatios: [...DEEPINFRA_VIDEO_ASPECT_RATIOS],
providerOptions: {
seed: "number",
negative_prompt: "string",
negativePrompt: "string",
style: "string",
guidance_scale: "number",
guidanceScale: "number",
},
},
imageToVideo: {
enabled: false,
},
videoToVideo: {
enabled: false,
},
},
async generateVideo(req) {
if ((req.inputImages?.length ?? 0) > 0) {
throw new Error("DeepInfra video generation currently supports text-to-video only.");
}
if ((req.inputVideos?.length ?? 0) > 0) {
throw new Error("DeepInfra video generation does not support video reference inputs.");
}
const auth = await resolveApiKeyForProvider({
provider: "deepinfra",
cfg: req.cfg,
agentDir: req.agentDir,
store: req.authStore,
});
if (!auth.apiKey) {
throw new Error("DeepInfra API key missing");
}
const model = normalizeDeepInfraModelRef(req.model, defaultModel);
const resolvedBaseUrl = resolveDeepInfraNativeBaseUrl(req);
const { baseUrl, allowPrivateNetwork, headers, dispatcherPolicy } =
resolveProviderHttpRequestConfig({
baseUrl: resolvedBaseUrl,
defaultBaseUrl: DEEPINFRA_NATIVE_BASE_URL,
allowPrivateNetwork: false,
defaultHeaders: {
Authorization: `Bearer ${auth.apiKey}`,
"Content-Type": "application/json",
},
provider: "deepinfra",
capability: "video",
transport: "http",
});
const { response, release } = await postJsonRequest({
url: `${baseUrl}/${encodeDeepInfraModelPath(model)}`,
headers,
body: buildDeepInfraVideoBody(req, model),
timeoutMs: req.timeoutMs,
fetchFn: fetch,
allowPrivateNetwork,
dispatcherPolicy,
});
try {
await assertOkOrThrowHttpError(response, "DeepInfra video generation failed");
const payload = await readProviderJsonResponse<DeepInfraVideoResponse>(
response,
"DeepInfra video generation failed",
);
const failed = failureMessage(payload);
if (failed) {
throw new Error(failed);
}
const video = extractDeepInfraVideoAsset(payload);
return {
videos: [video],
model,
metadata: {
requestId: normalizeOptionalString(payload.request_id),
seed: resolveSeed(payload.seed),
status: payload.inference_status?.status ?? payload.status,
},
};
} finally {
await release();
}
},
};
}