openclaw/extensions/chutes/models.ts
Ayaan Zaidi d141dc65ec
fix(models): report failed bundled catalog refreshes (#139649)
Related: #136257. Builds on merged #139357.

Make all bundled shared-helper catalog callers report actual acquisition failures and authoritative empty results. Preserve shipped external advisory defaults through the same transport/cache implementation.

The existing publication owner retains compatible learned inventory or degraded provider-owned starters. Static preparation consumes registered hooks with their authoritative plugin identity, without activating unknown runtimes or widening successful empty membership. No operator option, persistence schema, new cache, or auth re-observation is introduced.

Proof: retained built baseline failure, packaged CLI/Gateway fault/recovery/empty checks, public SDK/provider API compatibility, fresh/upgrade starter checks, and the decisive seven-clause downstream composite on exact shared head 5e0ecef411cc7bcabbcab2672227910a05209ffd. Composite 296bfa19ec7663044ce50b8ebb3652555900d531 adds only the unchanged separate DeepInfra/NVIDIA producer patch; that patch is not part of this PR. Controlled clock advancement and fixture-rejected background inference are disclosed. No real vendor inference or UI claim.

Production net +199; tests/support +313; docs +34; ratchet metadata -1; generated/lockfile delta zero. Growth preserves shipped API and shared failure/empty/ownership contracts rather than adding parallel provider policy.

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-09-06 10:03:57 +05:30

126 lines
4.4 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 { buildManifestModelProviderConfig } 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 manifest from "./openclaw.plugin.json" with { type: "json" };
import { normalizeChutesModelPricing } from "./pricing-api.js";
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[] = buildManifestModelProviderConfig({
providerId: "chutes",
catalog: CHUTES_MANIFEST_CATALOG,
}).models.map(decorateChutesModelDefinition);
interface ChutesModelEntry {
id: string;
name?: string;
supported_features?: string[];
input_modalities?: string[];
context_length?: number;
max_model_len?: number;
max_output_length?: number;
pricing?: unknown;
[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",
),
// Runtime requires a cost object; unknown pricing must not retain partial paid rates.
cost: normalizeChutesModelPricing(entry.pricing) ?? {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow:
asPositiveSafeInteger(entry.context_length) ??
asPositiveSafeInteger(entry.max_model_len) ??
CHUTES_DEFAULT_CONTEXT_WINDOW,
maxTokens: asPositiveSafeInteger(entry.max_output_length) ?? CHUTES_DEFAULT_MAX_TOKENS,
compat: { supportsUsageInStreaming: false },
});
}
return models;
}
export async function discoverChutesModels(
accessToken?: string,
options: { discoveryMode?: "strict" } = {},
): Promise<ModelDefinitionConfig[]> {
const provider = await buildLiveModelProviderConfig({
...options,
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;
}