mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-01 12:46:47 +00:00
fix(ollama): keep native tool ids through replay
This commit is contained in:
@@ -783,8 +783,8 @@ describe("ollama plugin", () => {
|
||||
modelApi: "ollama",
|
||||
modelId: "qwen3.5:9b",
|
||||
} as never);
|
||||
expect(nativePolicy?.sanitizeToolCallIds).toBe(true);
|
||||
expect(nativePolicy?.toolCallIdMode).toBe("strict");
|
||||
expect(nativePolicy?.sanitizeToolCallIds).toBe(false);
|
||||
expect(nativePolicy?.toolCallIdMode).toBeUndefined();
|
||||
expect(nativePolicy?.applyAssistantFirstOrderingFix).toBe(true);
|
||||
expect(nativePolicy?.validateGeminiTurns).toBe(true);
|
||||
expect(nativePolicy?.validateAnthropicTurns).toBe(true);
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
type ProviderAuthMethodNonInteractiveContext,
|
||||
type ProviderAuthResult,
|
||||
type ProviderCatalogContext,
|
||||
type ProviderReplayPolicy,
|
||||
type ProviderRuntimeModel,
|
||||
} from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { buildApiKeyCredential } from "openclaw/plugin-sdk/provider-auth";
|
||||
@@ -66,6 +67,15 @@ function usesOllamaOpenAICompatTransport(model: {
|
||||
);
|
||||
}
|
||||
|
||||
function buildNativeOllamaReplayPolicy(): ProviderReplayPolicy {
|
||||
return {
|
||||
...buildOpenAICompatibleReplayPolicy("openai-completions", {
|
||||
sanitizeToolCallIds: false,
|
||||
}),
|
||||
sanitizeToolCallIds: false,
|
||||
};
|
||||
}
|
||||
|
||||
const dynamicModelCache = new Map<string, ProviderRuntimeModel[]>();
|
||||
|
||||
function buildDynamicCacheKey(provider: string, baseUrl: string | undefined): string {
|
||||
@@ -245,7 +255,7 @@ export default definePluginEntry({
|
||||
...OPENAI_COMPATIBLE_REPLAY_HOOKS,
|
||||
buildReplayPolicy: (ctx) =>
|
||||
ctx.modelApi === "ollama"
|
||||
? buildOpenAICompatibleReplayPolicy("openai-completions")
|
||||
? buildNativeOllamaReplayPolicy()
|
||||
: buildOpenAICompatibleReplayPolicy(ctx.modelApi),
|
||||
contributeResolvedModelCompat: ({ model }) =>
|
||||
usesOllamaOpenAICompatTransport(model) ? { supportsUsageInStreaming: true } : undefined,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { sanitizeReplayToolCallIdsForStream } from "./attempt.tool-call-normalization.js";
|
||||
import {
|
||||
sanitizeReplayToolCallIdsForStream,
|
||||
shouldApplyReplayToolCallIdSanitizer,
|
||||
} from "./attempt.tool-call-normalization.js";
|
||||
|
||||
type AssistantMessage = Extract<AgentMessage, { role: "assistant" }>;
|
||||
type ToolResultMessage = Extract<AgentMessage, { role: "toolResult" }>;
|
||||
@@ -47,6 +50,29 @@ function toolResultSummary(message: AgentMessage | undefined) {
|
||||
}
|
||||
|
||||
describe("sanitizeReplayToolCallIdsForStream", () => {
|
||||
it("skips strict stream id sanitization when provider policy opts out", () => {
|
||||
expect(
|
||||
shouldApplyReplayToolCallIdSanitizer({
|
||||
sanitizeToolCallIds: false,
|
||||
isOpenAIResponsesApi: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
shouldApplyReplayToolCallIdSanitizer({
|
||||
sanitizeToolCallIds: true,
|
||||
toolCallIdMode: "strict",
|
||||
isOpenAIResponsesApi: false,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldApplyReplayToolCallIdSanitizer({
|
||||
sanitizeToolCallIds: true,
|
||||
toolCallIdMode: "strict",
|
||||
isOpenAIResponsesApi: true,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("drops orphaned tool results after strict id sanitization", () => {
|
||||
const messages: AgentMessage[] = [
|
||||
{
|
||||
|
||||
@@ -879,6 +879,20 @@ export function wrapStreamFnTrimToolCallNames(
|
||||
};
|
||||
}
|
||||
|
||||
type ReplayToolCallIdSanitizerDecision = {
|
||||
sanitizeToolCallIds: boolean;
|
||||
toolCallIdMode?: ToolCallIdMode;
|
||||
isOpenAIResponsesApi: boolean;
|
||||
};
|
||||
|
||||
export function shouldApplyReplayToolCallIdSanitizer(
|
||||
params: ReplayToolCallIdSanitizerDecision,
|
||||
): params is ReplayToolCallIdSanitizerDecision & { toolCallIdMode: ToolCallIdMode } {
|
||||
return (
|
||||
params.sanitizeToolCallIds && Boolean(params.toolCallIdMode) && !params.isOpenAIResponsesApi
|
||||
);
|
||||
}
|
||||
|
||||
export function sanitizeReplayToolCallIdsForStream(params: {
|
||||
messages: AgentMessage[];
|
||||
mode: ToolCallIdMode;
|
||||
|
||||
@@ -355,6 +355,7 @@ import {
|
||||
wrapStreamFnRepairMalformedToolCallArguments,
|
||||
} from "./attempt.tool-call-argument-repair.js";
|
||||
import {
|
||||
shouldApplyReplayToolCallIdSanitizer,
|
||||
sanitizeReplayToolCallIdsForStream,
|
||||
wrapStreamFnSanitizeMalformedToolCalls,
|
||||
wrapStreamFnTrimToolCallNames,
|
||||
@@ -2779,13 +2780,14 @@ export async function runEmbeddedAttempt(
|
||||
params.model.api === "azure-openai-responses" ||
|
||||
params.model.api === "openai-codex-responses";
|
||||
|
||||
if (
|
||||
transcriptPolicy.sanitizeToolCallIds &&
|
||||
transcriptPolicy.toolCallIdMode &&
|
||||
!isOpenAIResponsesApi
|
||||
) {
|
||||
const replayToolCallIdSanitizerDecision = {
|
||||
sanitizeToolCallIds: transcriptPolicy.sanitizeToolCallIds,
|
||||
toolCallIdMode: transcriptPolicy.toolCallIdMode,
|
||||
isOpenAIResponsesApi,
|
||||
};
|
||||
if (shouldApplyReplayToolCallIdSanitizer(replayToolCallIdSanitizerDecision)) {
|
||||
const inner = activeSession.agent.streamFn;
|
||||
const mode = transcriptPolicy.toolCallIdMode;
|
||||
const mode = replayToolCallIdSanitizerDecision.toolCallIdMode;
|
||||
activeSession.agent.streamFn = (model, context, options) => {
|
||||
const ctx = context as unknown as { messages?: unknown };
|
||||
const messages = ctx?.messages;
|
||||
|
||||
Reference in New Issue
Block a user