Files
openclaw/extensions/venice/models.ts
Peter Steinberger 603f839058 refactor(providers): collapse live model discovery onto shared projection hook (#113903)
* feat(plugin-sdk): add live catalog row projection

* refactor(providers): share live catalog projection

* fix(venice): keep live projection internal
2026-07-25 16:17:27 -07:00

173 lines
5.6 KiB
TypeScript

import {
buildManifestModelDefinition,
readManifestProviderDefaultModelRef,
} from "openclaw/plugin-sdk/provider-catalog-shared";
import type {
ModelDefinitionConfig,
ModelProviderConfig,
} from "openclaw/plugin-sdk/provider-model-shared";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
import manifest from "./openclaw.plugin.json" with { type: "json" };
const VENICE_MANIFEST_CATALOG = manifest.modelCatalog.providers.venice;
export const VENICE_BASE_URL = VENICE_MANIFEST_CATALOG.baseUrl;
export const VENICE_DEFAULT_MODEL_REF = readManifestProviderDefaultModelRef(manifest, "venice")!;
const VENICE_DEFAULT_COST = {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
};
const VENICE_DEFAULT_CONTEXT_WINDOW = 128_000;
const VENICE_DEFAULT_MAX_TOKENS = 4096;
const VENICE_DISCOVERY_HARD_MAX_TOKENS = 131_072;
const VENICE_DISCOVERY_TIMEOUT_MS = 10_000;
const VENICE_DISCOVERY_CACHE_TTL_MS = 60_000;
function decorateVeniceModelDefinition(entry: ModelDefinitionConfig): ModelDefinitionConfig {
return {
id: entry.id,
name: entry.name,
reasoning: entry.reasoning,
input: [...entry.input],
cost: VENICE_DEFAULT_COST,
contextWindow: entry.contextWindow,
maxTokens: entry.maxTokens,
compat: {
supportsUsageInStreaming: false,
...entry.compat,
},
};
}
/** Venice's decorated network-free fallback catalog. */
export const VENICE_MODEL_CATALOG: ModelDefinitionConfig[] = VENICE_MANIFEST_CATALOG.models.map(
buildManifestModelDefinition({
providerId: "venice",
catalog: VENICE_MANIFEST_CATALOG,
decorate: decorateVeniceModelDefinition,
}),
);
interface VeniceModelSpec {
name: string;
privacy: "private" | "anonymized";
availableContextTokens?: number;
maxCompletionTokens?: number;
capabilities?: {
supportsReasoning?: boolean;
supportsVision?: boolean;
supportsFunctionCalling?: boolean;
};
}
interface VeniceModel {
id: string;
model_spec?: VeniceModelSpec;
}
function normalizePositiveInt(value: unknown): number | undefined {
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
return undefined;
}
return Math.floor(value);
}
function resolveApiMaxCompletionTokens(params: {
apiModel: VeniceModel;
knownMaxTokens?: number;
}): number | undefined {
const raw = normalizePositiveInt(params.apiModel.model_spec?.maxCompletionTokens);
if (!raw) {
return undefined;
}
const contextWindow = normalizePositiveInt(params.apiModel.model_spec?.availableContextTokens);
const knownMaxTokens =
typeof params.knownMaxTokens === "number" && Number.isFinite(params.knownMaxTokens)
? Math.floor(params.knownMaxTokens)
: undefined;
const hardCap = knownMaxTokens ?? VENICE_DISCOVERY_HARD_MAX_TOKENS;
const fallbackContextWindow = knownMaxTokens ?? VENICE_DEFAULT_CONTEXT_WINDOW;
return Math.min(raw, contextWindow ?? fallbackContextWindow, hardCap);
}
function resolveApiSupportsTools(apiModel: VeniceModel): boolean | undefined {
const supportsFunctionCalling = apiModel.model_spec?.capabilities?.supportsFunctionCalling;
return typeof supportsFunctionCalling === "boolean" ? supportsFunctionCalling : undefined;
}
function projectVeniceModels(
rows: readonly unknown[],
fallback: ModelProviderConfig,
): ModelDefinitionConfig[] {
const catalogById = new Map(fallback.models.map((model) => [model.id, model]));
const models: ModelDefinitionConfig[] = [];
for (const row of rows) {
if (!row || typeof row !== "object" || Array.isArray(row)) {
continue;
}
const apiModel = row as VeniceModel;
if (typeof apiModel.id !== "string" || !apiModel.id.trim()) {
continue;
}
const catalogEntry = catalogById.get(apiModel.id);
const apiMaxTokens = resolveApiMaxCompletionTokens({
apiModel,
knownMaxTokens: catalogEntry?.maxTokens,
});
const apiSupportsTools = resolveApiSupportsTools(apiModel);
if (catalogEntry) {
const definition: ModelDefinitionConfig = {
...catalogEntry,
input: [...catalogEntry.input],
cost: { ...catalogEntry.cost },
...(catalogEntry.compat ? { compat: { ...catalogEntry.compat } } : {}),
};
if (apiMaxTokens !== undefined) {
definition.maxTokens = apiMaxTokens;
}
if (apiSupportsTools === false) {
definition.compat = {
...definition.compat,
supportsTools: false,
};
}
models.push(definition);
} else {
const apiSpec = apiModel.model_spec;
const lowerModelId = normalizeLowercaseStringOrEmpty(apiModel.id);
const isReasoning =
apiSpec?.capabilities?.supportsReasoning ||
lowerModelId.includes("thinking") ||
lowerModelId.includes("reason") ||
lowerModelId.includes("r1");
const hasVision = apiSpec?.capabilities?.supportsVision === true;
models.push({
id: apiModel.id,
name: apiSpec?.name || apiModel.id,
reasoning: isReasoning,
input: hasVision ? ["text", "image"] : ["text"],
cost: VENICE_DEFAULT_COST,
contextWindow:
normalizePositiveInt(apiSpec?.availableContextTokens) ?? VENICE_DEFAULT_CONTEXT_WINDOW,
maxTokens: apiMaxTokens ?? VENICE_DEFAULT_MAX_TOKENS,
compat: {
supportsUsageInStreaming: false,
...(apiSupportsTools === false ? { supportsTools: false } : {}),
},
});
}
}
return models;
}
export const VENICE_MODEL_DISCOVERY_OPTIONS = {
timeoutMs: VENICE_DISCOVERY_TIMEOUT_MS,
ttlMs: VENICE_DISCOVERY_CACHE_TTL_MS,
buildRequestHeaders: () => ({ Accept: "application/json" }),
projectRows: projectVeniceModels,
} as const;