fix(api-ai): support responses style openai payloads
This commit is contained in:
@@ -122,6 +122,20 @@ export class OpenAiCompatibleProvider implements AiChannelExecutor {
|
||||
}
|
||||
|
||||
private extractAssistantText(payload: unknown): string {
|
||||
const chatCompletionText = this.extractChatCompletionText(payload);
|
||||
if (chatCompletionText) {
|
||||
return chatCompletionText;
|
||||
}
|
||||
|
||||
const responsesText = this.extractResponsesApiText(payload);
|
||||
if (responsesText) {
|
||||
return responsesText;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
private extractChatCompletionText(payload: unknown): string {
|
||||
if (!this.isRecord(payload)) {
|
||||
return "";
|
||||
}
|
||||
@@ -137,11 +151,49 @@ export class OpenAiCompatibleProvider implements AiChannelExecutor {
|
||||
}
|
||||
|
||||
const message = firstChoice["message"];
|
||||
if (!this.isRecord(message)) {
|
||||
if (this.isRecord(message)) {
|
||||
const messageContent = this.extractMessageContent(message["content"]);
|
||||
if (messageContent) {
|
||||
return messageContent;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof firstChoice["text"] === "string") {
|
||||
return firstChoice["text"];
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
return this.extractMessageContent(message["content"]);
|
||||
private extractResponsesApiText(payload: unknown): string {
|
||||
if (!this.isRecord(payload)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (typeof payload["output_text"] === "string") {
|
||||
return payload["output_text"];
|
||||
}
|
||||
|
||||
const output = payload["output"];
|
||||
if (!Array.isArray(output)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return output
|
||||
.map((item) => {
|
||||
if (!this.isRecord(item)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (typeof item["text"] === "string") {
|
||||
return item["text"];
|
||||
}
|
||||
|
||||
return this.extractMessageContent(item["content"]);
|
||||
})
|
||||
.filter((item) => item.length > 0)
|
||||
.join("\n")
|
||||
.trim();
|
||||
}
|
||||
|
||||
private extractMessageContent(content: unknown): string {
|
||||
@@ -154,7 +206,13 @@ export class OpenAiCompatibleProvider implements AiChannelExecutor {
|
||||
}
|
||||
|
||||
return content
|
||||
.map((item) => {
|
||||
.map((item) => this.extractContentPartText(item))
|
||||
.filter((item) => item.length > 0)
|
||||
.join("\n")
|
||||
.trim();
|
||||
}
|
||||
|
||||
private extractContentPartText(item: unknown): string {
|
||||
if (!this.isRecord(item)) {
|
||||
return "";
|
||||
}
|
||||
@@ -163,10 +221,19 @@ export class OpenAiCompatibleProvider implements AiChannelExecutor {
|
||||
return item["text"];
|
||||
}
|
||||
|
||||
if (this.isRecord(item["text"]) && typeof item["text"]["value"] === "string") {
|
||||
return item["text"]["value"];
|
||||
}
|
||||
|
||||
if (typeof item["content"] === "string") {
|
||||
return item["content"];
|
||||
}
|
||||
|
||||
if (this.isRecord(item["content"]) && typeof item["content"]["text"] === "string") {
|
||||
return item["content"]["text"];
|
||||
}
|
||||
|
||||
return "";
|
||||
})
|
||||
.filter((item) => item.length > 0)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
private extractModel(payload: unknown): string | null {
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { AiChannel } from "../generated/prisma/client";
|
||||
import { OpenAiCompatibleProvider } from "../src/ai/providers/openai-compatible.provider";
|
||||
|
||||
describe("OpenAiCompatibleProvider", () => {
|
||||
const originalFetch = global.fetch;
|
||||
|
||||
afterEach(() => {
|
||||
global.fetch = originalFetch;
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should read text from responses style payload when chat content is empty", async () => {
|
||||
const provider = new OpenAiCompatibleProvider();
|
||||
const fetchMock = jest.fn(async (_input: unknown, init?: RequestInit) => {
|
||||
expect(init?.method).toBe("POST");
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
id: "resp_123",
|
||||
object: "response",
|
||||
model: "gpt-5.4",
|
||||
output: [
|
||||
{
|
||||
id: "msg_123",
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "output_text",
|
||||
text: "今天优先先完成截止时间最近的任务。"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
usage: {
|
||||
prompt_tokens: 15,
|
||||
completion_tokens: 9,
|
||||
total_tokens: 24
|
||||
}
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
global.fetch = fetchMock as typeof global.fetch;
|
||||
|
||||
const result = await provider.execute(
|
||||
{
|
||||
channel: AiChannel.USER_KEY,
|
||||
source: "binding",
|
||||
sourceId: "binding_user_key_1",
|
||||
providerName: "airouter",
|
||||
model: "gpt-5.4",
|
||||
configId: null,
|
||||
configName: null,
|
||||
endpoint: "https://api.airouter.io/v1",
|
||||
apiKey: "sk_test"
|
||||
},
|
||||
{
|
||||
userId: "user_1",
|
||||
message: "帮我安排今天的任务",
|
||||
sessionId: null
|
||||
}
|
||||
);
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
expect(result.content).toBe("今天优先先完成截止时间最近的任务。");
|
||||
expect(result.model).toBe("gpt-5.4");
|
||||
expect(result.usage).toEqual({
|
||||
promptTokens: 15,
|
||||
completionTokens: 9,
|
||||
totalTokens: 24
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user