diff --git a/extensions/openai/openai-codex-provider.ts b/extensions/openai/openai-codex-provider.ts index 8d86e436fd4..7782a593894 100644 --- a/extensions/openai/openai-codex-provider.ts +++ b/extensions/openai/openai-codex-provider.ts @@ -317,7 +317,7 @@ export function buildOpenAICodexProviderPlugin(): ProviderPlugin { transport: "auto", }; }, - wrapStreamFn: (ctx) => OPENAI_RESPONSES_STREAM_HOOKS.wrapStreamFn?.(ctx), + ...OPENAI_RESPONSES_STREAM_HOOKS, resolveTransportTurnState: (ctx) => resolveOpenAITransportTurnState(ctx), resolveWebSocketSessionPolicy: (ctx) => resolveOpenAIWebSocketSessionPolicy(ctx), resolveReasoningOutputMode: () => "native", diff --git a/extensions/openai/openai-provider.ts b/extensions/openai/openai-provider.ts index 4c7f093449c..59cc7404722 100644 --- a/extensions/openai/openai-provider.ts +++ b/extensions/openai/openai-provider.ts @@ -256,7 +256,7 @@ export function buildOpenAIProvider(): ProviderPlugin { ...(hasExplicitWarmup ? {} : { openaiWsWarmup: true }), }; }, - wrapStreamFn: (ctx) => OPENAI_RESPONSES_STREAM_HOOKS.wrapStreamFn?.(ctx), + ...OPENAI_RESPONSES_STREAM_HOOKS, matchesContextOverflowError: ({ errorMessage }) => /content_filter.*(?:prompt|input).*(?:too long|exceed)/i.test(errorMessage), resolveTransportTurnState: (ctx) => resolveOpenAITransportTurnState(ctx), diff --git a/extensions/openrouter/index.test.ts b/extensions/openrouter/index.test.ts index a0d63058c27..e1edc1b2f9c 100644 --- a/extensions/openrouter/index.test.ts +++ b/extensions/openrouter/index.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { registerSingleProviderPlugin } from "../../test/helpers/plugins/plugin-registration.js"; import openrouterPlugin from "./index.js"; @@ -14,4 +14,41 @@ describe("openrouter provider hooks", () => { } as never), ).toBe("native"); }); + + it("injects provider routing into compat before applying stream wrappers", async () => { + const provider = await registerSingleProviderPlugin(openrouterPlugin); + const baseStreamFn = vi.fn(() => ({ 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); + + wrapped?.( + { + provider: "openrouter", + api: "openai-completions", + id: "openai/gpt-5.4", + compat: {}, + } as never, + { messages: [] } as never, + {}, + ); + + expect(baseStreamFn).toHaveBeenCalledOnce(); + expect(baseStreamFn.mock.calls[0]?.[0]).toMatchObject({ + compat: { + openRouterRouting: { + order: ["moonshot"], + }, + }, + }); + }); }); diff --git a/extensions/openrouter/index.ts b/extensions/openrouter/index.ts index 25c267954af..4e9d82ed6b5 100644 --- a/extensions/openrouter/index.ts +++ b/extensions/openrouter/index.ts @@ -3,6 +3,7 @@ import { definePluginEntry, type ProviderResolveDynamicModelContext, type ProviderRuntimeModel, + type ProviderWrapStreamFnContext, } from "openclaw/plugin-sdk/plugin-entry"; const PROVIDER_ID = "openrouter"; @@ -80,6 +81,22 @@ export default definePluginEntry({ ); } + function wrapOpenRouterProviderStream( + ctx: ProviderWrapStreamFnContext, + ): StreamFn | undefined { + const providerRouting = + ctx.extraParams?.provider != null && typeof ctx.extraParams.provider === "object" + ? (ctx.extraParams.provider as Record) + : undefined; + const routedStreamFn = providerRouting + ? injectOpenRouterRouting(ctx.streamFn, providerRouting) + : ctx.streamFn; + return OPENROUTER_THINKING_STREAM_HOOKS.wrapStreamFn?.({ + ...ctx, + streamFn: routedStreamFn, + }); + } + function isOpenRouterCacheTtlModel(modelId: string): boolean { return OPENROUTER_CACHE_TTL_MODEL_PREFIXES.some((prefix) => modelId.startsWith(prefix)); } @@ -133,19 +150,7 @@ export default definePluginEntry({ ...PASSTHROUGH_GEMINI_REPLAY_HOOKS, resolveReasoningOutputMode: () => "native", isModernModelRef: () => true, - wrapStreamFn: (ctx) => { - const providerRouting = - ctx.extraParams?.provider != null && typeof ctx.extraParams.provider === "object" - ? (ctx.extraParams.provider as Record) - : undefined; - const routedStreamFn = providerRouting - ? injectOpenRouterRouting(ctx.streamFn, providerRouting) - : ctx.streamFn; - return OPENROUTER_THINKING_STREAM_HOOKS.wrapStreamFn?.({ - ...ctx, - streamFn: routedStreamFn, - }); - }, + wrapStreamFn: wrapOpenRouterProviderStream, isCacheTtlEligible: (ctx) => isOpenRouterCacheTtlModel(ctx.modelId), }); api.registerMediaUnderstandingProvider(openrouterMediaUnderstandingProvider);