Skip to content

Commit 8c38fb2

Browse files
committed
feat: migrate openai to the responses endpoint
1 parent ededb96 commit 8c38fb2

File tree

11 files changed

+450
-170
lines changed

11 files changed

+450
-170
lines changed
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { lng } from '../../../lng';
2+
3+
import type { ServiceHandler } from '../../types';
4+
5+
type CompletionsData = {
6+
id: string;
7+
choices?: {
8+
message?: {
9+
content?: string | null;
10+
tool_calls?: {
11+
id: string;
12+
type: string;
13+
function: {
14+
name: string;
15+
arguments: string;
16+
};
17+
}[];
18+
};
19+
}[];
20+
usage: {
21+
prompt_tokens: number;
22+
completion_tokens: number;
23+
};
24+
};
25+
26+
type ImageData = {
27+
data?: {
28+
url?: string;
29+
b64_json?: string;
30+
}[];
31+
};
32+
33+
export const legacyOpenai: ServiceHandler<CompletionsData, ImageData> = {
34+
content: (data) => {
35+
const content = data?.choices?.[0]?.message?.content;
36+
const tools = data?.choices?.[0]?.message?.tool_calls;
37+
38+
if (!content && !tools) {
39+
throw new Error(lng('modai.error.failed_request'));
40+
}
41+
42+
const id = data.id;
43+
44+
if (!tools) {
45+
return {
46+
__type: 'TextDataNoTools',
47+
id,
48+
content: content as string,
49+
usage: {
50+
completionTokens: data?.usage.completion_tokens,
51+
promptTokens: data?.usage.prompt_tokens,
52+
},
53+
};
54+
}
55+
56+
if (!content) {
57+
return {
58+
__type: 'ToolsData',
59+
id,
60+
toolCalls: tools.map((tool) => ({
61+
id: tool.id,
62+
name: tool.function.name,
63+
arguments: tool.function.arguments,
64+
})),
65+
usage: {
66+
completionTokens: data?.usage.completion_tokens,
67+
promptTokens: data?.usage.prompt_tokens,
68+
},
69+
};
70+
}
71+
72+
return {
73+
__type: 'TextDataMaybeTools',
74+
id,
75+
content: content,
76+
toolCalls: tools.map((tool) => ({
77+
id: tool.id,
78+
name: tool.function.name,
79+
arguments: tool.function.arguments,
80+
})),
81+
usage: {
82+
completionTokens: data?.usage.completion_tokens,
83+
promptTokens: data?.usage.prompt_tokens,
84+
},
85+
};
86+
},
87+
image: (data) => {
88+
let url = data?.data?.[0]?.url;
89+
90+
if (!url) {
91+
url = data?.data?.[0]?.b64_json;
92+
93+
if (!url) {
94+
throw new Error(lng('modai.error.failed_request'));
95+
}
96+
97+
url = `data:image/png;base64,${url}`;
98+
}
99+
100+
return {
101+
__type: 'ImageData',
102+
id: crypto.randomUUID(),
103+
url,
104+
};
105+
},
106+
};

_build/js/src/executor/services/handlers/openai.ts

Lines changed: 21 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,15 @@
11
import { lng } from '../../../lng';
2+
import { OutputItem } from '../../types/openai';
23

34
import type { ServiceHandler } from '../../types';
45

56
type CompletionsData = {
67
id: string;
7-
choices?: {
8-
message?: {
9-
content?: string | null;
10-
tool_calls?: {
11-
id: string;
12-
type: string;
13-
function: {
14-
name: string;
15-
arguments: string;
16-
};
17-
}[];
18-
};
19-
}[];
8+
output: OutputItem[];
209
usage: {
21-
prompt_tokens: number;
22-
completion_tokens: number;
10+
input_tokens: number;
11+
output_tokens: number;
12+
total_tokens: number;
2313
};
2414
};
2515

