mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:20:43 +00:00
169 lines
6.1 KiB
TypeScript
169 lines
6.1 KiB
TypeScript
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 {
|
|
resolveOpenRouterThinkingProfile,
|
|
supportsOpenRouterXHighThinking,
|
|
} from "./thinking-policy.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<T extends ProviderRuntimeModel>(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",
|
|
supportsXHighThinking: ({ modelId }) => supportsOpenRouterXHighThinking(modelId),
|
|
resolveThinkingProfile: ({ modelId }) => resolveOpenRouterThinkingProfile(modelId),
|
|
isModernModelRef: () => true,
|
|
wrapStreamFn: wrapOpenRouterProviderStream,
|
|
isCacheTtlEligible: (ctx) => isOpenRouterCacheTtlModel(ctx.modelId),
|
|
});
|
|
api.registerMediaUnderstandingProvider(openrouterMediaUnderstandingProvider);
|
|
api.registerImageGenerationProvider(buildOpenRouterImageGenerationProvider());
|
|
api.registerVideoGenerationProvider(buildOpenRouterVideoGenerationProvider());
|
|
api.registerSpeechProvider(buildOpenRouterSpeechProvider());
|
|
},
|
|
});
|