feat(ollama): add loop prevention to tool-call fixer
This commit is contained in:
parent
994452f201
commit
4740bfe4bb
@ -82,6 +82,9 @@ function isOllamaProvider(model: Model<Api> | undefined | null): boolean {
|
|||||||
return m?.provider === "ollama";
|
return m?.provider === "ollama";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Max consecutive turns where the same tool set is called before we stop converting. */
|
||||||
|
const MAX_REPEAT_TURNS = 3;
|
||||||
|
|
||||||
export type OllamaToolCallFixer = {
|
export type OllamaToolCallFixer = {
|
||||||
wrapStreamFn: (streamFn: StreamFn) => StreamFn;
|
wrapStreamFn: (streamFn: StreamFn) => StreamFn;
|
||||||
};
|
};
|
||||||
@ -90,11 +93,16 @@ export type OllamaToolCallFixer = {
|
|||||||
* Creates a streamFn wrapper that detects tool-call JSON emitted as plain
|
* Creates a streamFn wrapper that detects tool-call JSON emitted as plain
|
||||||
* text by Ollama models and re-emits them as proper toolcall_* events.
|
* text by Ollama models and re-emits them as proper toolcall_* events.
|
||||||
*
|
*
|
||||||
* Consumes the inner stream fully, inspects the final result, and if the
|
* Includes loop prevention: if the model calls the same tool(s) for
|
||||||
* text content matches a known tool schema, replays with fixed events.
|
* {@link MAX_REPEAT_TURNS} consecutive turns, conversion is skipped so
|
||||||
|
* the raw text passes through instead of feeding an infinite tool loop.
|
||||||
*/
|
*/
|
||||||
export function createOllamaToolCallFixer(): OllamaToolCallFixer {
|
export function createOllamaToolCallFixer(): OllamaToolCallFixer {
|
||||||
const wrapStreamFn = (inner: StreamFn): StreamFn => {
|
const wrapStreamFn = (inner: StreamFn): StreamFn => {
|
||||||
|
// Cross-turn state for loop detection.
|
||||||
|
let lastToolKey = "";
|
||||||
|
let repeatCount = 0;
|
||||||
|
|
||||||
const wrapped: StreamFn = (model, context, options) => {
|
const wrapped: StreamFn = (model, context, options) => {
|
||||||
if (!isOllamaProvider(model as Model<Api>)) {
|
if (!isOllamaProvider(model as Model<Api>)) {
|
||||||
return inner(model, context, options);
|
return inner(model, context, options);
|
||||||
@ -128,6 +136,35 @@ export function createOllamaToolCallFixer(): OllamaToolCallFixer {
|
|||||||
const outerStream = new StreamClass() as AssistantMessageEventStream;
|
const outerStream = new StreamClass() as AssistantMessageEventStream;
|
||||||
|
|
||||||
if (!extracted || extracted.length === 0) {
|
if (!extracted || extracted.length === 0) {
|
||||||
|
// Model produced plain text — reset loop counter.
|
||||||
|
lastToolKey = "";
|
||||||
|
repeatCount = 0;
|
||||||
|
for (const event of buffered) {
|
||||||
|
outerStream.push(event);
|
||||||
|
}
|
||||||
|
return outerStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop detection: build a stable key from sorted tool names.
|
||||||
|
const toolKey = extracted
|
||||||
|
.map((tc) => tc.name)
|
||||||
|
.sort()
|
||||||
|
.join(",");
|
||||||
|
|
||||||
|
if (toolKey === lastToolKey) {
|
||||||
|
repeatCount++;
|
||||||
|
} else {
|
||||||
|
lastToolKey = toolKey;
|
||||||
|
repeatCount = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (repeatCount > MAX_REPEAT_TURNS) {
|
||||||
|
log.warn("loop detected, skipping tool-call conversion", {
|
||||||
|
toolKey,
|
||||||
|
repeatCount,
|
||||||
|
threshold: MAX_REPEAT_TURNS,
|
||||||
|
});
|
||||||
|
// Pass through original text so the user sees raw model output.
|
||||||
for (const event of buffered) {
|
for (const event of buffered) {
|
||||||
outerStream.push(event);
|
outerStream.push(event);
|
||||||
}
|
}
|
||||||
@ -138,6 +175,7 @@ export function createOllamaToolCallFixer(): OllamaToolCallFixer {
|
|||||||
provider: (model as { provider?: string }).provider,
|
provider: (model as { provider?: string }).provider,
|
||||||
model: (model as { id?: string }).id,
|
model: (model as { id?: string }).id,
|
||||||
toolCalls: extracted.map((tc) => tc.name),
|
toolCalls: extracted.map((tc) => tc.name),
|
||||||
|
repeatCount,
|
||||||
});
|
});
|
||||||
|
|
||||||
const fixedToolCalls: ToolCall[] = extracted.map((tc) => ({
|
const fixedToolCalls: ToolCall[] = extracted.map((tc) => ({
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user