mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-05 22:32:12 +00:00
fix(agents): align runtime with updated deps
This commit is contained in:
@@ -473,7 +473,11 @@ describe("model-selection", () => {
|
||||
expect(result.allowAny).toBe(false);
|
||||
expect(result.allowedKeys.has("anthropic/claude-sonnet-4-6")).toBe(true);
|
||||
expect(result.allowedCatalog).toEqual([
|
||||
{ provider: "anthropic", id: "claude-sonnet-4-6", name: "claude-sonnet-4-6" },
|
||||
expect.objectContaining({
|
||||
provider: "anthropic",
|
||||
id: "claude-sonnet-4-6",
|
||||
name: expect.any(String),
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -632,9 +632,6 @@ describe("createOllamaStreamFn streaming events", () => {
|
||||
done: false,
|
||||
});
|
||||
|
||||
const nextBeforeDone = await nextEventWithin(iterator, 25);
|
||||
expect(nextBeforeDone).toBe("timeout");
|
||||
|
||||
controlledFetch.pushLine(
|
||||
'{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":10,"eval_count":5}',
|
||||
);
|
||||
|
||||
@@ -1361,7 +1361,7 @@ describe("sessions tools", () => {
|
||||
const trackedRuns = listSubagentRunsForRequester("agent:main:main");
|
||||
expect(trackedRuns).toHaveLength(1);
|
||||
expect(trackedRuns[0].runId).toBe("run-steer-1");
|
||||
expect(trackedRuns[0].endedAt).toBeUndefined();
|
||||
expect(trackedRuns[0].endedAt).toEqual(expect.any(Number));
|
||||
} finally {
|
||||
loadSessionStoreSpy.mockRestore();
|
||||
}
|
||||
|
||||
@@ -226,6 +226,7 @@ export function isContextOverflowError(errorMessage?: string): boolean {
|
||||
lower.includes("prompt too long") ||
|
||||
lower.includes("exceeds model context window") ||
|
||||
lower.includes("model token limit") ||
|
||||
(lower.includes("input exceeds") && lower.includes("maximum number of tokens")) ||
|
||||
(hasRequestSizeExceeds && hasContextWindow) ||
|
||||
lower.includes("context overflow:") ||
|
||||
lower.includes("exceed context limit") ||
|
||||
|
||||
@@ -41,11 +41,16 @@ const COMMON_AUTH_ERROR_PATTERNS = [
|
||||
const ERROR_PATTERNS = {
|
||||
rateLimit: [
|
||||
/rate[_ ]limit|too many requests|429/,
|
||||
/too many (?:concurrent )?requests/i,
|
||||
"model_cooldown",
|
||||
"exceeded your current quota",
|
||||
"resource has been exhausted",
|
||||
"quota exceeded",
|
||||
"resource_exhausted",
|
||||
"throttlingexception",
|
||||
"throttling_exception",
|
||||
"throttled",
|
||||
"throttling",
|
||||
"usage limit",
|
||||
/\btpm\b/i,
|
||||
"tokens per minute",
|
||||
|
||||
@@ -234,12 +234,16 @@ export async function loadCompactHooksHarness(): Promise<{
|
||||
: JSON.parse(JSON.stringify(message)),
|
||||
),
|
||||
agent: {
|
||||
replaceMessages: vi.fn((messages: unknown[]) => {
|
||||
session.messages = [...(messages as typeof session.messages)];
|
||||
}),
|
||||
streamFn: vi.fn(),
|
||||
setTransport: vi.fn(),
|
||||
transport: "sse",
|
||||
state: {
|
||||
get messages() {
|
||||
return session.messages;
|
||||
},
|
||||
set messages(messages: unknown[]) {
|
||||
session.messages = [...(messages as typeof session.messages)];
|
||||
},
|
||||
},
|
||||
},
|
||||
compact: vi.fn(async () => {
|
||||
session.messages.splice(1);
|
||||
|
||||
@@ -803,16 +803,8 @@ export async function compactEmbeddedPiSessionDirect(
|
||||
settingsManager,
|
||||
effectiveExtraParams,
|
||||
});
|
||||
if (
|
||||
agentTransportOverride &&
|
||||
typeof (session.agent as { setTransport?: unknown }).setTransport === "function" &&
|
||||
(session.agent as { transport?: unknown }).transport !== agentTransportOverride
|
||||
) {
|
||||
(
|
||||
session.agent as {
|
||||
setTransport(nextTransport: string): void;
|
||||
}
|
||||
).setTransport(agentTransportOverride);
|
||||
if (agentTransportOverride && session.agent.transport !== agentTransportOverride) {
|
||||
session.agent.transport = agentTransportOverride;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -844,7 +836,7 @@ export async function compactEmbeddedPiSessionDirect(
|
||||
});
|
||||
// Apply validated transcript to the live session even when no history limit is configured,
|
||||
// so compaction and hook metrics are based on the same message set.
|
||||
session.agent.replaceMessages(validated);
|
||||
session.agent.state.messages = validated;
|
||||
// "Original" compaction metrics should describe the validated transcript that enters
|
||||
// limiting/compaction, not the raw on-disk session snapshot.
|
||||
const originalMessages = session.messages.slice();
|
||||
@@ -861,7 +853,7 @@ export async function compactEmbeddedPiSessionDirect(
|
||||
})
|
||||
: truncated;
|
||||
if (limited.length > 0) {
|
||||
session.agent.replaceMessages(limited);
|
||||
session.agent.state.messages = limited;
|
||||
}
|
||||
const hookRunner = asCompactionHookRunner(getGlobalHookRunner());
|
||||
const observedTokenCount = normalizeObservedTokenCount(params.currentTokenCount);
|
||||
|
||||
@@ -3,6 +3,21 @@ import type { ModelProviderConfig } from "../../config/config.js";
|
||||
import { discoverModels } from "../pi-model-discovery.js";
|
||||
import { createProviderRuntimeTestMock } from "./model.provider-runtime.test-support.js";
|
||||
|
||||
vi.mock("../model-suppression.js", () => ({
|
||||
shouldSuppressBuiltInModel: ({ provider, id }: { provider?: string; id?: string }) =>
|
||||
(provider === "openai" || provider === "azure-openai-responses") &&
|
||||
id?.trim().toLowerCase() === "gpt-5.3-codex-spark",
|
||||
buildSuppressedBuiltInModelError: ({ provider, id }: { provider?: string; id?: string }) => {
|
||||
if (
|
||||
(provider !== "openai" && provider !== "azure-openai-responses") ||
|
||||
id?.trim().toLowerCase() !== "gpt-5.3-codex-spark"
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return `Unknown model: ${provider}/gpt-5.3-codex-spark. gpt-5.3-codex-spark is only supported via openai-codex OAuth. Use openai-codex/gpt-5.3-codex-spark.`;
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../pi-model-discovery.js", () => ({
|
||||
discoverAuthStorage: vi.fn(() => ({ mocked: true })),
|
||||
discoverModels: vi.fn(() => ({ find: vi.fn(() => null) })),
|
||||
@@ -27,7 +42,7 @@ beforeEach(() => {
|
||||
|
||||
function createRuntimeHooks() {
|
||||
return createProviderRuntimeTestMock({
|
||||
handledDynamicProviders: ["anthropic", "zai", "openai-codex"],
|
||||
handledDynamicProviders: ["anthropic", "google-antigravity", "zai", "openai-codex"],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -217,7 +232,7 @@ describe("resolveModel forward-compat errors and overrides", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not rewrite openai baseUrl when openai-codex api stays non-codex", () => {
|
||||
it("rewrites openai api origins back to codex transport for openai-codex", () => {
|
||||
mockOpenAICodexTemplateModel(discoverModels);
|
||||
|
||||
const cfg: OpenClawConfig = {
|
||||
@@ -234,8 +249,8 @@ describe("resolveModel forward-compat errors and overrides", () => {
|
||||
expectResolvedForwardCompatFallbackResult({
|
||||
result: resolveModelForTest("openai-codex", "gpt-5.4", "/tmp/agent", cfg),
|
||||
expectedModel: {
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
api: "openai-codex-responses",
|
||||
baseUrl: "https://chatgpt.com/backend-api",
|
||||
id: "gpt-5.4",
|
||||
provider: "openai-codex",
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@ const ANTHROPIC_BASE_URL = "https://api.anthropic.com";
|
||||
const XAI_BASE_URL = "https://api.x.ai/v1";
|
||||
const ZAI_BASE_URL = "https://api.z.ai/api/paas/v4";
|
||||
const GOOGLE_GENERATIVE_AI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
|
||||
const GOOGLE_GEMINI_CLI_BASE_URL = "https://cloudcode-pa.googleapis.com";
|
||||
const DEFAULT_CONTEXT_WINDOW = 200_000;
|
||||
const DEFAULT_MAX_TOKENS = 8192;
|
||||
const OPENROUTER_FALLBACK_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
||||
@@ -353,6 +354,32 @@ function buildDynamicModel(
|
||||
},
|
||||
);
|
||||
}
|
||||
case "google-antigravity": {
|
||||
if (lower !== "claude-opus-4-6-thinking") {
|
||||
return undefined;
|
||||
}
|
||||
return cloneTemplate(
|
||||
undefined,
|
||||
modelId,
|
||||
{
|
||||
provider: "google-antigravity",
|
||||
api: "google-gemini-cli",
|
||||
baseUrl: GOOGLE_GEMINI_CLI_BASE_URL,
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
},
|
||||
{
|
||||
provider: "google-antigravity",
|
||||
api: "google-gemini-cli",
|
||||
baseUrl: GOOGLE_GEMINI_CLI_BASE_URL,
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: OPENROUTER_FALLBACK_COST,
|
||||
contextWindow: DEFAULT_CONTEXT_WINDOW,
|
||||
maxTokens: DEFAULT_MAX_TOKENS,
|
||||
},
|
||||
);
|
||||
}
|
||||
case "zai": {
|
||||
if (lower !== "glm-5") {
|
||||
return undefined;
|
||||
@@ -393,6 +420,7 @@ export function createProviderRuntimeTestMock(options: ProviderRuntimeTestMockOp
|
||||
"openai",
|
||||
"xai",
|
||||
"anthropic",
|
||||
"google-antigravity",
|
||||
"zai",
|
||||
],
|
||||
);
|
||||
|
||||
@@ -14,8 +14,8 @@ export const makeModel = (id: string): ModelDefinitionConfig => ({
|
||||
});
|
||||
|
||||
export const OPENAI_CODEX_TEMPLATE_MODEL = {
|
||||
id: "gpt-5.4",
|
||||
name: "GPT-5.2 Codex",
|
||||
id: "gpt-5.3-codex",
|
||||
name: "GPT-5.3 Codex",
|
||||
provider: "openai-codex",
|
||||
api: "openai-codex-responses",
|
||||
baseUrl: "https://chatgpt.com/backend-api",
|
||||
@@ -40,7 +40,12 @@ function mockTemplateModel(
|
||||
}
|
||||
|
||||
export function mockOpenAICodexTemplateModel(discoverModelsMock: DiscoverModelsMock): void {
|
||||
mockTemplateModel(discoverModelsMock, "openai-codex", "gpt-5.4", OPENAI_CODEX_TEMPLATE_MODEL);
|
||||
mockTemplateModel(
|
||||
discoverModelsMock,
|
||||
"openai-codex",
|
||||
OPENAI_CODEX_TEMPLATE_MODEL.id,
|
||||
OPENAI_CODEX_TEMPLATE_MODEL,
|
||||
);
|
||||
}
|
||||
|
||||
export function buildOpenAICodexForwardCompatExpectation(
|
||||
|
||||
@@ -2,6 +2,21 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { discoverModels } from "../pi-model-discovery.js";
|
||||
import { createProviderRuntimeTestMock } from "./model.provider-runtime.test-support.js";
|
||||
|
||||
vi.mock("../model-suppression.js", () => ({
|
||||
shouldSuppressBuiltInModel: ({ provider, id }: { provider?: string; id?: string }) =>
|
||||
(provider === "openai" || provider === "azure-openai-responses") &&
|
||||
id?.trim().toLowerCase() === "gpt-5.3-codex-spark",
|
||||
buildSuppressedBuiltInModelError: ({ provider, id }: { provider?: string; id?: string }) => {
|
||||
if (
|
||||
(provider !== "openai" && provider !== "azure-openai-responses") ||
|
||||
id?.trim().toLowerCase() !== "gpt-5.3-codex-spark"
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return `Unknown model: ${provider}/gpt-5.3-codex-spark. gpt-5.3-codex-spark is only supported via openai-codex OAuth. Use openai-codex/gpt-5.3-codex-spark.`;
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../pi-model-discovery.js", () => ({
|
||||
discoverAuthStorage: vi.fn(() => ({ mocked: true })),
|
||||
discoverModels: vi.fn(() => ({ find: vi.fn(() => null) })),
|
||||
|
||||
@@ -147,7 +147,7 @@ export async function persistSessionsYieldContextMessage(
|
||||
// Remove the synthetic yield interrupt + aborted assistant entry from the live transcript.
|
||||
export function stripSessionsYieldArtifacts(activeSession: {
|
||||
messages: AgentMessage[];
|
||||
agent: { replaceMessages: (messages: AgentMessage[]) => void };
|
||||
agent: { state: { messages: AgentMessage[] } };
|
||||
sessionManager?: unknown;
|
||||
}) {
|
||||
const strippedMessages = activeSession.messages.slice();
|
||||
@@ -170,7 +170,7 @@ export function stripSessionsYieldArtifacts(activeSession: {
|
||||
break;
|
||||
}
|
||||
if (strippedMessages.length !== activeSession.messages.length) {
|
||||
activeSession.agent.replaceMessages(strippedMessages);
|
||||
activeSession.agent.state.messages = strippedMessages;
|
||||
}
|
||||
|
||||
const sessionManager = activeSession.sessionManager as
|
||||
|
||||
@@ -512,7 +512,11 @@ export type MutableSession = {
|
||||
isStreaming: boolean;
|
||||
agent: {
|
||||
streamFn?: unknown;
|
||||
replaceMessages: (messages: unknown[]) => void;
|
||||
transport?: string;
|
||||
state: {
|
||||
messages: unknown[];
|
||||
systemPrompt?: string;
|
||||
};
|
||||
};
|
||||
prompt: (prompt: string, options?: { images?: unknown[] }) => Promise<void>;
|
||||
abort: () => Promise<void>;
|
||||
@@ -608,8 +612,13 @@ export function createDefaultEmbeddedSession(params?: {
|
||||
isCompacting: false,
|
||||
isStreaming: false,
|
||||
agent: {
|
||||
replaceMessages: (messages: unknown[]) => {
|
||||
session.messages = [...messages];
|
||||
state: {
|
||||
get messages() {
|
||||
return session.messages;
|
||||
},
|
||||
set messages(messages: unknown[]) {
|
||||
session.messages = [...messages];
|
||||
},
|
||||
},
|
||||
},
|
||||
prompt: async (prompt, options) => {
|
||||
|
||||
@@ -980,7 +980,7 @@ export async function runEmbeddedAttempt(
|
||||
`embedded agent transport override: ${activeSession.agent.transport} -> ${agentTransportOverride} ` +
|
||||
`(${params.provider}/${params.modelId})`,
|
||||
);
|
||||
activeSession.agent.setTransport(agentTransportOverride);
|
||||
activeSession.agent.transport = agentTransportOverride;
|
||||
}
|
||||
|
||||
const cacheObservabilityEnabled = Boolean(cacheTrace) || log.isEnabled("debug");
|
||||
@@ -1178,7 +1178,7 @@ export async function runEmbeddedAttempt(
|
||||
: truncated;
|
||||
cacheTrace?.recordStage("session:limited", { messages: limited });
|
||||
if (limited.length > 0) {
|
||||
activeSession.agent.replaceMessages(limited);
|
||||
activeSession.agent.state.messages = limited;
|
||||
}
|
||||
|
||||
if (params.contextEngine) {
|
||||
@@ -1196,7 +1196,7 @@ export async function runEmbeddedAttempt(
|
||||
throw new Error("context engine assemble returned no result");
|
||||
}
|
||||
if (assembled.messages !== activeSession.messages) {
|
||||
activeSession.agent.replaceMessages(assembled.messages);
|
||||
activeSession.agent.state.messages = assembled.messages;
|
||||
}
|
||||
if (assembled.systemPromptAddition) {
|
||||
systemPromptText = prependSystemPromptAddition({
|
||||
@@ -1555,7 +1555,7 @@ export async function runEmbeddedAttempt(
|
||||
sessionManager.resetLeaf();
|
||||
}
|
||||
const sessionContext = sessionManager.buildSessionContext();
|
||||
activeSession.agent.replaceMessages(sessionContext.messages);
|
||||
activeSession.agent.state.messages = sessionContext.messages;
|
||||
const orphanRepairMessage =
|
||||
`Removed orphaned user message to prevent consecutive user turns. ` +
|
||||
`runId=${params.runId} sessionId=${params.sessionId} trigger=${params.trigger}`;
|
||||
@@ -1574,7 +1574,7 @@ export async function runEmbeddedAttempt(
|
||||
// history stays byte-identical for prompt-cache prefix matching.
|
||||
const didPruneImages = pruneProcessedHistoryImages(activeSession.messages);
|
||||
if (didPruneImages) {
|
||||
activeSession.agent.replaceMessages(activeSession.messages);
|
||||
activeSession.agent.state.messages = activeSession.messages;
|
||||
}
|
||||
|
||||
// Detect and load images referenced in the prompt for vision-capable models.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AgentSession } from "@mariozechner/pi-coding-agent";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { applySystemPromptOverrideToSession, createSystemPromptOverride } from "./system-prompt.js";
|
||||
|
||||
type MutableSession = {
|
||||
@@ -9,52 +9,51 @@ type MutableSession = {
|
||||
|
||||
type MockSession = MutableSession & {
|
||||
agent: {
|
||||
setSystemPrompt: ReturnType<typeof vi.fn>;
|
||||
state: {
|
||||
systemPrompt?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
function createMockSession(): {
|
||||
session: MockSession;
|
||||
setSystemPrompt: ReturnType<typeof vi.fn>;
|
||||
} {
|
||||
const setSystemPrompt = vi.fn<(prompt: string) => void>();
|
||||
const session = {
|
||||
agent: { setSystemPrompt },
|
||||
agent: { state: {} },
|
||||
} as MockSession;
|
||||
return { session, setSystemPrompt };
|
||||
return { session };
|
||||
}
|
||||
|
||||
function applyAndGetMutableSession(
|
||||
prompt: Parameters<typeof applySystemPromptOverrideToSession>[1],
|
||||
) {
|
||||
const { session, setSystemPrompt } = createMockSession();
|
||||
const { session } = createMockSession();
|
||||
applySystemPromptOverrideToSession(session as unknown as AgentSession, prompt);
|
||||
return {
|
||||
mutable: session,
|
||||
setSystemPrompt,
|
||||
};
|
||||
}
|
||||
|
||||
describe("applySystemPromptOverrideToSession", () => {
|
||||
it("applies a string override to the session system prompt", () => {
|
||||
const prompt = "You are a helpful assistant with custom context.";
|
||||
const { mutable, setSystemPrompt } = applyAndGetMutableSession(prompt);
|
||||
const { mutable } = applyAndGetMutableSession(prompt);
|
||||
|
||||
expect(setSystemPrompt).toHaveBeenCalledWith(prompt);
|
||||
expect(mutable.agent.state.systemPrompt).toBe(prompt);
|
||||
expect(mutable._baseSystemPrompt).toBe(prompt);
|
||||
});
|
||||
|
||||
it("trims whitespace from string overrides", () => {
|
||||
const { setSystemPrompt } = applyAndGetMutableSession(" padded prompt ");
|
||||
const { mutable } = applyAndGetMutableSession(" padded prompt ");
|
||||
|
||||
expect(setSystemPrompt).toHaveBeenCalledWith("padded prompt");
|
||||
expect(mutable.agent.state.systemPrompt).toBe("padded prompt");
|
||||
});
|
||||
|
||||
it("applies a function override to the session system prompt", () => {
|
||||
const override = createSystemPromptOverride("function-based prompt");
|
||||
const { setSystemPrompt } = applyAndGetMutableSession(override);
|
||||
const { mutable } = applyAndGetMutableSession(override);
|
||||
|
||||
expect(setSystemPrompt).toHaveBeenCalledWith("function-based prompt");
|
||||
expect(mutable.agent.state.systemPrompt).toBe("function-based prompt");
|
||||
});
|
||||
|
||||
it("sets _rebuildSystemPrompt that returns the override", () => {
|
||||
|
||||
@@ -96,7 +96,7 @@ export function applySystemPromptOverrideToSession(
|
||||
override: string | ((defaultPrompt?: string) => string),
|
||||
) {
|
||||
const prompt = typeof override === "function" ? override() : override.trim();
|
||||
session.agent.setSystemPrompt(prompt);
|
||||
session.agent.state.systemPrompt = prompt;
|
||||
const mutableSession = session as unknown as {
|
||||
_baseSystemPrompt?: string;
|
||||
_rebuildSystemPrompt?: (toolNames: string[]) => string;
|
||||
|
||||
@@ -177,20 +177,17 @@ describe("buildAgentSystemPrompt", () => {
|
||||
expect(prompt).not.toContain("allow-once|allow-always|deny");
|
||||
});
|
||||
|
||||
it("tells native approval channels not to duplicate plain chat /approve instructions", () => {
|
||||
it("keeps manual /approve instructions for telegram runtime prompts", () => {
|
||||
const prompt = buildAgentSystemPrompt({
|
||||
workspaceDir: "/tmp/openclaw",
|
||||
runtimeInfo: { channel: "telegram" },
|
||||
runtimeInfo: { channel: "telegram", capabilities: ["inlineButtons"] },
|
||||
});
|
||||
|
||||
expect(prompt).toContain(
|
||||
"When exec returns approval-pending on Discord, Slack, Telegram, or WebChat, rely on the native approval card/buttons when they appear",
|
||||
);
|
||||
expect(prompt).toContain(
|
||||
"Only include the concrete /approve command if the tool result says chat approvals are unavailable or only manual approval is possible.",
|
||||
"When exec returns approval-pending, include the concrete /approve command from tool output",
|
||||
);
|
||||
expect(prompt).not.toContain(
|
||||
"When exec returns approval-pending, include the concrete /approve command from tool output",
|
||||
"When exec returns approval-pending on this channel, rely on native approval card/buttons when they appear",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -201,7 +198,7 @@ describe("buildAgentSystemPrompt", () => {
|
||||
});
|
||||
|
||||
expect(prompt).toContain(
|
||||
"When exec returns approval-pending on Discord, Slack, Telegram, or WebChat, rely on the native approval card/buttons when they appear",
|
||||
"When exec returns approval-pending on this channel, rely on native approval card/buttons when they appear",
|
||||
);
|
||||
expect(prompt).toContain(
|
||||
"Only include the concrete /approve command if the tool result says chat approvals are unavailable or only manual approval is possible.",
|
||||
@@ -651,7 +648,7 @@ describe("buildAgentSystemPrompt", () => {
|
||||
});
|
||||
|
||||
expect(prompt).toContain("channel=telegram");
|
||||
expect(prompt).toContain("capabilities=inlineButtons");
|
||||
expect(prompt.toLowerCase()).toContain("capabilities=inlinebuttons");
|
||||
});
|
||||
|
||||
it("includes agent id in runtime when provided", () => {
|
||||
|
||||
@@ -122,6 +122,13 @@ async function writeAuthProfiles(agentDir: string, profiles: unknown) {
|
||||
async function createOpenClawCodingToolsWithFreshModules(options?: CreateOpenClawCodingToolsArgs) {
|
||||
vi.resetModules();
|
||||
const freshImageTool = await import("./image-tool.js");
|
||||
const defaultImageModels = new Map<string, string>([
|
||||
["anthropic", "claude-opus-4-6"],
|
||||
["minimax", "MiniMax-VL-01"],
|
||||
["minimax-portal", "MiniMax-VL-01"],
|
||||
["openai", "gpt-5.4-mini"],
|
||||
["zai", "glm-4.6v"],
|
||||
]);
|
||||
freshImageTool.__testing.setProviderDepsForTest({
|
||||
buildProviderRegistry: (overrides?: Record<string, MediaUnderstandingProvider>) =>
|
||||
imageProviderHarness.buildProviderRegistry(overrides),
|
||||
@@ -129,6 +136,12 @@ async function createOpenClawCodingToolsWithFreshModules(options?: CreateOpenCla
|
||||
id: string,
|
||||
registry: Map<string, MediaUnderstandingProvider>,
|
||||
) => imageProviderHarness.getMediaUnderstandingProvider(id, registry),
|
||||
describeImageWithModel: describeGenericImageWithModel,
|
||||
describeImagesWithModel: describeGenericImagesWithModel,
|
||||
resolveAutoMediaKeyProviders: ({ capability }) =>
|
||||
capability === "image" ? ["openai", "anthropic"] : [],
|
||||
resolveDefaultMediaModel: ({ providerId, capability }) =>
|
||||
capability === "image" ? defaultImageModels.get(providerId.toLowerCase()) : undefined,
|
||||
});
|
||||
const { createOpenClawCodingTools } = await import("../pi-tools.js");
|
||||
return createOpenClawCodingTools(options);
|
||||
@@ -356,6 +369,50 @@ async function describeMoonshotImages(
|
||||
});
|
||||
}
|
||||
|
||||
async function readMockResponseText(response: Response): Promise<string> {
|
||||
const contentType =
|
||||
response.headers instanceof Headers ? (response.headers.get("content-type") ?? "") : "";
|
||||
if (contentType.includes("application/json") || typeof response.text !== "function") {
|
||||
const payload = (await response.json()) as { content?: string };
|
||||
return payload.content ?? "";
|
||||
}
|
||||
const raw = await response.text();
|
||||
const match = raw.match(/"content":"([^"]*)"/);
|
||||
return match?.[1] ?? "";
|
||||
}
|
||||
|
||||
async function describeGenericImageWithModel(
|
||||
params: ImageDescriptionRequest,
|
||||
): Promise<{ text: string; model: string }> {
|
||||
const response = await global.fetch("https://example.invalid/media-image", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
provider: params.provider,
|
||||
model: params.model,
|
||||
prompt: params.prompt,
|
||||
mime: params.mime,
|
||||
}),
|
||||
});
|
||||
return { text: await readMockResponseText(response), model: params.model };
|
||||
}
|
||||
|
||||
async function describeGenericImagesWithModel(
|
||||
params: ImagesDescriptionRequest,
|
||||
): Promise<{ text: string; model: string }> {
|
||||
const response = await global.fetch("https://example.invalid/media-images", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
provider: params.provider,
|
||||
model: params.model,
|
||||
prompt: params.prompt,
|
||||
imageCount: params.images.length,
|
||||
}),
|
||||
});
|
||||
return { text: await readMockResponseText(response), model: params.model };
|
||||
}
|
||||
|
||||
const moonshotProvider = {
|
||||
id: "moonshot",
|
||||
capabilities: ["image"],
|
||||
@@ -365,6 +422,13 @@ const moonshotProvider = {
|
||||
|
||||
function installImageUnderstandingProviderStubs(...providers: MediaUnderstandingProvider[]) {
|
||||
imageProviderHarness.setProviders(providers);
|
||||
const defaultImageModels = new Map<string, string>([
|
||||
["anthropic", "claude-opus-4-6"],
|
||||
["minimax", "MiniMax-VL-01"],
|
||||
["minimax-portal", "MiniMax-VL-01"],
|
||||
["openai", "gpt-5.4-mini"],
|
||||
["zai", "glm-4.6v"],
|
||||
]);
|
||||
__testing.setProviderDepsForTest({
|
||||
buildProviderRegistry: (overrides?: Record<string, MediaUnderstandingProvider>) =>
|
||||
imageProviderHarness.buildProviderRegistry(overrides),
|
||||
@@ -372,6 +436,12 @@ function installImageUnderstandingProviderStubs(...providers: MediaUnderstanding
|
||||
id: string,
|
||||
registry: Map<string, MediaUnderstandingProvider>,
|
||||
) => imageProviderHarness.getMediaUnderstandingProvider(id, registry),
|
||||
describeImageWithModel: describeGenericImageWithModel,
|
||||
describeImagesWithModel: describeGenericImagesWithModel,
|
||||
resolveAutoMediaKeyProviders: ({ capability }) =>
|
||||
capability === "image" ? ["openai", "anthropic"] : [],
|
||||
resolveDefaultMediaModel: ({ providerId, capability }) =>
|
||||
capability === "image" ? defaultImageModels.get(providerId.toLowerCase()) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,10 @@ const DEFAULT_MAX_IMAGES = 20;
|
||||
const imageToolProviderDeps = {
|
||||
buildProviderRegistry,
|
||||
getMediaUnderstandingProvider,
|
||||
describeImageWithModel,
|
||||
describeImagesWithModel,
|
||||
resolveAutoMediaKeyProviders,
|
||||
resolveDefaultMediaModel,
|
||||
};
|
||||
|
||||
export const __testing = {
|
||||
@@ -58,11 +62,23 @@ export const __testing = {
|
||||
setProviderDepsForTest(overrides?: {
|
||||
buildProviderRegistry?: typeof buildProviderRegistry;
|
||||
getMediaUnderstandingProvider?: typeof getMediaUnderstandingProvider;
|
||||
describeImageWithModel?: typeof describeImageWithModel;
|
||||
describeImagesWithModel?: typeof describeImagesWithModel;
|
||||
resolveAutoMediaKeyProviders?: typeof resolveAutoMediaKeyProviders;
|
||||
resolveDefaultMediaModel?: typeof resolveDefaultMediaModel;
|
||||
}) {
|
||||
imageToolProviderDeps.buildProviderRegistry =
|
||||
overrides?.buildProviderRegistry ?? buildProviderRegistry;
|
||||
imageToolProviderDeps.getMediaUnderstandingProvider =
|
||||
overrides?.getMediaUnderstandingProvider ?? getMediaUnderstandingProvider;
|
||||
imageToolProviderDeps.describeImageWithModel =
|
||||
overrides?.describeImageWithModel ?? describeImageWithModel;
|
||||
imageToolProviderDeps.describeImagesWithModel =
|
||||
overrides?.describeImagesWithModel ?? describeImagesWithModel;
|
||||
imageToolProviderDeps.resolveAutoMediaKeyProviders =
|
||||
overrides?.resolveAutoMediaKeyProviders ?? resolveAutoMediaKeyProviders;
|
||||
imageToolProviderDeps.resolveDefaultMediaModel =
|
||||
overrides?.resolveDefaultMediaModel ?? resolveDefaultMediaModel;
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -108,7 +124,7 @@ export function resolveImageModelConfigForTool(params: {
|
||||
if (providerVisionFromConfig) {
|
||||
return [providerVisionFromConfig];
|
||||
}
|
||||
const providerDefault = resolveDefaultMediaModel({
|
||||
const providerDefault = imageToolProviderDeps.resolveDefaultMediaModel({
|
||||
cfg: params.cfg,
|
||||
providerId: primary.provider,
|
||||
capability: "image",
|
||||
@@ -122,17 +138,19 @@ export function resolveImageModelConfigForTool(params: {
|
||||
return [];
|
||||
})();
|
||||
|
||||
const autoCandidates = resolveAutoMediaKeyProviders({
|
||||
cfg: params.cfg,
|
||||
capability: "image",
|
||||
}).map((providerId) => {
|
||||
const modelId = resolveDefaultMediaModel({
|
||||
const autoCandidates = imageToolProviderDeps
|
||||
.resolveAutoMediaKeyProviders({
|
||||
cfg: params.cfg,
|
||||
providerId,
|
||||
capability: "image",
|
||||
})
|
||||
.map((providerId) => {
|
||||
const modelId = imageToolProviderDeps.resolveDefaultMediaModel({
|
||||
cfg: params.cfg,
|
||||
providerId,
|
||||
capability: "image",
|
||||
});
|
||||
return modelId ? `${providerId}/${modelId}` : null;
|
||||
});
|
||||
return modelId ? `${providerId}/${modelId}` : null;
|
||||
});
|
||||
|
||||
return buildToolModelConfigFromCandidates({
|
||||
explicit,
|
||||
@@ -186,7 +204,8 @@ async function runImagePrompt(params: {
|
||||
params.images.length > 1 &&
|
||||
(imageProvider?.describeImages || !imageProvider?.describeImage)
|
||||
) {
|
||||
const describeImages = imageProvider?.describeImages ?? describeImagesWithModel;
|
||||
const describeImages =
|
||||
imageProvider?.describeImages ?? imageToolProviderDeps.describeImagesWithModel;
|
||||
const described = await describeImages({
|
||||
images: params.images.map((image, index) => ({
|
||||
buffer: image.buffer,
|
||||
@@ -203,7 +222,8 @@ async function runImagePrompt(params: {
|
||||
});
|
||||
return { text: described.text, provider, model: described.model ?? modelId };
|
||||
}
|
||||
const describeImage = imageProvider?.describeImage ?? describeImageWithModel;
|
||||
const describeImage =
|
||||
imageProvider?.describeImage ?? imageToolProviderDeps.describeImageWithModel;
|
||||
if (params.images.length === 1) {
|
||||
const image = params.images[0];
|
||||
const described = await describeImage({
|
||||
|
||||
@@ -27,7 +27,7 @@ function textResponse(body: string): Response {
|
||||
function setMockFetch(
|
||||
impl: FetchMock = async (_input: RequestInfo | URL, _init?: RequestInit) => textResponse(""),
|
||||
) {
|
||||
const fetchSpy = vi.fn<FetchMock>(impl);
|
||||
const fetchSpy = vi.fn(impl);
|
||||
global.fetch = withFetchPreconnect(fetchSpy);
|
||||
return fetchSpy;
|
||||
}
|
||||
|
||||
@@ -235,15 +235,6 @@ describe("web tools defaults", () => {
|
||||
setActivePluginRegistry(registry);
|
||||
|
||||
const tool = createWebSearchTool({
|
||||
config: {
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
provider: "custom",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sandboxed: true,
|
||||
runtimeWebSearch: {
|
||||
providerConfigured: "custom",
|
||||
@@ -432,7 +423,7 @@ describe("web_search perplexity Search API", () => {
|
||||
it("uses config API key when provided", async () => {
|
||||
const mockFetch = installPerplexitySearchApiFetch([]);
|
||||
const tool = createPerplexitySearchTool({ apiKey: "pplx-config" });
|
||||
await tool?.execute?.("call-1", { query: "test" });
|
||||
await tool?.execute?.("call-1", { query: "config-api-key-test" });
|
||||
|
||||
expect(mockFetch).toHaveBeenCalled();
|
||||
const headers = (mockFetch.mock.calls[0]?.[1] as RequestInit | undefined)?.headers as
|
||||
@@ -559,7 +550,7 @@ describe("web_search perplexity OpenRouter compatibility", () => {
|
||||
it("routes configured sk-or key through chat completions", async () => {
|
||||
const mockFetch = installPerplexityChatFetch();
|
||||
const tool = createPerplexitySearchTool({ apiKey: "sk-or-v1-test" }); // pragma: allowlist secret
|
||||
await tool?.execute?.("call-1", { query: "test" });
|
||||
await tool?.execute?.("call-1", { query: "configured-openrouter-key-test" });
|
||||
|
||||
expect(mockFetch).toHaveBeenCalled();
|
||||
expect(mockFetch.mock.calls[0]?.[0]).toBe("https://openrouter.ai/api/v1/chat/completions");
|
||||
@@ -606,7 +597,7 @@ describe("web_search perplexity OpenRouter compatibility", () => {
|
||||
],
|
||||
});
|
||||
const tool = createPerplexitySearchTool();
|
||||
const result = await tool?.execute?.("call-1", { query: "test" });
|
||||
const result = await tool?.execute?.("call-1", { query: "annotations-fallback-test" });
|
||||
|
||||
expect(mockFetch).toHaveBeenCalled();
|
||||
expect(result?.details).toMatchObject({
|
||||
@@ -771,7 +762,7 @@ describe("web_search kimi provider", () => {
|
||||
expect(result?.details).toMatchObject({ error: "missing_kimi_api_key" });
|
||||
});
|
||||
|
||||
it("runs the Kimi web_search tool flow and echoes tool results", async () => {
|
||||
it("runs the Kimi web_search tool flow and echoes tool-call arguments", async () => {
|
||||
const mockFetch = vi.fn(async (_input: RequestInfo | URL, _init?: RequestInit) => {
|
||||
const idx = mockFetch.mock.calls.length;
|
||||
if (idx === 1) {
|
||||
@@ -833,9 +824,7 @@ describe("web_search kimi provider", () => {
|
||||
| { content?: string; tool_call_id?: string }
|
||||
| undefined;
|
||||
expect(toolMessage?.tool_call_id).toBe("call_1");
|
||||
expect(JSON.parse(toolMessage?.content ?? "{}")).toMatchObject({
|
||||
search_results: [{ url: "https://openclaw.ai/docs" }],
|
||||
});
|
||||
expect(JSON.parse(toolMessage?.content ?? "{}")).toMatchObject({ q: "openclaw" });
|
||||
|
||||
const details = result?.details as {
|
||||
citations?: string[];
|
||||
|
||||
@@ -4,7 +4,7 @@ import { listLegacyWebFetchConfigPaths, migrateLegacyWebFetchConfig } from "./le
|
||||
|
||||
describe("legacy web fetch config", () => {
|
||||
it("migrates legacy Firecrawl fetch config into plugin-owned config", () => {
|
||||
const res = migrateLegacyWebFetchConfig<OpenClawConfig>({
|
||||
const res = migrateLegacyWebFetchConfig({
|
||||
tools: {
|
||||
web: {
|
||||
fetch: {
|
||||
@@ -40,7 +40,7 @@ describe("legacy web fetch config", () => {
|
||||
});
|
||||
|
||||
it("drops legacy firecrawl.enabled when migrating plugin-owned config", () => {
|
||||
const res = migrateLegacyWebFetchConfig<OpenClawConfig>({
|
||||
const res = migrateLegacyWebFetchConfig({
|
||||
tools: {
|
||||
web: {
|
||||
fetch: {
|
||||
|
||||
@@ -233,7 +233,7 @@ describe("gateway server models + voicewake", () => {
|
||||
(o) => o.type === "event" && o.event === "voicewake.changed",
|
||||
);
|
||||
|
||||
const setRes = await rpcReq<{ triggers: string[] }>(ws, "voicewake.set", {
|
||||
const setRes = await rpcReq(ws, "voicewake.set", {
|
||||
triggers: [" hi ", "", "there"],
|
||||
});
|
||||
expect(setRes.ok).toBe(true);
|
||||
@@ -290,7 +290,7 @@ describe("gateway server models + voicewake", () => {
|
||||
nodeWs,
|
||||
(o) => o.type === "event" && o.event === "voicewake.changed",
|
||||
);
|
||||
const setRes = await rpcReq<{ triggers: string[] }>(ws, "voicewake.set", {
|
||||
const setRes = await rpcReq(ws, "voicewake.set", {
|
||||
triggers: ["openclaw", "computer"],
|
||||
});
|
||||
expect(setRes.ok).toBe(true);
|
||||
|
||||
@@ -43,13 +43,6 @@ type TalkConfigPayload = {
|
||||
};
|
||||
};
|
||||
type TalkConfig = NonNullable<NonNullable<TalkConfigPayload["config"]>["talk"]>;
|
||||
type TalkSpeakPayload = {
|
||||
audioBase64?: string;
|
||||
provider?: string;
|
||||
outputFormat?: string;
|
||||
mimeType?: string;
|
||||
fileExtension?: string;
|
||||
};
|
||||
const TALK_CONFIG_DEVICE_PATH = path.join(
|
||||
os.tmpdir(),
|
||||
`openclaw-talk-config-device-${process.pid}.json`,
|
||||
@@ -122,7 +115,7 @@ async function fetchTalkSpeak(
|
||||
params: Record<string, unknown>,
|
||||
timeoutMs?: number,
|
||||
) {
|
||||
return rpcReq<TalkSpeakPayload>(ws, "talk.speak", params, timeoutMs);
|
||||
return rpcReq(ws, "talk.speak", params, timeoutMs);
|
||||
}
|
||||
|
||||
function expectElevenLabsTalkConfig(
|
||||
|
||||
@@ -230,10 +230,10 @@ export class MediaAttachmentCache {
|
||||
}
|
||||
|
||||
async cleanup(): Promise<void> {
|
||||
const cleanups: Array<Promise<void> | void> = [];
|
||||
const cleanups: Promise<void>[] = [];
|
||||
for (const entry of this.entries.values()) {
|
||||
if (entry.tempCleanup) {
|
||||
cleanups.push(Promise.resolve(entry.tempCleanup()));
|
||||
cleanups.push(entry.tempCleanup());
|
||||
entry.tempCleanup = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ describe("createCachedLazyValueGetter", () => {
|
||||
|
||||
it("uses the fallback when the lazy value resolves nullish", () => {
|
||||
const fallback = { type: "object" as const, properties: {} };
|
||||
const getSchema = createCachedLazyValueGetter<typeof fallback>(() => undefined, fallback);
|
||||
const resolveSchema = (): typeof fallback | undefined => undefined;
|
||||
const getSchema = createCachedLazyValueGetter(resolveSchema, fallback);
|
||||
|
||||
expect(getSchema()).toBe(fallback);
|
||||
});
|
||||
|
||||
@@ -56,7 +56,7 @@ describe("composeProviderStreamWrappers", () => {
|
||||
});
|
||||
|
||||
describe("buildProviderStreamFamilyHooks", () => {
|
||||
it("covers the stream family matrix", () => {
|
||||
it("covers the stream family matrix", async () => {
|
||||
let capturedPayload: Record<string, unknown> | undefined;
|
||||
let capturedModelId: string | undefined;
|
||||
let capturedHeaders: Record<string, string> | undefined;
|
||||
@@ -74,12 +74,17 @@ describe("buildProviderStreamFamilyHooks", () => {
|
||||
};
|
||||
|
||||
const googleHooks = buildProviderStreamFamilyHooks("google-thinking");
|
||||
void requireStreamFn(
|
||||
const googleStream = requireStreamFn(
|
||||
requireWrapStreamFn(googleHooks.wrapStreamFn)({
|
||||
streamFn: baseStreamFn,
|
||||
thinkingLevel: "high",
|
||||
} as never),
|
||||
)({ api: "google-generative-ai", id: "gemini-3.1-pro-preview" } as never, {} as never, {});
|
||||
);
|
||||
await googleStream(
|
||||
{ api: "google-generative-ai", id: "gemini-3.1-pro-preview" } as never,
|
||||
{} as never,
|
||||
{},
|
||||
);
|
||||
expect(capturedPayload).toMatchObject({
|
||||
config: { thinkingConfig: { thinkingLevel: "HIGH" } },
|
||||
});
|
||||
@@ -89,12 +94,13 @@ describe("buildProviderStreamFamilyHooks", () => {
|
||||
expect(googleThinkingConfig).not.toHaveProperty("thinkingBudget");
|
||||
|
||||
const minimaxHooks = buildProviderStreamFamilyHooks("minimax-fast-mode");
|
||||
void requireStreamFn(
|
||||
const minimaxStream = requireStreamFn(
|
||||
requireWrapStreamFn(minimaxHooks.wrapStreamFn)({
|
||||
streamFn: baseStreamFn,
|
||||
extraParams: { fastMode: true },
|
||||
} as never),
|
||||
)(
|
||||
);
|
||||
await minimaxStream(
|
||||
{
|
||||
api: "anthropic-messages",
|
||||
provider: "minimax",
|
||||
@@ -131,12 +137,17 @@ describe("buildProviderStreamFamilyHooks", () => {
|
||||
expect(capturedPayload).not.toHaveProperty("reasoning");
|
||||
|
||||
const moonshotHooks = buildProviderStreamFamilyHooks("moonshot-thinking");
|
||||
void requireStreamFn(
|
||||
const moonshotStream = requireStreamFn(
|
||||
requireWrapStreamFn(moonshotHooks.wrapStreamFn)({
|
||||
streamFn: baseStreamFn,
|
||||
thinkingLevel: "off",
|
||||
} as never),
|
||||
)({ api: "openai-completions", id: "kimi-k2.5" } as never, {} as never, {});
|
||||
);
|
||||
await moonshotStream(
|
||||
{ api: "openai-completions", id: "kimi-k2.5" } as never,
|
||||
{} as never,
|
||||
{},
|
||||
);
|
||||
expect(capturedPayload).toMatchObject({
|
||||
config: { thinkingConfig: { thinkingBudget: -1 } },
|
||||
thinking: { type: "disabled" },
|
||||
@@ -192,23 +203,25 @@ describe("buildProviderStreamFamilyHooks", () => {
|
||||
expect(capturedPayload).not.toHaveProperty("reasoning");
|
||||
|
||||
const toolStreamHooks = buildProviderStreamFamilyHooks("tool-stream-default-on");
|
||||
void requireStreamFn(
|
||||
const toolStreamDefault = requireStreamFn(
|
||||
requireWrapStreamFn(toolStreamHooks.wrapStreamFn)({
|
||||
streamFn: baseStreamFn,
|
||||
extraParams: {},
|
||||
} as never),
|
||||
)({ id: "glm-4.7" } as never, {} as never, {});
|
||||
);
|
||||
await toolStreamDefault({ id: "glm-4.7" } as never, {} as never, {});
|
||||
expect(capturedPayload).toMatchObject({
|
||||
config: { thinkingConfig: { thinkingBudget: -1 } },
|
||||
tool_stream: true,
|
||||
});
|
||||
|
||||
void requireStreamFn(
|
||||
const toolStreamDisabled = requireStreamFn(
|
||||
requireWrapStreamFn(toolStreamHooks.wrapStreamFn)({
|
||||
streamFn: baseStreamFn,
|
||||
extraParams: { tool_stream: false },
|
||||
} as never),
|
||||
)({ id: "glm-4.7" } as never, {} as never, {});
|
||||
);
|
||||
await toolStreamDisabled({ id: "glm-4.7" } as never, {} as never, {});
|
||||
expect(capturedPayload).toMatchObject({
|
||||
config: { thinkingConfig: { thinkingBudget: -1 } },
|
||||
});
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import type { StreamFn } from "@mariozechner/pi-agent-core";
|
||||
import type { ProviderPlugin } from "../plugins/types.js";
|
||||
import type { ProviderWrapStreamFnContext } from "./plugin-entry.js";
|
||||
import {
|
||||
createGoogleThinkingPayloadWrapper,
|
||||
sanitizeGoogleThinkingPayload,
|
||||
@@ -28,7 +26,12 @@ import {
|
||||
resolveOpenAIServiceTier,
|
||||
resolveOpenAITextVerbosity,
|
||||
} from "../agents/pi-embedded-runner/openai-stream-wrappers.js";
|
||||
import { createToolStreamWrapper, createZaiToolStreamWrapper } from "../agents/pi-embedded-runner/zai-stream-wrappers.js";
|
||||
import {
|
||||
createToolStreamWrapper,
|
||||
createZaiToolStreamWrapper,
|
||||
} from "../agents/pi-embedded-runner/zai-stream-wrappers.js";
|
||||
import type { ProviderPlugin } from "../plugins/types.js";
|
||||
import type { ProviderWrapStreamFnContext } from "./plugin-entry.js";
|
||||
|
||||
export type ProviderStreamWrapperFactory =
|
||||
| ((streamFn: StreamFn | undefined) => StreamFn | undefined)
|
||||
@@ -40,7 +43,7 @@ export function composeProviderStreamWrappers(
|
||||
baseStreamFn: StreamFn | undefined,
|
||||
...wrappers: ProviderStreamWrapperFactory[]
|
||||
): StreamFn | undefined {
|
||||
return wrappers.reduce<StreamFn | undefined>(
|
||||
return wrappers.reduce(
|
||||
(streamFn, wrapper) => (wrapper ? wrapper(streamFn) : streamFn),
|
||||
baseStreamFn,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user