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
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:
522
extensions/deepinfra/provider-models.test.ts
Normal file
522
extensions/deepinfra/provider-models.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user