mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 08:11:34 +00:00
* feat(plugin-sdk): add live catalog row projection * refactor(providers): share live catalog projection * fix(venice): keep live projection internal
129 lines
4.5 KiB
TypeScript
129 lines
4.5 KiB
TypeScript
/**
|
|
* Chutes model catalog, static model definitions, and dynamic model discovery.
|
|
*/
|
|
import { withTrustedEnvProxyGuardedFetchMode } from "openclaw/plugin-sdk/fetch-runtime";
|
|
import { buildLiveModelProviderConfig } from "openclaw/plugin-sdk/provider-catalog-live-runtime";
|
|
import { buildManifestModelDefinition } from "openclaw/plugin-sdk/provider-catalog-shared";
|
|
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared";
|
|
import {
|
|
fetchWithSsrFGuard,
|
|
ssrfPolicyFromHttpBaseUrlAllowedHostname,
|
|
} from "openclaw/plugin-sdk/ssrf-runtime";
|
|
import {
|
|
asPositiveSafeInteger,
|
|
normalizeLowercaseStringOrEmpty,
|
|
normalizeOptionalString,
|
|
} from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
import { isChutesModelDiscoveryTestEnvironment } from "./model-discovery-env.js";
|
|
import manifest from "./openclaw.plugin.json" with { type: "json" };
|
|
|
|
const CHUTES_MANIFEST_CATALOG = manifest.modelCatalog.providers.chutes;
|
|
|
|
/** Base URL for Chutes OpenAI-compatible inference. */
|
|
export const CHUTES_BASE_URL = CHUTES_MANIFEST_CATALOG.baseUrl;
|
|
|
|
const CHUTES_DEFAULT_CONTEXT_WINDOW = 128000;
|
|
const CHUTES_DEFAULT_MAX_TOKENS = 4096;
|
|
|
|
function decorateChutesModelDefinition(model: ModelDefinitionConfig): ModelDefinitionConfig {
|
|
return {
|
|
...model,
|
|
compat: {
|
|
...model.compat,
|
|
supportsUsageInStreaming: false,
|
|
},
|
|
};
|
|
}
|
|
|
|
/** Bundled fallback Chutes model catalog, normalized from the plugin manifest. */
|
|
export const CHUTES_MODEL_CATALOG: ModelDefinitionConfig[] = CHUTES_MANIFEST_CATALOG.models.map(
|
|
buildManifestModelDefinition({
|
|
providerId: "chutes",
|
|
catalog: CHUTES_MANIFEST_CATALOG,
|
|
decorate: decorateChutesModelDefinition,
|
|
}),
|
|
);
|
|
|
|
interface ChutesModelEntry {
|
|
id: string;
|
|
name?: string;
|
|
supported_features?: string[];
|
|
input_modalities?: string[];
|
|
context_length?: number;
|
|
max_output_length?: number;
|
|
pricing?: {
|
|
prompt?: number;
|
|
completion?: number;
|
|
input_cache_read?: number;
|
|
};
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
const CACHE_TTL = 5 * 60 * 1000;
|
|
|
|
function projectChutesModels(rows: readonly unknown[]): ModelDefinitionConfig[] {
|
|
const seen = new Set<string>();
|
|
const models: ModelDefinitionConfig[] = [];
|
|
for (const row of rows) {
|
|
if (!row || typeof row !== "object" || Array.isArray(row)) {
|
|
continue;
|
|
}
|
|
const entry = row as ChutesModelEntry;
|
|
const id = normalizeOptionalString(entry.id) ?? "";
|
|
if (!id || seen.has(id)) {
|
|
continue;
|
|
}
|
|
seen.add(id);
|
|
const lowerId = normalizeLowercaseStringOrEmpty(id);
|
|
models.push({
|
|
id,
|
|
name: id,
|
|
reasoning:
|
|
entry.supported_features?.includes("reasoning") ||
|
|
lowerId.includes("r1") ||
|
|
lowerId.includes("thinking") ||
|
|
lowerId.includes("reason") ||
|
|
lowerId.includes("tee"),
|
|
input: (entry.input_modalities || ["text"]).filter(
|
|
(item): item is "text" | "image" => item === "text" || item === "image",
|
|
),
|
|
cost: {
|
|
input: entry.pricing?.prompt || 0,
|
|
output: entry.pricing?.completion || 0,
|
|
cacheRead: entry.pricing?.input_cache_read || 0,
|
|
cacheWrite: 0,
|
|
},
|
|
contextWindow: asPositiveSafeInteger(entry.context_length) ?? CHUTES_DEFAULT_CONTEXT_WINDOW,
|
|
maxTokens: asPositiveSafeInteger(entry.max_output_length) ?? CHUTES_DEFAULT_MAX_TOKENS,
|
|
compat: { supportsUsageInStreaming: false },
|
|
});
|
|
}
|
|
return models;
|
|
}
|
|
|
|
/** Discovers Chutes models dynamically, falling back to the bundled static catalog. */
|
|
export async function discoverChutesModels(accessToken?: string): Promise<ModelDefinitionConfig[]> {
|
|
if (isChutesModelDiscoveryTestEnvironment()) {
|
|
return structuredClone(CHUTES_MODEL_CATALOG);
|
|
}
|
|
const provider = await buildLiveModelProviderConfig({
|
|
providerId: "chutes",
|
|
endpoint: `${CHUTES_BASE_URL}/models`,
|
|
providerConfig: { baseUrl: CHUTES_BASE_URL, api: "openai-completions" },
|
|
models: structuredClone(CHUTES_MODEL_CATALOG),
|
|
discoveryApiKey: normalizeOptionalString(accessToken),
|
|
timeoutMs: 10_000,
|
|
ttlMs: CACHE_TTL,
|
|
buildRequestHeaders: ({ discoveryApiKey }) => ({
|
|
Accept: "application/json",
|
|
...(discoveryApiKey ? { Authorization: `Bearer ${discoveryApiKey}` } : {}),
|
|
}),
|
|
policy: ssrfPolicyFromHttpBaseUrlAllowedHostname(CHUTES_BASE_URL),
|
|
auditContext: "chutes-model-discovery",
|
|
fetchGuard: (params) => fetchWithSsrFGuard(withTrustedEnvProxyGuardedFetchMode(params)),
|
|
fallbackToAnonymousOnUnauthorized: true,
|
|
projectRows: projectChutesModels,
|
|
});
|
|
return provider.models;
|
|
}
|