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 {
|
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)) {
|
if (!this.isRecord(payload)) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -137,11 +151,49 @@ export class OpenAiCompatibleProvider implements AiChannelExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const message = firstChoice["message"];
|
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 "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractResponsesApiText(payload: unknown): string {
|
||||||
|
if (!this.isRecord(payload)) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.extractMessageContent(message["content"]);
|
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 {
|
private extractMessageContent(content: unknown): string {
|
||||||
@@ -154,19 +206,34 @@ export class OpenAiCompatibleProvider implements AiChannelExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return content
|
return content
|
||||||
.map((item) => {
|
.map((item) => this.extractContentPartText(item))
|
||||||
if (!this.isRecord(item)) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof item["text"] === "string") {
|
|
||||||
return item["text"];
|
|
||||||
}
|
|
||||||
|
|
||||||
return "";
|
|
||||||
})
|
|
||||||
.filter((item) => item.length > 0)
|
.filter((item) => item.length > 0)
|
||||||
.join("\n");
|
.join("\n")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractContentPartText(item: unknown): string {
|
||||||
|
if (!this.isRecord(item)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof item["text"] === "string") {
|
||||||
|
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 "";
|
||||||
}
|
}
|
||||||
|
|
||||||
private extractModel(payload: unknown): string | null {
|
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