import { definePluginEntry, type ProviderResolveDynamicModelContext, type ProviderRuntimeModel, } from "openclaw/plugin-sdk/plugin-entry"; import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key"; import { DEFAULT_CONTEXT_TOKENS, PASSTHROUGH_GEMINI_REPLAY_HOOKS, } from "openclaw/plugin-sdk/provider-model-shared"; import { getOpenRouterModelCapabilities, loadOpenRouterModelCapabilities, } from "openclaw/plugin-sdk/provider-stream-family"; import { buildOpenRouterImageGenerationProvider } from "./image-generation-provider.js"; import { openrouterMediaUnderstandingProvider } from "./media-understanding-provider.js"; import { applyOpenrouterConfig, OPENROUTER_DEFAULT_MODEL_REF } from "./onboard.js"; import { buildOpenrouterProvider, isOpenRouterProxyReasoningUnsupportedModel, normalizeOpenRouterBaseUrl, OPENROUTER_BASE_URL, } from "./provider-catalog.js"; import { buildOpenRouterSpeechProvider } from "./speech-provider.js"; import { wrapOpenRouterProviderStream } from "./stream.js"; import { buildOpenRouterVideoGenerationProvider } from "./video-generation-provider.js"; const PROVIDER_ID = "openrouter"; const OPENROUTER_DEFAULT_MAX_TOKENS = 8192; const OPENROUTER_CACHE_TTL_MODEL_PREFIXES = [ "anthropic/", "deepseek/", "moonshot/", "moonshotai/", "zai/", ] as const; function normalizeOpenRouterResolvedModel(model: T): T | undefined { const normalizedBaseUrl = normalizeOpenRouterBaseUrl(model.baseUrl); const reasoning = isOpenRouterProxyReasoningUnsupportedModel(model.id) ? false : model.reasoning; if ( (!normalizedBaseUrl || normalizedBaseUrl === model.baseUrl) && reasoning === model.reasoning ) { return undefined; } return { ...model, ...(normalizedBaseUrl ? { baseUrl: normalizedBaseUrl } : {}), reasoning, }; } export default definePluginEntry({ id: "openrouter", name: "OpenRouter Provider", description: "Bundled OpenRouter provider plugin", register(api) { function buildDynamicOpenRouterModel( ctx: ProviderResolveDynamicModelContext, ): ProviderRuntimeModel { const capabilities = getOpenRouterModelCapabilities(ctx.modelId); return { id: ctx.modelId, name: capabilities?.name ?? ctx.modelId, api: "openai-completions", provider: PROVIDER_ID, baseUrl: OPENROUTER_BASE_URL, reasoning: (capabilities?.reasoning ?? false) && !isOpenRouterProxyReasoningUnsupportedModel(ctx.modelId), input: capabilities?.input ?? ["text"], cost: capabilities?.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: capabilities?.contextWindow ?? DEFAULT_CONTEXT_TOKENS, maxTokens: capabilities?.maxTokens ?? OPENROUTER_DEFAULT_MAX_TOKENS, }; } function isOpenRouterCacheTtlModel(modelId: string): boolean { return OPENROUTER_CACHE_TTL_MODEL_PREFIXES.some((prefix) => modelId.startsWith(prefix)); } api.registerProvider({ id: PROVIDER_ID, label: "OpenRouter", docsPath: "/providers/models", envVars: ["OPENROUTER_API_KEY"], auth: [ createProviderApiKeyAuthMethod({ providerId: PROVIDER_ID, methodId: "api-key", label: "OpenRouter API key", hint: "API key", optionKey: "openrouterApiKey", flagName: "--openrouter-api-key", envVar: "OPENROUTER_API_KEY", promptMessage: "Enter OpenRouter API key", defaultModel: OPENROUTER_DEFAULT_MODEL_REF, expectedProviders: ["openrouter"], applyConfig: (cfg) => applyOpenrouterConfig(cfg), wizard: { choiceId: "openrouter-api-key", choiceLabel: "OpenRouter API key", groupId: "openrouter", groupLabel: "OpenRouter", groupHint: "API key", }, }), ], catalog: { order: "simple", run: async (ctx) => { const apiKey = ctx.resolveProviderApiKey(PROVIDER_ID).apiKey; if (!apiKey) { return null; } return { provider: { ...buildOpenrouterProvider(), apiKey, }, }; }, }, staticCatalog: { order: "simple", run: async () => ({ provider: buildOpenrouterProvider(), }), }, resolveDynamicModel: (ctx) => buildDynamicOpenRouterModel(ctx), prepareDynamicModel: async (ctx) => { await loadOpenRouterModelCapabilities(ctx.modelId); }, normalizeConfig: ({ providerConfig }) => { const normalizedBaseUrl = normalizeOpenRouterBaseUrl(providerConfig.baseUrl); return normalizedBaseUrl && normalizedBaseUrl !== providerConfig.baseUrl ? { ...providerConfig, baseUrl: normalizedBaseUrl } : undefined; }, normalizeResolvedModel: ({ model }) => normalizeOpenRouterResolvedModel(model), normalizeTransport: ({ api, baseUrl }) => { const normalizedBaseUrl = normalizeOpenRouterBaseUrl(baseUrl); return normalizedBaseUrl && normalizedBaseUrl !== baseUrl ? { api, baseUrl: normalizedBaseUrl, } : undefined; }, ...PASSTHROUGH_GEMINI_REPLAY_HOOKS, resolveReasoningOutputMode: () => "native", isModernModelRef: () => true, wrapStreamFn: wrapOpenRouterProviderStream, isCacheTtlEligible: (ctx) => isOpenRouterCacheTtlModel(ctx.modelId), }); api.registerMediaUnderstandingProvider(openrouterMediaUnderstandingProvider); api.registerImageGenerationProvider(buildOpenRouterImageGenerationProvider()); api.registerVideoGenerationProvider(buildOpenRouterVideoGenerationProvider()); api.registerSpeechProvider(buildOpenRouterSpeechProvider()); }, });