mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-12 17:43:17 +08:00
重构主控编排与运行时预热链路,统一工作区提示词/专家调度协议并补齐 wiki 记忆注入与写回闭环。
同时收敛启动与运维脚本默认行为(含 wiki worker)、更新 Admin 可观测性与相关测试,降低首轮时延并提高运行稳定性。 Made-with: Cursor
This commit is contained in:
parent
4a23b715a2
commit
dbbe3add6a
14438 changed files with 2693620 additions and 2546 deletions
6
openclaw/extensions/openrouter/api.ts
Normal file
6
openclaw/extensions/openrouter/api.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export { buildOpenrouterProvider } from "./provider-catalog.js";
|
||||
export {
|
||||
applyOpenrouterConfig,
|
||||
applyOpenrouterProviderConfig,
|
||||
OPENROUTER_DEFAULT_MODEL_REF,
|
||||
} from "./onboard.js";
|
||||
122
openclaw/extensions/openrouter/index.test.ts
Normal file
122
openclaw/extensions/openrouter/index.test.ts
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
import { registerSingleProviderPlugin } from "../../test/helpers/plugins/plugin-registration.js";
|
||||
import { expectPassthroughReplayPolicy } from "../../test/helpers/provider-replay-policy.ts";
|
||||
import openrouterPlugin from "./index.js";
|
||||
|
||||
describe("openrouter provider hooks", () => {
|
||||
it("owns passthrough-gemini replay policy for Gemini-backed models", async () => {
|
||||
await expectPassthroughReplayPolicy({
|
||||
plugin: openrouterPlugin,
|
||||
providerId: "openrouter",
|
||||
modelId: "gemini-2.5-pro",
|
||||
sanitizeThoughtSignatures: true,
|
||||
});
|
||||
await expectPassthroughReplayPolicy({
|
||||
plugin: openrouterPlugin,
|
||||
providerId: "openrouter",
|
||||
modelId: "openai/gpt-5.4",
|
||||
});
|
||||
});
|
||||
|
||||
it("owns native reasoning output mode", async () => {
|
||||
const provider = await registerSingleProviderPlugin(openrouterPlugin);
|
||||
|
||||
expect(
|
||||
provider.resolveReasoningOutputMode?.({
|
||||
provider: "openrouter",
|
||||
modelApi: "openai-completions",
|
||||
modelId: "openai/gpt-5.4",
|
||||
} as never),
|
||||
).toBe("native");
|
||||
});
|
||||
|
||||
it("canonicalizes stale OpenRouter /v1 config and runtime metadata", async () => {
|
||||
const provider = await registerSingleProviderPlugin(openrouterPlugin);
|
||||
|
||||
expect(
|
||||
provider.normalizeConfig?.({
|
||||
provider: "openrouter",
|
||||
providerConfig: {
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://openrouter.ai/v1/",
|
||||
models: [],
|
||||
},
|
||||
} as never),
|
||||
).toMatchObject({
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
});
|
||||
|
||||
expect(
|
||||
provider.normalizeResolvedModel?.({
|
||||
provider: "openrouter",
|
||||
model: {
|
||||
provider: "openrouter",
|
||||
id: "openai/gpt-5.4",
|
||||
name: "openai/gpt-5.4",
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://openrouter.ai/v1",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 200_000,
|
||||
maxTokens: 8192,
|
||||
},
|
||||
} as never),
|
||||
).toMatchObject({
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
});
|
||||
|
||||
expect(
|
||||
provider.normalizeTransport?.({
|
||||
provider: "openrouter",
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://openrouter.ai/v1",
|
||||
} as never),
|
||||
).toEqual({
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
});
|
||||
});
|
||||
|
||||
it("injects provider routing into compat before applying stream wrappers", async () => {
|
||||
const provider = await registerSingleProviderPlugin(openrouterPlugin);
|
||||
const baseStreamFn = vi.fn(
|
||||
(..._args: Parameters<import("@mariozechner/pi-agent-core").StreamFn>) =>
|
||||
({ async *[Symbol.asyncIterator]() {} }) as never,
|
||||
);
|
||||
|
||||
const wrapped = provider.wrapStreamFn?.({
|
||||
provider: "openrouter",
|
||||
modelId: "openai/gpt-5.4",
|
||||
extraParams: {
|
||||
provider: {
|
||||
order: ["moonshot"],
|
||||
},
|
||||
},
|
||||
streamFn: baseStreamFn,
|
||||
thinkingLevel: "high",
|
||||
} as never);
|
||||
|
||||
void wrapped?.(
|
||||
{
|
||||
provider: "openrouter",
|
||||
api: "openai-completions",
|
||||
id: "openai/gpt-5.4",
|
||||
compat: {},
|
||||
} as never,
|
||||
{ messages: [] } as never,
|
||||
{},
|
||||
);
|
||||
|
||||
expect(baseStreamFn).toHaveBeenCalledOnce();
|
||||
const firstCall = baseStreamFn.mock.calls[0];
|
||||
const firstModel = firstCall?.[0];
|
||||
expect(firstModel).toMatchObject({
|
||||
compat: {
|
||||
openRouterRouting: {
|
||||
order: ["moonshot"],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
141
openclaw/extensions/openrouter/index.ts
Normal file
141
openclaw/extensions/openrouter/index.ts
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
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 { 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 { wrapOpenRouterProviderStream } from "./stream.js";
|
||||
|
||||
const PROVIDER_ID = "openrouter";
|
||||
const OPENROUTER_DEFAULT_MAX_TOKENS = 8192;
|
||||
const OPENROUTER_CACHE_TTL_MODEL_PREFIXES = [
|
||||
"anthropic/",
|
||||
"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,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
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);
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import {
|
||||
describeImageWithModel,
|
||||
describeImagesWithModel,
|
||||
type MediaUnderstandingProvider,
|
||||
} from "openclaw/plugin-sdk/media-understanding";
|
||||
|
||||
export const openrouterMediaUnderstandingProvider: MediaUnderstandingProvider = {
|
||||
id: "openrouter",
|
||||
capabilities: ["image"],
|
||||
defaultModels: { image: "auto" },
|
||||
describeImage: describeImageWithModel,
|
||||
describeImages: describeImagesWithModel,
|
||||
};
|
||||
27
openclaw/extensions/openrouter/onboard.test.ts
Normal file
27
openclaw/extensions/openrouter/onboard.test.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { describe, it } from "vitest";
|
||||
import {
|
||||
expectProviderOnboardAllowlistAlias,
|
||||
expectProviderOnboardPrimaryAndFallbacks,
|
||||
} from "../../test/helpers/plugins/provider-onboard.js";
|
||||
import {
|
||||
applyOpenrouterConfig,
|
||||
applyOpenrouterProviderConfig,
|
||||
OPENROUTER_DEFAULT_MODEL_REF,
|
||||
} from "./onboard.js";
|
||||
|
||||
describe("openrouter onboard", () => {
|
||||
it("adds allowlist entry and preserves alias", () => {
|
||||
expectProviderOnboardAllowlistAlias({
|
||||
applyProviderConfig: applyOpenrouterProviderConfig,
|
||||
modelRef: OPENROUTER_DEFAULT_MODEL_REF,
|
||||
alias: "Router",
|
||||
});
|
||||
});
|
||||
|
||||
it("sets primary model and preserves existing model fallbacks", () => {
|
||||
expectProviderOnboardPrimaryAndFallbacks({
|
||||
applyConfig: applyOpenrouterConfig,
|
||||
modelRef: OPENROUTER_DEFAULT_MODEL_REF,
|
||||
});
|
||||
});
|
||||
});
|
||||
32
openclaw/extensions/openrouter/onboard.ts
Normal file
32
openclaw/extensions/openrouter/onboard.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import {
|
||||
applyAgentDefaultModelPrimary,
|
||||
type OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk/provider-onboard";
|
||||
|
||||
export const OPENROUTER_DEFAULT_MODEL_REF = "openrouter/auto";
|
||||
|
||||
export function applyOpenrouterProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
const models = { ...cfg.agents?.defaults?.models };
|
||||
models[OPENROUTER_DEFAULT_MODEL_REF] = {
|
||||
...models[OPENROUTER_DEFAULT_MODEL_REF],
|
||||
alias: models[OPENROUTER_DEFAULT_MODEL_REF]?.alias ?? "OpenRouter",
|
||||
};
|
||||
|
||||
return {
|
||||
...cfg,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
models,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function applyOpenrouterConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
return applyAgentDefaultModelPrimary(
|
||||
applyOpenrouterProviderConfig(cfg),
|
||||
OPENROUTER_DEFAULT_MODEL_REF,
|
||||
);
|
||||
}
|
||||
31
openclaw/extensions/openrouter/openclaw.plugin.json
Normal file
31
openclaw/extensions/openrouter/openclaw.plugin.json
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"id": "openrouter",
|
||||
"enabledByDefault": true,
|
||||
"providers": ["openrouter"],
|
||||
"providerAuthEnvVars": {
|
||||
"openrouter": ["OPENROUTER_API_KEY"]
|
||||
},
|
||||
"providerAuthChoices": [
|
||||
{
|
||||
"provider": "openrouter",
|
||||
"method": "api-key",
|
||||
"choiceId": "openrouter-api-key",
|
||||
"choiceLabel": "OpenRouter API key",
|
||||
"groupId": "openrouter",
|
||||
"groupLabel": "OpenRouter",
|
||||
"groupHint": "API key",
|
||||
"optionKey": "openrouterApiKey",
|
||||
"cliFlag": "--openrouter-api-key",
|
||||
"cliOption": "--openrouter-api-key <key>",
|
||||
"cliDescription": "OpenRouter API key"
|
||||
}
|
||||
],
|
||||
"contracts": {
|
||||
"mediaUnderstandingProviders": ["openrouter"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {}
|
||||
}
|
||||
}
|
||||
59
openclaw/extensions/openrouter/openrouter.live.test.ts
Normal file
59
openclaw/extensions/openrouter/openrouter.live.test.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
|
||||
import OpenAI from "openai";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
registerProviderPlugin,
|
||||
requireRegisteredProvider,
|
||||
} from "../../test/helpers/plugins/provider-registration.js";
|
||||
import plugin from "./index.js";
|
||||
|
||||
const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY ?? "";
|
||||
const LIVE_MODEL_ID =
|
||||
process.env.OPENCLAW_LIVE_OPENROUTER_PLUGIN_MODEL?.trim() || "openai/gpt-5.4-nano";
|
||||
const liveEnabled = OPENROUTER_API_KEY.trim().length > 0 && process.env.OPENCLAW_LIVE_TEST === "1";
|
||||
const describeLive = liveEnabled ? describe : describe.skip;
|
||||
const ModelRegistryCtor = ModelRegistry as unknown as {
|
||||
new (authStorage: AuthStorage, modelsJsonPath?: string): ModelRegistry;
|
||||
};
|
||||
|
||||
const registerOpenRouterPlugin = async () =>
|
||||
registerProviderPlugin({
|
||||
plugin,
|
||||
id: "openrouter",
|
||||
name: "OpenRouter Provider",
|
||||
});
|
||||
|
||||
describeLive("openrouter plugin live", () => {
|
||||
it("registers an OpenRouter provider that can complete a live request", async () => {
|
||||
const { providers } = await registerOpenRouterPlugin();
|
||||
const provider = requireRegisteredProvider(providers, "openrouter");
|
||||
|
||||
const resolved = provider.resolveDynamicModel?.({
|
||||
provider: "openrouter",
|
||||
modelId: LIVE_MODEL_ID,
|
||||
modelRegistry: new ModelRegistryCtor(AuthStorage.inMemory()),
|
||||
});
|
||||
if (!resolved) {
|
||||
throw new Error(`openrouter provider did not resolve ${LIVE_MODEL_ID}`);
|
||||
}
|
||||
|
||||
expect(resolved).toMatchObject({
|
||||
provider: "openrouter",
|
||||
id: LIVE_MODEL_ID,
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
});
|
||||
|
||||
const client = new OpenAI({
|
||||
apiKey: OPENROUTER_API_KEY,
|
||||
baseURL: resolved.baseUrl,
|
||||
});
|
||||
const response = await client.chat.completions.create({
|
||||
model: resolved.id,
|
||||
messages: [{ role: "user", content: "Reply with exactly OK." }],
|
||||
max_tokens: 16,
|
||||
});
|
||||
|
||||
expect(response.choices[0]?.message?.content?.trim()).toMatch(/^OK[.!]?$/);
|
||||
}, 30_000);
|
||||
});
|
||||
15
openclaw/extensions/openrouter/package.json
Normal file
15
openclaw/extensions/openrouter/package.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"name": "@openclaw/openrouter-provider",
|
||||
"version": "2026.4.20",
|
||||
"private": true,
|
||||
"description": "OpenClaw OpenRouter provider plugin",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*"
|
||||
},
|
||||
"openclaw": {
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
64
openclaw/extensions/openrouter/provider-catalog.ts
Normal file
64
openclaw/extensions/openrouter/provider-catalog.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
|
||||
export const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
|
||||
const OPENROUTER_LEGACY_BASE_URL = "https://openrouter.ai/v1";
|
||||
const OPENROUTER_DEFAULT_MODEL_ID = "auto";
|
||||
const OPENROUTER_DEFAULT_CONTEXT_WINDOW = 200000;
|
||||
const OPENROUTER_DEFAULT_MAX_TOKENS = 8192;
|
||||
const OPENROUTER_DEFAULT_COST = {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
};
|
||||
|
||||
function normalizeBaseUrl(baseUrl: string | undefined): string {
|
||||
return (baseUrl ?? "").trim().replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
export function normalizeOpenRouterBaseUrl(baseUrl: string | undefined): string | undefined {
|
||||
const normalized = normalizeBaseUrl(baseUrl);
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
if (normalized === OPENROUTER_BASE_URL || normalized === OPENROUTER_LEGACY_BASE_URL) {
|
||||
return OPENROUTER_BASE_URL;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function buildOpenrouterProvider(): ModelProviderConfig {
|
||||
return {
|
||||
baseUrl: OPENROUTER_BASE_URL,
|
||||
api: "openai-completions",
|
||||
models: [
|
||||
{
|
||||
id: OPENROUTER_DEFAULT_MODEL_ID,
|
||||
name: "OpenRouter Auto",
|
||||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
cost: OPENROUTER_DEFAULT_COST,
|
||||
contextWindow: OPENROUTER_DEFAULT_CONTEXT_WINDOW,
|
||||
maxTokens: OPENROUTER_DEFAULT_MAX_TOKENS,
|
||||
},
|
||||
{
|
||||
id: "openrouter/hunter-alpha",
|
||||
name: "Hunter Alpha",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: OPENROUTER_DEFAULT_COST,
|
||||
contextWindow: 1048576,
|
||||
maxTokens: 65536,
|
||||
},
|
||||
{
|
||||
id: "openrouter/healer-alpha",
|
||||
name: "Healer Alpha",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: OPENROUTER_DEFAULT_COST,
|
||||
contextWindow: 262144,
|
||||
maxTokens: 65536,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
26
openclaw/extensions/openrouter/provider-contract-api.ts
Normal file
26
openclaw/extensions/openrouter/provider-contract-api.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import type { ProviderPlugin } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
|
||||
export function createOpenrouterProvider(): ProviderPlugin {
|
||||
return {
|
||||
id: "openrouter",
|
||||
label: "OpenRouter",
|
||||
docsPath: "/providers/models",
|
||||
envVars: ["OPENROUTER_API_KEY"],
|
||||
auth: [
|
||||
{
|
||||
id: "api-key",
|
||||
kind: "api_key",
|
||||
label: "OpenRouter API key",
|
||||
hint: "API key",
|
||||
run: async () => ({ profiles: [] }),
|
||||
wizard: {
|
||||
choiceId: "openrouter-api-key",
|
||||
choiceLabel: "OpenRouter API key",
|
||||
groupId: "openrouter",
|
||||
groupLabel: "OpenRouter",
|
||||
groupHint: "API key",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
32
openclaw/extensions/openrouter/register.runtime.ts
Normal file
32
openclaw/extensions/openrouter/register.runtime.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key";
|
||||
import {
|
||||
buildProviderReplayFamilyHooks,
|
||||
DEFAULT_CONTEXT_TOKENS,
|
||||
} from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import {
|
||||
buildProviderStreamFamilyHooks,
|
||||
createOpenRouterSystemCacheWrapper,
|
||||
createOpenRouterWrapper,
|
||||
getOpenRouterModelCapabilities,
|
||||
isProxyReasoningUnsupported,
|
||||
loadOpenRouterModelCapabilities,
|
||||
} from "openclaw/plugin-sdk/provider-stream-family";
|
||||
import { openrouterMediaUnderstandingProvider } from "./media-understanding-provider.js";
|
||||
import { applyOpenrouterConfig, OPENROUTER_DEFAULT_MODEL_REF } from "./onboard.js";
|
||||
import { buildOpenrouterProvider } from "./provider-catalog.js";
|
||||
|
||||
export {
|
||||
applyOpenrouterConfig,
|
||||
buildOpenrouterProvider,
|
||||
buildProviderReplayFamilyHooks,
|
||||
buildProviderStreamFamilyHooks,
|
||||
createOpenRouterSystemCacheWrapper,
|
||||
createOpenRouterWrapper,
|
||||
createProviderApiKeyAuthMethod,
|
||||
DEFAULT_CONTEXT_TOKENS,
|
||||
getOpenRouterModelCapabilities,
|
||||
isProxyReasoningUnsupported,
|
||||
loadOpenRouterModelCapabilities,
|
||||
OPENROUTER_DEFAULT_MODEL_REF,
|
||||
openrouterMediaUnderstandingProvider,
|
||||
};
|
||||
50
openclaw/extensions/openrouter/stream.ts
Normal file
50
openclaw/extensions/openrouter/stream.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import type { StreamFn } from "@mariozechner/pi-agent-core";
|
||||
import type { ProviderWrapStreamFnContext } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { OPENROUTER_THINKING_STREAM_HOOKS } from "openclaw/plugin-sdk/provider-stream-family";
|
||||
|
||||
function injectOpenRouterRouting(
|
||||
baseStreamFn: StreamFn | undefined,
|
||||
providerRouting?: Record<string, unknown>,
|
||||
): StreamFn | undefined {
|
||||
if (!providerRouting) {
|
||||
return baseStreamFn;
|
||||
}
|
||||
return (model, context, options) =>
|
||||
(
|
||||
baseStreamFn ??
|
||||
((nextModel) => {
|
||||
throw new Error(
|
||||
`OpenRouter routing wrapper requires an underlying streamFn for ${nextModel.id}.`,
|
||||
);
|
||||
})
|
||||
)(
|
||||
{
|
||||
...model,
|
||||
compat: { ...model.compat, openRouterRouting: providerRouting },
|
||||
} as typeof model,
|
||||
context,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
export function wrapOpenRouterProviderStream(
|
||||
ctx: ProviderWrapStreamFnContext,
|
||||
): StreamFn | null | undefined {
|
||||
const providerRouting =
|
||||
ctx.extraParams?.provider != null && typeof ctx.extraParams.provider === "object"
|
||||
? (ctx.extraParams.provider as Record<string, unknown>)
|
||||
: undefined;
|
||||
const routedStreamFn = providerRouting
|
||||
? injectOpenRouterRouting(ctx.streamFn, providerRouting)
|
||||
: ctx.streamFn;
|
||||
const wrapStreamFn = OPENROUTER_THINKING_STREAM_HOOKS.wrapStreamFn ?? undefined;
|
||||
if (!wrapStreamFn) {
|
||||
return routedStreamFn;
|
||||
}
|
||||
return (
|
||||
wrapStreamFn({
|
||||
...ctx,
|
||||
streamFn: routedStreamFn,
|
||||
}) ?? undefined
|
||||
);
|
||||
}
|
||||
1
openclaw/extensions/openrouter/test-api.ts
Normal file
1
openclaw/extensions/openrouter/test-api.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { openrouterMediaUnderstandingProvider } from "./media-understanding-provider.js";
|
||||
16
openclaw/extensions/openrouter/tsconfig.json
Normal file
16
openclaw/extensions/openrouter/tsconfig.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"extends": "../tsconfig.package-boundary.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": ["./*.ts", "./src/**/*.ts"],
|
||||
"exclude": [
|
||||
"./**/*.test.ts",
|
||||
"./dist/**",
|
||||
"./node_modules/**",
|
||||
"./src/test-support/**",
|
||||
"./src/**/*test-helpers.ts",
|
||||
"./src/**/*test-harness.ts",
|
||||
"./src/**/*test-support.ts"
|
||||
]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue