mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 22:20:48 +00:00
243 lines
7.9 KiB
TypeScript
243 lines
7.9 KiB
TypeScript
// Shared provider catalog helpers for provider plugins.
|
|
//
|
|
// Keep provider-owned exports out of this subpath so plugin loaders can import it
|
|
// without recursing through provider-specific facades.
|
|
|
|
import { resolveProviderRequestCapabilities } from "../agents/provider-attribution.js";
|
|
import { findNormalizedProviderKey } from "../agents/provider-id.js";
|
|
import type { ModelDefinitionConfig } from "../config/types.models.js";
|
|
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
|
import { normalizeModelCatalog } from "../model-catalog/normalize.js";
|
|
import type {
|
|
ModelCatalogCost,
|
|
ModelCatalogModel,
|
|
ModelCatalogTieredCost,
|
|
} from "../model-catalog/types.js";
|
|
import type { ModelProviderConfig } from "./provider-model-shared.js";
|
|
|
|
export type { ProviderCatalogContext, ProviderCatalogResult } from "../plugins/types.js";
|
|
|
|
export {
|
|
buildPairedProviderApiKeyCatalog,
|
|
buildSingleProviderApiKeyCatalog,
|
|
findCatalogTemplate,
|
|
} from "../plugins/provider-catalog.js";
|
|
|
|
export type ConfiguredProviderCatalogEntry = {
|
|
id: string;
|
|
name: string;
|
|
provider: string;
|
|
contextWindow?: number;
|
|
reasoning?: boolean;
|
|
input?: Array<"text" | "image" | "audio" | "video" | "document">;
|
|
};
|
|
|
|
function countRawManifestCatalogModels(catalog: unknown): number | undefined {
|
|
if (!catalog || typeof catalog !== "object") {
|
|
return undefined;
|
|
}
|
|
const models = (catalog as { models?: unknown }).models;
|
|
return Array.isArray(models) ? models.length : undefined;
|
|
}
|
|
|
|
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 buildManifestCatalogModelInput(model: ModelCatalogModel): ModelDefinitionConfig["input"] {
|
|
if (model.input?.includes("document")) {
|
|
throw new Error(
|
|
`Manifest modelCatalog row ${model.id} uses unsupported runtime input document`,
|
|
);
|
|
}
|
|
return model.input?.filter((item): item is "text" | "image" => item !== "document") ?? ["text"];
|
|
}
|
|
|
|
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`);
|
|
}
|
|
return {
|
|
id: model.id,
|
|
name: model.name ?? model.id,
|
|
...(model.api ? { api: model.api } : {}),
|
|
...(model.baseUrl ? { baseUrl: model.baseUrl } : {}),
|
|
reasoning: model.reasoning ?? false,
|
|
input: buildManifestCatalogModelInput(model),
|
|
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: unknown;
|
|
}): ModelProviderConfig {
|
|
const catalog = normalizeModelCatalog(
|
|
{ providers: { [params.providerId]: params.catalog } },
|
|
{ ownedProviders: new Set([params.providerId]) },
|
|
)?.providers?.[params.providerId];
|
|
if (!catalog) {
|
|
throw new Error(`Missing modelCatalog.providers.${params.providerId}`);
|
|
}
|
|
if (!catalog.baseUrl) {
|
|
throw new Error(`Missing modelCatalog.providers.${params.providerId}.baseUrl`);
|
|
}
|
|
const rawModelCount = countRawManifestCatalogModels(params.catalog);
|
|
if (rawModelCount !== undefined && rawModelCount !== catalog.models.length) {
|
|
throw new Error(`Invalid modelCatalog.providers.${params.providerId}.models`);
|
|
}
|
|
return {
|
|
baseUrl: catalog.baseUrl,
|
|
...(catalog.api ? { api: catalog.api } : {}),
|
|
...(catalog.headers ? { headers: { ...catalog.headers } } : {}),
|
|
models: catalog.models.map(buildManifestCatalogModel),
|
|
};
|
|
}
|
|
|
|
function normalizeConfiguredCatalogModelInput(
|
|
input: unknown,
|
|
): ConfiguredProviderCatalogEntry["input"] | undefined {
|
|
if (!Array.isArray(input)) {
|
|
return undefined;
|
|
}
|
|
const normalized = input.filter(
|
|
(item): item is "text" | "image" | "audio" | "video" | "document" =>
|
|
item === "text" ||
|
|
item === "image" ||
|
|
item === "audio" ||
|
|
item === "video" ||
|
|
item === "document",
|
|
);
|
|
return normalized.length > 0 ? normalized : undefined;
|
|
}
|
|
|
|
function resolveConfiguredProviderModels(
|
|
config: OpenClawConfig | undefined,
|
|
providerId: string,
|
|
): ModelDefinitionConfig[] {
|
|
const providers = config?.models?.providers;
|
|
if (!providers || typeof providers !== "object") {
|
|
return [];
|
|
}
|
|
const providerKey = findNormalizedProviderKey(providers, providerId);
|
|
if (!providerKey) {
|
|
return [];
|
|
}
|
|
const providerConfig = providers[providerKey];
|
|
if (!providerConfig || typeof providerConfig !== "object") {
|
|
return [];
|
|
}
|
|
return Array.isArray(providerConfig.models) ? providerConfig.models : [];
|
|
}
|
|
|
|
export function readConfiguredProviderCatalogEntries(params: {
|
|
config?: OpenClawConfig;
|
|
providerId: string;
|
|
publishedProviderId?: string;
|
|
}): ConfiguredProviderCatalogEntry[] {
|
|
const provider = params.publishedProviderId ?? params.providerId;
|
|
const models = resolveConfiguredProviderModels(params.config, params.providerId);
|
|
const entries: ConfiguredProviderCatalogEntry[] = [];
|
|
for (const model of models) {
|
|
if (!model || typeof model !== "object") {
|
|
continue;
|
|
}
|
|
const id = typeof model.id === "string" ? model.id.trim() : "";
|
|
if (!id) {
|
|
continue;
|
|
}
|
|
const name = (typeof model.name === "string" ? model.name : id).trim() || id;
|
|
const contextWindow =
|
|
typeof model.contextWindow === "number" && model.contextWindow > 0
|
|
? model.contextWindow
|
|
: undefined;
|
|
const reasoning = typeof model.reasoning === "boolean" ? model.reasoning : undefined;
|
|
const input = normalizeConfiguredCatalogModelInput(model.input);
|
|
entries.push({
|
|
provider,
|
|
id,
|
|
name,
|
|
...(contextWindow ? { contextWindow } : {}),
|
|
...(reasoning !== undefined ? { reasoning } : {}),
|
|
...(input ? { input } : {}),
|
|
});
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
function withStreamingUsageCompat(provider: ModelProviderConfig): ModelProviderConfig {
|
|
if (!Array.isArray(provider.models) || provider.models.length === 0) {
|
|
return provider;
|
|
}
|
|
|
|
let changed = false;
|
|
const models = provider.models.map((model) => {
|
|
if (model.compat?.supportsUsageInStreaming !== undefined) {
|
|
return model;
|
|
}
|
|
changed = true;
|
|
return {
|
|
...model,
|
|
compat: {
|
|
...model.compat,
|
|
supportsUsageInStreaming: true,
|
|
},
|
|
};
|
|
});
|
|
|
|
return changed ? { ...provider, models } : provider;
|
|
}
|
|
|
|
export function supportsNativeStreamingUsageCompat(params: {
|
|
providerId: string;
|
|
baseUrl: string | undefined;
|
|
}): boolean {
|
|
return resolveProviderRequestCapabilities({
|
|
provider: params.providerId,
|
|
api: "openai-completions",
|
|
baseUrl: params.baseUrl,
|
|
capability: "llm",
|
|
transport: "stream",
|
|
}).supportsNativeStreamingUsageCompat;
|
|
}
|
|
|
|
export function applyProviderNativeStreamingUsageCompat(params: {
|
|
providerId: string;
|
|
providerConfig: ModelProviderConfig;
|
|
}): ModelProviderConfig {
|
|
return supportsNativeStreamingUsageCompat({
|
|
providerId: params.providerId,
|
|
baseUrl: params.providerConfig.baseUrl,
|
|
})
|
|
? withStreamingUsageCompat(params.providerConfig)
|
|
: params.providerConfig;
|
|
}
|