mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
fix(openrouter): retire stealth model catalog entries
This commit is contained in:
@@ -25,6 +25,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Nodes/CLI: add `openclaw nodes remove --node <id|name|ip>` and `node.pair.remove` so stale gateway-owned node pairing records can be cleaned without hand-editing state files. Thanks @openclaw.
|
||||
- Docker: install the CA certificate bundle in the slim runtime image so HTTPS calls from containerized gateways no longer fail TLS setup after the `bookworm-slim` base switch. Fixes #72787. Thanks @ryuhaneul.
|
||||
- Providers/OpenRouter: remove retired Hunter Alpha and Healer Alpha static catalog rows and disable proxy reasoning injection for stale Hunter Alpha configs, so replies are not hidden when OpenRouter returns answer text in reasoning fields. Fixes #43942. Thanks @EvanDataForge.
|
||||
- Providers/reasoning: let Groq and LM Studio declare provider-native reasoning effort values, so Qwen thinking models receive `none`/`default` or `off`/`on` instead of OpenAI-only `low`/`medium` values. Fixes #32638. Thanks @Aqu1bp, @mgoulart, @Norpps, and @BSTail.
|
||||
- Local models: default custom providers with only `baseUrl` to the Chat Completions adapter and trust loopback model requests automatically, so local OpenAI-compatible proxies receive `/v1/chat/completions` without timing out. Fixes #40024. Thanks @parachuteshe.
|
||||
- Channels/message tool: surface Discord, Slack, and Mattermost `user:`/`channel:` target syntax in the shared message target schema and Discord ambiguity errors, so DM sends by numeric id stop burning retries before finding `user:<id>`. Fixes #72401. Thanks @garyd9, @hclsys, and @praveen9354.
|
||||
|
||||
@@ -53,12 +53,10 @@ available providers and models, see [/concepts/model-providers](/concepts/model-
|
||||
|
||||
Bundled fallback examples:
|
||||
|
||||
| Model ref | Notes |
|
||||
| ------------------------------------ | ----------------------------- |
|
||||
| `openrouter/auto` | OpenRouter automatic routing |
|
||||
| `openrouter/moonshotai/kimi-k2.6` | Kimi K2.6 via MoonshotAI |
|
||||
| `openrouter/openrouter/healer-alpha` | OpenRouter Healer Alpha route |
|
||||
| `openrouter/openrouter/hunter-alpha` | OpenRouter Hunter Alpha route |
|
||||
| Model ref | Notes |
|
||||
| --------------------------------- | ---------------------------- |
|
||||
| `openrouter/auto` | OpenRouter automatic routing |
|
||||
| `openrouter/moonshotai/kimi-k2.6` | Kimi K2.6 via MoonshotAI |
|
||||
|
||||
## Image generation
|
||||
|
||||
@@ -136,7 +134,9 @@ does **not** inject those OpenRouter-specific headers or Anthropic cache markers
|
||||
<Accordion title="Thinking / reasoning injection">
|
||||
On supported non-`auto` routes, OpenClaw maps the selected thinking level to
|
||||
OpenRouter proxy reasoning payloads. Unsupported model hints and
|
||||
`openrouter/auto` skip that reasoning injection.
|
||||
`openrouter/auto` skip that reasoning injection. Hunter Alpha also skips
|
||||
proxy reasoning for stale configured model refs because OpenRouter could
|
||||
return final answer text in reasoning fields for that retired route.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="OpenAI-only request shaping">
|
||||
|
||||
@@ -28,6 +28,7 @@ title: "Thinking levels"
|
||||
- Anthropic Claude Opus 4.7 also exposes `/think max`; it maps to the same provider-owned max effort path.
|
||||
- Ollama thinking-capable models expose `/think low|medium|high|max`; `max` maps to native `think: "high"` because Ollama's native API accepts `low`, `medium`, and `high` effort strings.
|
||||
- OpenAI GPT models map `/think` through model-specific Responses API effort support. `/think off` sends `reasoning.effort: "none"` only when the target model supports it; otherwise OpenClaw omits the disabled reasoning payload instead of sending an unsupported value.
|
||||
- Stale configured OpenRouter Hunter Alpha refs skip proxy reasoning injection because that retired route could return final answer text through reasoning fields.
|
||||
- Google Gemini maps `/think adaptive` to Gemini's provider-owned dynamic thinking. Gemini 3 requests omit a fixed `thinkingLevel`, while Gemini 2.5 requests send `thinkingBudget: -1`; fixed levels still map to the closest Gemini `thinkingLevel` or budget for that model family.
|
||||
- MiniMax (`minimax/*`) on the Anthropic-compatible streaming path defaults to `thinking: { type: "disabled" }` unless you explicitly set thinking in model params or request params. This avoids leaked `reasoning_content` deltas from MiniMax's non-native Anthropic stream format.
|
||||
- Z.AI (`zai/*`) only supports binary thinking (`on`/`off`). Any non-`off` level is treated as `on` (mapped to `low`).
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
export { buildOpenRouterImageGenerationProvider } from "./image-generation-provider.js";
|
||||
export { buildOpenrouterProvider } from "./provider-catalog.js";
|
||||
export {
|
||||
buildOpenrouterProvider,
|
||||
isOpenRouterProxyReasoningUnsupportedModel,
|
||||
} from "./provider-catalog.js";
|
||||
export { buildOpenRouterSpeechProvider } from "./speech-provider.js";
|
||||
export {
|
||||
applyOpenrouterConfig,
|
||||
|
||||
@@ -3,7 +3,10 @@ import { registerSingleProviderPlugin } from "../../test/helpers/plugins/plugin-
|
||||
import { registerProviderPlugin } from "../../test/helpers/plugins/provider-registration.js";
|
||||
import { expectPassthroughReplayPolicy } from "../../test/helpers/provider-replay-policy.ts";
|
||||
import openrouterPlugin from "./index.js";
|
||||
import { buildOpenrouterProvider } from "./provider-catalog.js";
|
||||
import {
|
||||
buildOpenrouterProvider,
|
||||
isOpenRouterProxyReasoningUnsupportedModel,
|
||||
} from "./provider-catalog.js";
|
||||
|
||||
describe("openrouter provider hooks", () => {
|
||||
it("registers OpenRouter speech alongside model and media providers", async () => {
|
||||
@@ -26,6 +29,18 @@ describe("openrouter provider hooks", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("does not include retired stealth models in the bundled catalog", () => {
|
||||
expect(buildOpenrouterProvider().models?.map((model) => model.id)).not.toEqual(
|
||||
expect.arrayContaining(["openrouter/hunter-alpha", "openrouter/healer-alpha"]),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps stale Hunter Alpha configs out of OpenRouter proxy reasoning", () => {
|
||||
expect(isOpenRouterProxyReasoningUnsupportedModel("openrouter/hunter-alpha")).toBe(true);
|
||||
expect(isOpenRouterProxyReasoningUnsupportedModel("openrouter/hunter-alpha:free")).toBe(true);
|
||||
expect(isOpenRouterProxyReasoningUnsupportedModel("openrouter/healer-alpha")).toBe(false);
|
||||
});
|
||||
|
||||
it("owns passthrough-gemini replay policy for Gemini-backed models", async () => {
|
||||
await expectPassthroughReplayPolicy({
|
||||
plugin: openrouterPlugin,
|
||||
@@ -88,6 +103,26 @@ describe("openrouter provider hooks", () => {
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
});
|
||||
|
||||
expect(
|
||||
provider.normalizeResolvedModel?.({
|
||||
provider: "openrouter",
|
||||
model: {
|
||||
provider: "openrouter",
|
||||
id: "openrouter/hunter-alpha",
|
||||
name: "Hunter Alpha",
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 1_048_576,
|
||||
maxTokens: 65_536,
|
||||
},
|
||||
} as never),
|
||||
).toMatchObject({
|
||||
reasoning: false,
|
||||
});
|
||||
|
||||
expect(
|
||||
provider.normalizeTransport?.({
|
||||
provider: "openrouter",
|
||||
@@ -141,4 +176,43 @@ describe("openrouter provider hooks", () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("does not inject OpenRouter reasoning for Hunter Alpha", async () => {
|
||||
const provider = await registerSingleProviderPlugin(openrouterPlugin);
|
||||
let capturedPayload: Record<string, unknown> | undefined;
|
||||
const baseStreamFn = vi.fn(
|
||||
(
|
||||
...args: Parameters<import("@mariozechner/pi-agent-core").StreamFn>
|
||||
): ReturnType<import("@mariozechner/pi-agent-core").StreamFn> => {
|
||||
void args[2]?.onPayload?.({}, args[0]);
|
||||
return { async *[Symbol.asyncIterator]() {} } as never;
|
||||
},
|
||||
);
|
||||
|
||||
const wrapped = provider.wrapStreamFn?.({
|
||||
provider: "openrouter",
|
||||
modelId: "openrouter/hunter-alpha",
|
||||
streamFn: baseStreamFn,
|
||||
thinkingLevel: "high",
|
||||
} as never);
|
||||
|
||||
void wrapped?.(
|
||||
{
|
||||
provider: "openrouter",
|
||||
api: "openai-completions",
|
||||
id: "openrouter/hunter-alpha",
|
||||
compat: {},
|
||||
} as never,
|
||||
{ messages: [] } as never,
|
||||
{
|
||||
onPayload: (payload: unknown) => {
|
||||
capturedPayload = payload as Record<string, unknown>;
|
||||
return payload;
|
||||
},
|
||||
} as never,
|
||||
);
|
||||
|
||||
expect(capturedPayload).toEqual({});
|
||||
expect(baseStreamFn).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ import { openrouterMediaUnderstandingProvider } from "./media-understanding-prov
|
||||
import { applyOpenrouterConfig, OPENROUTER_DEFAULT_MODEL_REF } from "./onboard.js";
|
||||
import {
|
||||
buildOpenrouterProvider,
|
||||
isOpenRouterProxyReasoningUnsupportedModel,
|
||||
normalizeOpenRouterBaseUrl,
|
||||
OPENROUTER_BASE_URL,
|
||||
} from "./provider-catalog.js";
|
||||
@@ -35,12 +36,17 @@ const OPENROUTER_CACHE_TTL_MODEL_PREFIXES = [
|
||||
|
||||
function normalizeOpenRouterResolvedModel<T extends ProviderRuntimeModel>(model: T): T | undefined {
|
||||
const normalizedBaseUrl = normalizeOpenRouterBaseUrl(model.baseUrl);
|
||||
if (!normalizedBaseUrl || normalizedBaseUrl === model.baseUrl) {
|
||||
const reasoning = isOpenRouterProxyReasoningUnsupportedModel(model.id) ? false : model.reasoning;
|
||||
if (
|
||||
(!normalizedBaseUrl || normalizedBaseUrl === model.baseUrl) &&
|
||||
reasoning === model.reasoning
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
...model,
|
||||
baseUrl: normalizedBaseUrl,
|
||||
...(normalizedBaseUrl ? { baseUrl: normalizedBaseUrl } : {}),
|
||||
reasoning,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -59,7 +65,9 @@ export default definePluginEntry({
|
||||
api: "openai-completions",
|
||||
provider: PROVIDER_ID,
|
||||
baseUrl: OPENROUTER_BASE_URL,
|
||||
reasoning: capabilities?.reasoning ?? false,
|
||||
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,
|
||||
|
||||
@@ -11,6 +11,7 @@ const OPENROUTER_DEFAULT_COST = {
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
};
|
||||
const OPENROUTER_PROXY_REASONING_UNSUPPORTED_MODEL_IDS = new Set(["openrouter/hunter-alpha"]);
|
||||
const OPENROUTER_KIMI_K2_6_COST = {
|
||||
input: 0.8,
|
||||
output: 3.5,
|
||||
@@ -33,6 +34,17 @@ export function normalizeOpenRouterBaseUrl(baseUrl: string | undefined): string
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function isOpenRouterProxyReasoningUnsupportedModel(modelId: string | undefined): boolean {
|
||||
const normalized = (modelId ?? "").trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
OPENROUTER_PROXY_REASONING_UNSUPPORTED_MODEL_IDS.has(normalized) ||
|
||||
normalized.startsWith("openrouter/hunter-alpha:")
|
||||
);
|
||||
}
|
||||
|
||||
export function buildOpenrouterProvider(): ModelProviderConfig {
|
||||
return {
|
||||
baseUrl: OPENROUTER_BASE_URL,
|
||||
@@ -47,24 +59,6 @@ export function buildOpenrouterProvider(): ModelProviderConfig {
|
||||
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,
|
||||
},
|
||||
{
|
||||
id: "moonshotai/kimi-k2.6",
|
||||
name: "MoonshotAI: Kimi K2.6",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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";
|
||||
import { isOpenRouterProxyReasoningUnsupportedModel } from "./provider-catalog.js";
|
||||
|
||||
function injectOpenRouterRouting(
|
||||
baseStreamFn: StreamFn | undefined,
|
||||
@@ -45,6 +46,9 @@ export function wrapOpenRouterProviderStream(
|
||||
wrapStreamFn({
|
||||
...ctx,
|
||||
streamFn: routedStreamFn,
|
||||
thinkingLevel: isOpenRouterProxyReasoningUnsupportedModel(ctx.modelId)
|
||||
? undefined
|
||||
: ctx.thinkingLevel,
|
||||
}) ?? undefined
|
||||
);
|
||||
}
|
||||
|
||||
@@ -852,6 +852,34 @@ describe("openai transport stream", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not build OpenRouter reasoning params for Hunter Alpha when reasoning is disabled", () => {
|
||||
const params = buildOpenAICompletionsParams(
|
||||
{
|
||||
id: "openrouter/hunter-alpha",
|
||||
name: "Hunter Alpha",
|
||||
api: "openai-completions",
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 1_048_576,
|
||||
maxTokens: 65_536,
|
||||
} satisfies Model<"openai-completions">,
|
||||
{
|
||||
systemPrompt: "system",
|
||||
messages: [],
|
||||
tools: [],
|
||||
} as never,
|
||||
{
|
||||
reasoningEffort: "high",
|
||||
} as never,
|
||||
) as { reasoning?: unknown; reasoning_effort?: unknown };
|
||||
|
||||
expect(params).not.toHaveProperty("reasoning");
|
||||
expect(params).not.toHaveProperty("reasoning_effort");
|
||||
});
|
||||
|
||||
it("uses system role instead of developer for responses providers that disable developer role", () => {
|
||||
const params = buildOpenAIResponsesParams(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user