mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 04:47:03 -06:00
refactor(openai): share completion stop reason mapping
This commit is contained in:
@@ -26,6 +26,7 @@ import { calculateCost } from "../llm/model-utils.js";
|
||||
import { resolveAzureDeploymentNameFromMap } from "../llm/providers/azure-deployment-map.js";
|
||||
import { convertMessages } from "../llm/providers/openai-completions.js";
|
||||
import { clampOpenAIPromptCacheKey } from "../llm/providers/openai-prompt-cache.js";
|
||||
import { mapOpenAIStopReason } from "../llm/providers/openai-stop-reason.js";
|
||||
import type { Api, Context, Model } from "../llm/types.js";
|
||||
import { createAssistantMessageEventStream } from "../llm/utils/event-stream.js";
|
||||
import { parseStreamingJson } from "../llm/utils/json-parse.js";
|
||||
@@ -3101,7 +3102,9 @@ async function processOpenAICompletionsStream(
|
||||
hasReasoningUsageActivity = hasOpenAICompletionsReasoningUsageActivity(choiceUsage);
|
||||
}
|
||||
if (choice.finish_reason) {
|
||||
const finishReasonResult = mapStopReason(choice.finish_reason);
|
||||
const finishReasonResult = mapOpenAIStopReason(choice.finish_reason, {
|
||||
allowSingularToolCall: true,
|
||||
});
|
||||
output.stopReason = finishReasonResult.stopReason;
|
||||
if (finishReasonResult.stopReason === "stop") {
|
||||
sawStopFinishReason = true;
|
||||
@@ -4478,32 +4481,6 @@ function hasOpenAICompletionsReasoningUsageActivity(
|
||||
);
|
||||
}
|
||||
|
||||
function mapStopReason(reason: string | null) {
|
||||
if (reason === null) {
|
||||
return { stopReason: "stop" };
|
||||
}
|
||||
switch (reason) {
|
||||
case "stop":
|
||||
case "end":
|
||||
return { stopReason: "stop" };
|
||||
case "length":
|
||||
return { stopReason: "length" };
|
||||
case "function_call":
|
||||
case "tool_call":
|
||||
case "tool_calls":
|
||||
return { stopReason: "toolUse" };
|
||||
case "content_filter":
|
||||
return { stopReason: "error", errorMessage: "Provider finish_reason: content_filter" };
|
||||
case "network_error":
|
||||
return { stopReason: "error", errorMessage: "Provider finish_reason: network_error" };
|
||||
default:
|
||||
return {
|
||||
stopReason: "error",
|
||||
errorMessage: `Provider finish_reason: ${reason}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const testing = {
|
||||
getCompat,
|
||||
assertCodeModeResponsesToolSurface,
|
||||
|
||||
@@ -33,7 +33,6 @@ import type {
|
||||
Model,
|
||||
OpenAICompletionsCompat,
|
||||
SimpleStreamOptions,
|
||||
StopReason,
|
||||
StreamFunction,
|
||||
StreamOptions,
|
||||
TextContent,
|
||||
@@ -50,6 +49,7 @@ import { resolveCacheRetention } from "./cache-retention.js";
|
||||
import { isCloudflareProvider, resolveCloudflareBaseUrl } from "./cloudflare.js";
|
||||
import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js";
|
||||
import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.js";
|
||||
import { mapOpenAIStopReason } from "./openai-stop-reason.js";
|
||||
import { buildBaseOptions } from "./simple-options.js";
|
||||
import { transformMessages } from "./transform-messages.js";
|
||||
|
||||
@@ -372,7 +372,7 @@ export const streamOpenAICompletions: StreamFunction<
|
||||
}
|
||||
|
||||
if (choice.finish_reason) {
|
||||
const finishReasonResult = mapStopReason(choice.finish_reason);
|
||||
const finishReasonResult = mapOpenAIStopReason(choice.finish_reason);
|
||||
output.stopReason = finishReasonResult.stopReason;
|
||||
if (finishReasonResult.errorMessage) {
|
||||
output.errorMessage = finishReasonResult.errorMessage;
|
||||
@@ -1243,34 +1243,6 @@ function parseChunkUsage(
|
||||
return usage;
|
||||
}
|
||||
|
||||
function mapStopReason(reason: string): {
|
||||
stopReason: StopReason;
|
||||
errorMessage?: string;
|
||||
} {
|
||||
if (reason === null) {
|
||||
return { stopReason: "stop" };
|
||||
}
|
||||
switch (reason) {
|
||||
case "stop":
|
||||
case "end":
|
||||
return { stopReason: "stop" };
|
||||
case "length":
|
||||
return { stopReason: "length" };
|
||||
case "function_call":
|
||||
case "tool_calls":
|
||||
return { stopReason: "toolUse" };
|
||||
case "content_filter":
|
||||
return { stopReason: "error", errorMessage: "Provider finish_reason: content_filter" };
|
||||
case "network_error":
|
||||
return { stopReason: "error", errorMessage: "Provider finish_reason: network_error" };
|
||||
default:
|
||||
return {
|
||||
stopReason: "error",
|
||||
errorMessage: `Provider finish_reason: ${reason}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect compatibility settings from provider and baseUrl for known providers.
|
||||
* Provider takes precedence over URL-based detection since it's explicitly configured.
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { mapOpenAIStopReason } from "./openai-stop-reason.js";
|
||||
|
||||
describe("mapOpenAIStopReason", () => {
|
||||
it.each([
|
||||
["stop", { stopReason: "stop" }],
|
||||
["end", { stopReason: "stop" }],
|
||||
["length", { stopReason: "length" }],
|
||||
["function_call", { stopReason: "toolUse" }],
|
||||
["tool_calls", { stopReason: "toolUse" }],
|
||||
[null, { stopReason: "stop" }],
|
||||
] as const)("maps %s", (reason, expected) => {
|
||||
expect(mapOpenAIStopReason(reason)).toEqual(expected);
|
||||
});
|
||||
|
||||
it("keeps singular tool_call opt-in", () => {
|
||||
expect(mapOpenAIStopReason("tool_call")).toEqual({
|
||||
stopReason: "error",
|
||||
errorMessage: "Provider finish_reason: tool_call",
|
||||
});
|
||||
expect(mapOpenAIStopReason("tool_call", { allowSingularToolCall: true })).toEqual({
|
||||
stopReason: "toolUse",
|
||||
});
|
||||
});
|
||||
|
||||
it("surfaces provider errors and unknown reasons", () => {
|
||||
expect(mapOpenAIStopReason("content_filter")).toEqual({
|
||||
stopReason: "error",
|
||||
errorMessage: "Provider finish_reason: content_filter",
|
||||
});
|
||||
expect(mapOpenAIStopReason("unexpected")).toEqual({
|
||||
stopReason: "error",
|
||||
errorMessage: "Provider finish_reason: unexpected",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { StopReason } from "../types.js";
|
||||
|
||||
export type OpenAIStopReasonResult = {
|
||||
stopReason: StopReason;
|
||||
errorMessage?: string;
|
||||
};
|
||||
|
||||
export function mapOpenAIStopReason(
|
||||
reason: string | null,
|
||||
options?: { allowSingularToolCall?: boolean },
|
||||
): OpenAIStopReasonResult {
|
||||
if (reason === null) {
|
||||
return { stopReason: "stop" };
|
||||
}
|
||||
|
||||
switch (reason) {
|
||||
case "stop":
|
||||
case "end":
|
||||
return { stopReason: "stop" };
|
||||
case "length":
|
||||
return { stopReason: "length" };
|
||||
case "function_call":
|
||||
case "tool_calls":
|
||||
return { stopReason: "toolUse" };
|
||||
case "tool_call":
|
||||
if (options?.allowSingularToolCall) {
|
||||
return { stopReason: "toolUse" };
|
||||
}
|
||||
break;
|
||||
case "content_filter":
|
||||
return { stopReason: "error", errorMessage: "Provider finish_reason: content_filter" };
|
||||
case "network_error":
|
||||
return { stopReason: "error", errorMessage: "Provider finish_reason: network_error" };
|
||||
}
|
||||
|
||||
return {
|
||||
stopReason: "error",
|
||||
errorMessage: `Provider finish_reason: ${reason}`,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user