@@ -32,55 +22,55 @@ type ImageData = {
3222

3323
export const openai: ServiceHandler<CompletionsData, ImageData> = {
3424
content: (data) => {
35-
const content = data?.choices?.[0]?.message?.content;
36-
const tools = data?.choices?.[0]?.message?.tool_calls;
25+
const content = data?.output.filter((item) => item.type === 'message');
26+
const tools = data?.output.filter((item) => item.type === 'function_call');
3727

3828
if (!content && !tools) {
3929
throw new Error(lng('modai.error.failed_request'));
4030
}
4131

4232
const id = data.id;
4333

44-
if (!tools) {
34+
if (!tools || tools.length === 0) {
4535
return {
4636
__type: 'TextDataNoTools',
4737
id,
48-
content: content as string,
38+
content: content[0].content[0].text,
4939
usage: {
50-
completionTokens: data?.usage.completion_tokens,
51-
promptTokens: data?.usage.prompt_tokens,
40+
completionTokens: data?.usage.output_tokens,
41+
promptTokens: data?.usage.input_tokens,
5242
},
5343
};
5444
}
5545

56-
if (!content) {
46+
if (!content || content.length === 0) {
5747
return {
5848
__type: 'ToolsData',
5949
id,
6050
toolCalls: tools.map((tool) => ({
6151
id: tool.id,
62-
name: tool.function.name,
63-
arguments: tool.function.arguments,
52+
name: tool.name,
53+
arguments: tool.arguments,
6454
})),
6555
usage: {
66-
completionTokens: data?.usage.completion_tokens,
67-
promptTokens: data?.usage.prompt_tokens,
56+
completionTokens: data?.usage.output_tokens,
57+
promptTokens: data?.usage.input_tokens,
6858
},
6959
};
7060
}
7161

7262
return {
7363
__type: 'TextDataMaybeTools',
7464
id,
75-
content: content,
65+
content: content[0].content[0].text,
7666
toolCalls: tools.map((tool) => ({
7767
id: tool.id,
78-
name: tool.function.name,
79-
arguments: tool.function.arguments,
68+
name: tool.name,
69+
arguments: tool.arguments,
8070
})),
8171
usage: {
82-
completionTokens: data?.usage.completion_tokens,
83-
promptTokens: data?.usage.prompt_tokens,
72+
completionTokens: data?.usage.output_tokens,
73+
promptTokens: data?.usage.input_tokens,
8474
},
8575
};
8676
},

_build/js/src/executor/services/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { anthropic } from './handlers/anthropic';
22
import { google } from './handlers/google';
3+
import { legacyOpenai } from './handlers/legacyOpenai';
34
import { openai } from './handlers/openai';
45
import { lng } from '../../lng';
56
import { openrouter } from './handlers/openrouter';
@@ -11,6 +12,7 @@ const services = {
1112
google,
1213
anthropic,
1314
openrouter,
15+
legacyOpenai,
1416
};
1517

1618
export const getServiceParser = (
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import type { StreamHandler } from '../../types';
2+
3+
type StreamData = {
4+
id: string;
5+
choices?: {
6+
delta?: {
7+
content?: string | null;
8+
tool_calls?: {
9+
index: number;
10+
id: string;
11+
type: string;
12+
function: {
13+
name: string;
14+
arguments: string;
15+
};
16+
}[];
17+
};
18+
}[];
19+
usage: null | {
20+
prompt_tokens: number;
21+
completion_tokens: number;
22+
};
23+
};
24+
25+
export const legacyOpenai: StreamHandler = (chunk, buffer, currentData) => {
26+
buffer += chunk;
27+
let lastNewlineIndex = 0;
28+
let newlineIndex;
29+
30+
while ((newlineIndex = buffer.indexOf('\n', lastNewlineIndex)) !== -1) {
31+
const line = buffer.slice(lastNewlineIndex, newlineIndex).trim();
32+
lastNewlineIndex = newlineIndex + 1;
33+
34+
if (line.startsWith('data: ')) {
35+
const data = line.slice(6);
36+
37+
if (data === '[DONE]') {
38+
continue;
39+
}
40+
41+
try {
42+
const parsedData = JSON.parse(data) as StreamData;
43+
44+
if (parsedData?.usage) {
45+
currentData.usage = {
46+
completionTokens: parsedData?.usage?.completion_tokens || 0,
47+
promptTokens: parsedData?.usage?.prompt_tokens || 0,
48+
};
49+
}
50+
51+
if (
52+
!parsedData?.choices?.[0]?.delta?.tool_calls &&
53+
!parsedData?.choices?.[0]?.delta?.content
54+
) {
55+
continue;
56+
}
57+
58+
let content = '';
59+
let toolCalls = currentData.toolCalls || undefined;
60+
61+
if (parsedData?.choices?.[0]?.delta?.tool_calls?.[0]) {
62+
if (!toolCalls) {
63+
toolCalls = [];
64+
}
65+
66+
const toolCall = parsedData.choices[0].delta.tool_calls[0];
67+
68+
if (!toolCalls[toolCall.index]) {
69+
toolCalls[toolCall.index] = {
70+
id: '',
71+
name: '',
72+
arguments: '',
73+
};
74+
}
75+
76+
if (toolCall.id) {
77+
toolCalls[toolCall.index].id = toolCall.id;
78+
}
79+
80+
if (toolCall.function.name) {
81+
toolCalls[toolCall.index].name = toolCall.function.name;
82+
}
83+
84+
if (toolCall.function.arguments) {
85+
toolCalls[toolCall.index].arguments += toolCall.function.arguments;
86+
}
87+
}
88+
89+
if (parsedData?.choices?.[0]?.delta?.content) {
90+
content = parsedData?.choices?.[0]?.delta?.content;
91+
}
92+
93+
currentData = {
94+
__type: 'TextDataMaybeTools',
95+
id: parsedData.id,
96+
content: (currentData.content ?? '') + content,
97+
toolCalls,
98+
usage: {
99+
completionTokens: currentData.usage.completionTokens ?? 0,
100+
promptTokens: currentData.usage.promptTokens ?? 0,
101+
},
102+
};
103+
} catch {
104+
/* empty */
105+
}
106+
}
107+
}
108+
109+
return { buffer: buffer.slice(lastNewlineIndex), currentData };
110+
};

0 commit comments

Comments
 (0)