feat: add manifest provider catalog helper

This commit is contained in:
Shakker
2026-04-28 03:33:15 +01:00
parent 1267a14326
commit 5cba55e520
2 changed files with 157 additions and 0 deletions

View File

@@ -1,6 +1,8 @@
import { describe, expect, it } from "vitest";
import type { ModelCatalogProvider } from "../model-catalog/types.js";
import {
applyProviderNativeStreamingUsageCompat,
buildManifestModelProviderConfig,
readConfiguredProviderCatalogEntries,
supportsNativeStreamingUsageCompat,
} from "./provider-catalog-shared.js";
@@ -95,3 +97,89 @@ describe("provider-catalog-shared configured catalog entries", () => {
]);
});
});
describe("provider-catalog-shared manifest provider configs", () => {
it("converts manifest model catalog rows into provider config rows", () => {
const catalog: ModelCatalogProvider = {
baseUrl: "https://api.example.test/v1",
api: "openai-completions",
headers: { "x-provider": "example" },
models: [
{
id: "example-model",
name: "Example Model",
input: ["text", "image"],
reasoning: true,
contextWindow: 128_000,
contextTokens: 64_000,
maxTokens: 8192,
cost: {
input: 1,
output: 2,
cacheRead: 0.25,
cacheWrite: 0.5,
tieredPricing: [
{
input: 0.5,
output: 1,
cacheRead: 0.1,
cacheWrite: 0.2,
range: [0, 1_000_000],
},
],
},
compat: { supportsUsageInStreaming: true },
},
],
};
expect(buildManifestModelProviderConfig({ providerId: "example", catalog })).toEqual({
baseUrl: "https://api.example.test/v1",
api: "openai-completions",
headers: { "x-provider": "example" },
models: [
{
id: "example-model",
name: "Example Model",
reasoning: true,
input: ["text", "image"],
cost: {
input: 1,
output: 2,
cacheRead: 0.25,
cacheWrite: 0.5,
tieredPricing: [
{
input: 0.5,
output: 1,
cacheRead: 0.1,
cacheWrite: 0.2,
range: [0, 1_000_000],
},
],
},
contextWindow: 128_000,
contextTokens: 64_000,
maxTokens: 8192,
compat: { supportsUsageInStreaming: true },
},
],
});
});
it("rejects incomplete manifest rows before building provider runtime config", () => {
expect(() =>
buildManifestModelProviderConfig({
providerId: "example",
catalog: {
models: [
{
id: "missing-context",
maxTokens: 8192,
},
],
},
}),
).toThrow("missing contextWindow");
});
});

View File

@@ -7,6 +7,12 @@ import { resolveProviderRequestCapabilities } from "../agents/provider-attributi
import { findNormalizedProviderKey } from "../agents/provider-id.js";
import type { ModelDefinitionConfig } from "../config/types.models.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type {
ModelCatalogCost,
ModelCatalogModel,
ModelCatalogProvider,
ModelCatalogTieredCost,
} from "../model-catalog/types.js";
import type { ModelProviderConfig } from "./provider-model-shared.js";
export type { ProviderCatalogContext, ProviderCatalogResult } from "../plugins/types.js";
@@ -26,6 +32,69 @@ export type ConfiguredProviderCatalogEntry = {
input?: Array<"text" | "image" | "audio" | "video" | "document">;
};
function cloneManifestCatalogTieredCost(
tier: ModelCatalogTieredCost,
): NonNullable<ModelDefinitionConfig["cost"]["tieredPricing"]>[number] {
return {
input: tier.input,
output: tier.output,
cacheRead: tier.cacheRead,
cacheWrite: tier.cacheWrite,
range: tier.range.length === 1 ? [tier.range[0]] : [tier.range[0], tier.range[1]],
};
}
function cloneManifestCatalogCost(cost: ModelCatalogCost): ModelDefinitionConfig["cost"] {
return {
input: cost.input ?? 0,
output: cost.output ?? 0,
cacheRead: cost.cacheRead ?? 0,
cacheWrite: cost.cacheWrite ?? 0,
...(cost.tieredPricing
? { tieredPricing: cost.tieredPricing.map(cloneManifestCatalogTieredCost) }
: {}),
};
}
function buildManifestCatalogModel(model: ModelCatalogModel): ModelDefinitionConfig {
if (model.contextWindow === undefined) {
throw new Error(`Manifest modelCatalog row ${model.id} is missing contextWindow`);
}
if (model.maxTokens === undefined) {
throw new Error(`Manifest modelCatalog row ${model.id} is missing maxTokens`);
}
const input = model.input?.filter((item) => item !== "document") ?? ["text"];
return {
id: model.id,
name: model.name ?? model.id,
...(model.api ? { api: model.api } : {}),
...(model.baseUrl ? { baseUrl: model.baseUrl } : {}),
reasoning: model.reasoning ?? false,
input: input.length > 0 ? input : ["text"],
cost: cloneManifestCatalogCost(model.cost ?? {}),
contextWindow: model.contextWindow,
...(model.contextTokens !== undefined ? { contextTokens: model.contextTokens } : {}),
maxTokens: model.maxTokens,
...(model.headers ? { headers: { ...model.headers } } : {}),
...(model.compat ? { compat: { ...model.compat } } : {}),
};
}
export function buildManifestModelProviderConfig(params: {
providerId: string;
catalog: ModelCatalogProvider | undefined;
}): ModelProviderConfig {
if (!params.catalog) {
throw new Error(`Missing modelCatalog.providers.${params.providerId}`);
}
return {
baseUrl: params.catalog.baseUrl ?? "",
...(params.catalog.api ? { api: params.catalog.api } : {}),
...(params.catalog.headers ? { headers: { ...params.catalog.headers } } : {}),
models: params.catalog.models.map(buildManifestCatalogModel),
};
}
function normalizeConfiguredCatalogModelInput(
input: unknown,
): ConfiguredProviderCatalogEntry["input"] | undefined {