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,
normalizeOpenRouterBaseUrl,
OPENROUTER_BASE_URL,
} from "./provider-catalog.js" ;
import { buildOpenRouterSpeechProvider } from "./speech-provider.js" ;
import { wrapOpenRouterProviderStream } from "./stream.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<T extends ProviderRuntimeModel>(model: T): T | undefined {
const normalizedBaseUrl = normalizeOpenRouterBaseUrl(model.baseUrl);
if (!normalizedBaseUrl || normalizedBaseUrl === model.baseUrl) {
return undefined;
}
return {
...model,
baseUrl: normalizedBaseUrl,
};
}
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 ,
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.registerSpeechProvider(buildOpenRouterSpeechProvider());
},
});
Messung V0.5 in Prozent C=100 H=100 G=100
¤ Dauer der Verarbeitung: 0.13 Sekunden
(vorverarbeitet am 2026-06-05)
¤
*© Formatika GbR, Deutschland