mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 01:29:56 +00:00
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>
229 lines
6.9 KiB
TypeScript
229 lines
6.9 KiB
TypeScript
// Vercel Ai Gateway plugin module implements models behavior.
|
|
import { withTrustedEnvProxyGuardedFetchMode } from "openclaw/plugin-sdk/fetch-runtime";
|
|
import { parseStrictFiniteNumber } from "openclaw/plugin-sdk/number-runtime";
|
|
import { buildLiveModelProviderConfig } from "openclaw/plugin-sdk/provider-catalog-live-runtime";
|
|
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared";
|
|
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
import { asPositiveSafeInteger } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
|
|
export const VERCEL_AI_GATEWAY_PROVIDER_ID = "vercel-ai-gateway";
|
|
export const VERCEL_AI_GATEWAY_BASE_URL = "https://ai-gateway.vercel.sh";
|
|
export const VERCEL_AI_GATEWAY_DEFAULT_MODEL_ID = "anthropic/claude-opus-4.6";
|
|
export const VERCEL_AI_GATEWAY_DEFAULT_CONTEXT_WINDOW = 200_000;
|
|
export const VERCEL_AI_GATEWAY_DEFAULT_MAX_TOKENS = 128_000;
|
|
export const VERCEL_AI_GATEWAY_DEFAULT_COST = {
|
|
input: 0,
|
|
output: 0,
|
|
cacheRead: 0,
|
|
cacheWrite: 0,
|
|
} as const;
|
|
|
|
const VERCEL_AI_GATEWAY_DISCOVERY_CACHE_TTL_MS = 60_000;
|
|
const VERCEL_AI_GATEWAY_DISCOVERY_TIMEOUT_MS = 5000;
|
|
|
|
type VercelPricingShape = {
|
|
input?: number | string;
|
|
output?: number | string;
|
|
input_cache_read?: number | string;
|
|
input_cache_write?: number | string;
|
|
};
|
|
|
|
type VercelGatewayModelShape = {
|
|
id?: string;
|
|
type?: string;
|
|
name?: string;
|
|
context_window?: number;
|
|
max_tokens?: number;
|
|
tags?: string[];
|
|
pricing?: VercelPricingShape;
|
|
};
|
|
|
|
type StaticVercelGatewayModel = Omit<ModelDefinitionConfig, "cost"> & {
|
|
cost?: Partial<ModelDefinitionConfig["cost"]>;
|
|
};
|
|
|
|
const STATIC_VERCEL_AI_GATEWAY_MODEL_CATALOG: readonly StaticVercelGatewayModel[] = [
|
|
{
|
|
id: "anthropic/claude-opus-4.6",
|
|
name: "Claude Opus 4.6",
|
|
reasoning: true,
|
|
input: ["text", "image"],
|
|
contextWindow: 1_000_000,
|
|
maxTokens: 128_000,
|
|
cost: {
|
|
input: 5,
|
|
output: 25,
|
|
cacheRead: 0.5,
|
|
cacheWrite: 6.25,
|
|
},
|
|
},
|
|
{
|
|
id: "openai/gpt-5.4",
|
|
name: "GPT 5.4",
|
|
reasoning: true,
|
|
input: ["text", "image"],
|
|
contextWindow: 200_000,
|
|
maxTokens: 128_000,
|
|
cost: {
|
|
input: 2.5,
|
|
output: 15,
|
|
cacheRead: 0.25,
|
|
},
|
|
},
|
|
{
|
|
id: "openai/gpt-5.4-pro",
|
|
name: "GPT 5.4 Pro",
|
|
reasoning: true,
|
|
input: ["text", "image"],
|
|
contextWindow: 200_000,
|
|
maxTokens: 128_000,
|
|
cost: {
|
|
input: 30,
|
|
output: 180,
|
|
cacheRead: 0,
|
|
},
|
|
},
|
|
{
|
|
id: "moonshotai/kimi-k2.6",
|
|
name: "Kimi K2.6",
|
|
reasoning: true,
|
|
input: ["text", "image"],
|
|
contextWindow: 262_144,
|
|
maxTokens: 262_144,
|
|
cost: {
|
|
input: 0.95,
|
|
output: 4,
|
|
cacheRead: 0.16,
|
|
},
|
|
},
|
|
] as const;
|
|
|
|
function toPerMillionCost(value: number | string | undefined): number {
|
|
const numeric =
|
|
typeof value === "number"
|
|
? value
|
|
: typeof value === "string"
|
|
? parseStrictFiniteNumber(value)
|
|
: undefined;
|
|
if (numeric === undefined || numeric < 0) {
|
|
return 0;
|
|
}
|
|
return numeric * 1_000_000;
|
|
}
|
|
|
|
function normalizeCost(pricing?: VercelPricingShape): ModelDefinitionConfig["cost"] {
|
|
return {
|
|
input: toPerMillionCost(pricing?.input),
|
|
output: toPerMillionCost(pricing?.output),
|
|
cacheRead: toPerMillionCost(pricing?.input_cache_read),
|
|
cacheWrite: toPerMillionCost(pricing?.input_cache_write),
|
|
};
|
|
}
|
|
|
|
function buildStaticModelDefinition(model: StaticVercelGatewayModel): ModelDefinitionConfig {
|
|
return {
|
|
id: model.id,
|
|
name: model.name,
|
|
reasoning: model.reasoning,
|
|
input: model.input,
|
|
contextWindow: model.contextWindow,
|
|
maxTokens: model.maxTokens,
|
|
cost: {
|
|
...VERCEL_AI_GATEWAY_DEFAULT_COST,
|
|
...model.cost,
|
|
},
|
|
};
|
|
}
|
|
|
|
function getStaticFallbackModel(id: string): ModelDefinitionConfig | undefined {
|
|
const fallback = STATIC_VERCEL_AI_GATEWAY_MODEL_CATALOG.find((model) => model.id === id);
|
|
return fallback ? buildStaticModelDefinition(fallback) : undefined;
|
|
}
|
|
|
|
/** Builds runtime metadata for models returned by the live gateway catalog. */
|
|
export function resolveVercelAiGatewayDynamicModel(modelId: string): ModelDefinitionConfig {
|
|
return (
|
|
getStaticFallbackModel(modelId) ?? {
|
|
id: modelId,
|
|
name: modelId,
|
|
reasoning: false,
|
|
input: ["text"],
|
|
contextWindow: VERCEL_AI_GATEWAY_DEFAULT_CONTEXT_WINDOW,
|
|
maxTokens: VERCEL_AI_GATEWAY_DEFAULT_MAX_TOKENS,
|
|
cost: VERCEL_AI_GATEWAY_DEFAULT_COST,
|
|
}
|
|
);
|
|
}
|
|
|
|
export function getStaticVercelAiGatewayModelCatalog(): ModelDefinitionConfig[] {
|
|
return STATIC_VERCEL_AI_GATEWAY_MODEL_CATALOG.map(buildStaticModelDefinition);
|
|
}
|
|
|
|
function buildDiscoveredModelDefinition(value: unknown): ModelDefinitionConfig | null {
|
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
throw new Error("Vercel AI Gateway model list: malformed JSON response");
|
|
}
|
|
const model = value as VercelGatewayModelShape;
|
|
const id = typeof model.id === "string" ? model.id.trim() : "";
|
|
if (!id || (model.type !== undefined && model.type !== "language")) {
|
|
return null;
|
|
}
|
|
|
|
const fallback = getStaticFallbackModel(id);
|
|
const contextWindow =
|
|
asPositiveSafeInteger(model.context_window) ??
|
|
fallback?.contextWindow ??
|
|
VERCEL_AI_GATEWAY_DEFAULT_CONTEXT_WINDOW;
|
|
const maxTokens =
|
|
asPositiveSafeInteger(model.max_tokens) ??
|
|
fallback?.maxTokens ??
|
|
VERCEL_AI_GATEWAY_DEFAULT_MAX_TOKENS;
|
|
const normalizedCost = normalizeCost(model.pricing);
|
|
|
|
return {
|
|
id,
|
|
name: (typeof model.name === "string" ? model.name.trim() : "") || fallback?.name || id,
|
|
reasoning:
|
|
Array.isArray(model.tags) && model.tags.includes("reasoning")
|
|
? true
|
|
: (fallback?.reasoning ?? false),
|
|
input: Array.isArray(model.tags)
|
|
? model.tags.includes("vision")
|
|
? ["text", "image"]
|
|
: ["text"]
|
|
: (fallback?.input ?? ["text"]),
|
|
contextWindow,
|
|
maxTokens,
|
|
cost:
|
|
normalizedCost.input > 0 ||
|
|
normalizedCost.output > 0 ||
|
|
normalizedCost.cacheRead > 0 ||
|
|
normalizedCost.cacheWrite > 0
|
|
? normalizedCost
|
|
: (fallback?.cost ?? VERCEL_AI_GATEWAY_DEFAULT_COST),
|
|
};
|
|
}
|
|
|
|
export async function discoverVercelAiGatewayModels(
|
|
options: { discoveryMode?: "strict" } = {},
|
|
): Promise<ModelDefinitionConfig[]> {
|
|
const provider = await buildLiveModelProviderConfig({
|
|
...options,
|
|
providerId: VERCEL_AI_GATEWAY_PROVIDER_ID,
|
|
endpoint: `${VERCEL_AI_GATEWAY_BASE_URL}/v1/models`,
|
|
providerConfig: {
|
|
baseUrl: VERCEL_AI_GATEWAY_BASE_URL,
|
|
api: "anthropic-messages",
|
|
},
|
|
models: getStaticVercelAiGatewayModelCatalog(),
|
|
timeoutMs: VERCEL_AI_GATEWAY_DISCOVERY_TIMEOUT_MS,
|
|
ttlMs: VERCEL_AI_GATEWAY_DISCOVERY_CACHE_TTL_MS,
|
|
auditContext: "vercel-ai-gateway.models",
|
|
fetchGuard: (params) => fetchWithSsrFGuard(withTrustedEnvProxyGuardedFetchMode(params)),
|
|
projectRows: (rows) =>
|
|
rows
|
|
.map(buildDiscoveredModelDefinition)
|
|
.filter((entry): entry is ModelDefinitionConfig => entry !== null),
|
|
});
|
|
return provider.models;
|
|
}
|