@arcote.tech/arc-ai-openai 0.4.9 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/index.ts +190 -133
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcote.tech/arc-ai-openai",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.5.0",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "OpenAI adapter for Arc AI framework",
|
|
7
7
|
"main": "./src/index.ts",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"type-check": "tsc --noEmit"
|
|
11
11
|
},
|
|
12
12
|
"peerDependencies": {
|
|
13
|
-
"@arcote.tech/arc-ai": "^0.
|
|
13
|
+
"@arcote.tech/arc-ai": "^0.5.0",
|
|
14
14
|
"typescript": "^5.0.0"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
ToolCall,
|
|
7
7
|
TokenUsage,
|
|
8
8
|
FinishReason,
|
|
9
|
+
Message,
|
|
9
10
|
} from "@arcote.tech/arc-ai";
|
|
10
11
|
|
|
11
12
|
// ─── Config ──────────────────────────────────────────────────────
|
|
@@ -16,7 +17,7 @@ export interface OpenAIConfig {
|
|
|
16
17
|
defaultModel?: string;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
// ─── Adapter
|
|
20
|
+
// ─── Adapter (Responses API) ────────────────────────────────────
|
|
20
21
|
|
|
21
22
|
export function openai(config: OpenAIConfig): LLMProvider {
|
|
22
23
|
const baseUrl = config.baseUrl ?? "https://api.openai.com/v1";
|
|
@@ -27,72 +28,126 @@ export function openai(config: OpenAIConfig): LLMProvider {
|
|
|
27
28
|
if (!tools || tools.length === 0) return undefined;
|
|
28
29
|
return tools.map((t) => ({
|
|
29
30
|
type: "function",
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
parameters: t.parameters,
|
|
34
|
-
},
|
|
31
|
+
name: t.name,
|
|
32
|
+
description: t.description,
|
|
33
|
+
parameters: t.parameters,
|
|
35
34
|
}));
|
|
36
35
|
}
|
|
37
36
|
|
|
38
|
-
function parseUsage(
|
|
39
|
-
const usage = raw.usage ?? {};
|
|
37
|
+
function parseUsage(usage: any): TokenUsage {
|
|
40
38
|
return {
|
|
41
|
-
inputTokens: usage
|
|
42
|
-
outputTokens: usage
|
|
43
|
-
totalTokens: usage
|
|
44
|
-
cachedTokens: usage
|
|
45
|
-
reasoningTokens:
|
|
46
|
-
usage.completion_tokens_details?.reasoning_tokens ?? 0,
|
|
39
|
+
inputTokens: usage?.input_tokens ?? 0,
|
|
40
|
+
outputTokens: usage?.output_tokens ?? 0,
|
|
41
|
+
totalTokens: (usage?.input_tokens ?? 0) + (usage?.output_tokens ?? 0),
|
|
42
|
+
cachedTokens: usage?.input_tokens_details?.cached_tokens ?? 0,
|
|
43
|
+
reasoningTokens: usage?.output_tokens_details?.reasoning_tokens ?? 0,
|
|
47
44
|
};
|
|
48
45
|
}
|
|
49
46
|
|
|
50
|
-
function
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
47
|
+
function buildInput(
|
|
48
|
+
messages: Message[],
|
|
49
|
+
previousResponseId?: string,
|
|
50
|
+
): { instructions?: string; input: unknown; previous_response_id?: string } {
|
|
51
|
+
// Extract system message as instructions
|
|
52
|
+
const systemMsg = messages.find((m) => m.role === "system");
|
|
53
|
+
const nonSystemMessages = messages.filter((m) => m.role !== "system");
|
|
54
|
+
|
|
55
|
+
if (previousResponseId) {
|
|
56
|
+
// Continuation — send only new messages (tool results or new user message)
|
|
57
|
+
const newMessages = nonSystemMessages.filter(
|
|
58
|
+
(m) => m.role === "tool" || m === nonSystemMessages[nonSystemMessages.length - 1],
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const input = newMessages.map((m) => {
|
|
62
|
+
if (m.role === "tool" && m.toolCallId) {
|
|
63
|
+
return {
|
|
64
|
+
type: "function_call_output",
|
|
65
|
+
call_id: m.toolCallId,
|
|
66
|
+
output: m.content,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
type: "message",
|
|
71
|
+
role: m.role,
|
|
72
|
+
content: m.content,
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
instructions: systemMsg?.content,
|
|
78
|
+
input,
|
|
79
|
+
previous_response_id: previousResponseId,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// First call — send all messages
|
|
84
|
+
const input = nonSystemMessages.map((m) => ({
|
|
85
|
+
type: "message",
|
|
86
|
+
role: m.role === "tool" ? "user" : m.role,
|
|
87
|
+
content: m.content,
|
|
56
88
|
}));
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
instructions: systemMsg?.content,
|
|
92
|
+
input,
|
|
93
|
+
};
|
|
57
94
|
}
|
|
58
95
|
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
96
|
+
function extractToolCallsFromOutput(output: any[]): ToolCall[] {
|
|
97
|
+
return output
|
|
98
|
+
.filter((item: any) => item.type === "function_call")
|
|
99
|
+
.map((item: any) => {
|
|
100
|
+
let args: Record<string, unknown> = {};
|
|
101
|
+
try {
|
|
102
|
+
args = JSON.parse(item.arguments);
|
|
103
|
+
} catch {}
|
|
104
|
+
return {
|
|
105
|
+
id: item.call_id,
|
|
106
|
+
name: item.name,
|
|
107
|
+
arguments: args,
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function extractContentFromOutput(output: any[]): string {
|
|
113
|
+
return output
|
|
114
|
+
.filter((item: any) => item.type === "message")
|
|
115
|
+
.flatMap((item: any) =>
|
|
116
|
+
(item.content ?? [])
|
|
117
|
+
.filter((c: any) => c.type === "output_text")
|
|
118
|
+
.map((c: any) => c.text),
|
|
119
|
+
)
|
|
120
|
+
.join("");
|
|
70
121
|
}
|
|
71
122
|
|
|
72
|
-
async function complete(
|
|
123
|
+
async function complete(
|
|
124
|
+
request: CompletionRequest,
|
|
125
|
+
): Promise<CompletionResult> {
|
|
126
|
+
const { instructions, input, previous_response_id } = buildInput(
|
|
127
|
+
request.messages,
|
|
128
|
+
request.previousResponseId,
|
|
129
|
+
);
|
|
130
|
+
|
|
73
131
|
const body: Record<string, unknown> = {
|
|
74
132
|
model: request.model,
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
133
|
+
input,
|
|
134
|
+
...(instructions ? { instructions } : {}),
|
|
135
|
+
...(previous_response_id ? { previous_response_id } : {}),
|
|
136
|
+
...(request.temperature != null
|
|
137
|
+
? { temperature: request.temperature }
|
|
138
|
+
: {}),
|
|
139
|
+
...(request.maxTokens != null
|
|
140
|
+
? { max_output_tokens: request.maxTokens }
|
|
141
|
+
: {}),
|
|
83
142
|
};
|
|
84
143
|
|
|
85
144
|
const tools = translateTools(request.tools);
|
|
86
145
|
if (tools) body.tools = tools;
|
|
87
|
-
|
|
88
146
|
if (request.webSearch) {
|
|
89
|
-
body.tools = [
|
|
90
|
-
...(tools ?? []),
|
|
91
|
-
{ type: "web_search_preview" },
|
|
92
|
-
];
|
|
147
|
+
body.tools = [...(tools ?? []), { type: "web_search_preview" }];
|
|
93
148
|
}
|
|
94
149
|
|
|
95
|
-
const response = await fetch(`${baseUrl}/
|
|
150
|
+
const response = await fetch(`${baseUrl}/responses`, {
|
|
96
151
|
method: "POST",
|
|
97
152
|
headers: {
|
|
98
153
|
"Content-Type": "application/json",
|
|
@@ -106,14 +161,16 @@ export function openai(config: OpenAIConfig): LLMProvider {
|
|
|
106
161
|
throw new Error(`OpenAI API error ${response.status}: ${error}`);
|
|
107
162
|
}
|
|
108
163
|
|
|
109
|
-
const data = await response.json()
|
|
110
|
-
const
|
|
164
|
+
const data = (await response.json());
|
|
165
|
+
const toolCalls = extractToolCallsFromOutput(data.output ?? []);
|
|
166
|
+
const hasToolCalls = toolCalls.length > 0;
|
|
111
167
|
|
|
112
168
|
return {
|
|
113
|
-
content:
|
|
114
|
-
toolCalls
|
|
115
|
-
usage: parseUsage(data),
|
|
116
|
-
finishReason:
|
|
169
|
+
content: extractContentFromOutput(data.output ?? []),
|
|
170
|
+
toolCalls,
|
|
171
|
+
usage: parseUsage(data.usage),
|
|
172
|
+
finishReason: hasToolCalls ? "tool_call" : "stop",
|
|
173
|
+
responseId: data.id,
|
|
117
174
|
};
|
|
118
175
|
}
|
|
119
176
|
|
|
@@ -121,31 +178,32 @@ export function openai(config: OpenAIConfig): LLMProvider {
|
|
|
121
178
|
request: CompletionRequest,
|
|
122
179
|
onChunk: (chunk: StreamChunk) => void,
|
|
123
180
|
): Promise<CompletionResult> {
|
|
181
|
+
const { instructions, input, previous_response_id } = buildInput(
|
|
182
|
+
request.messages,
|
|
183
|
+
request.previousResponseId,
|
|
184
|
+
);
|
|
185
|
+
|
|
124
186
|
const body: Record<string, unknown> = {
|
|
125
187
|
model: request.model,
|
|
126
|
-
|
|
127
|
-
role: m.role,
|
|
128
|
-
content: m.content,
|
|
129
|
-
...(m.name ? { name: m.name } : {}),
|
|
130
|
-
...(m.toolCallId ? { tool_call_id: m.toolCallId } : {}),
|
|
131
|
-
})),
|
|
132
|
-
temperature: request.temperature,
|
|
133
|
-
max_tokens: request.maxTokens,
|
|
188
|
+
input,
|
|
134
189
|
stream: true,
|
|
135
|
-
|
|
190
|
+
...(instructions ? { instructions } : {}),
|
|
191
|
+
...(previous_response_id ? { previous_response_id } : {}),
|
|
192
|
+
...(request.temperature != null
|
|
193
|
+
? { temperature: request.temperature }
|
|
194
|
+
: {}),
|
|
195
|
+
...(request.maxTokens != null
|
|
196
|
+
? { max_output_tokens: request.maxTokens }
|
|
197
|
+
: {}),
|
|
136
198
|
};
|
|
137
199
|
|
|
138
200
|
const tools = translateTools(request.tools);
|
|
139
201
|
if (tools) body.tools = tools;
|
|
140
|
-
|
|
141
202
|
if (request.webSearch) {
|
|
142
|
-
body.tools = [
|
|
143
|
-
...(tools ?? []),
|
|
144
|
-
{ type: "web_search_preview" },
|
|
145
|
-
];
|
|
203
|
+
body.tools = [...(tools ?? []), { type: "web_search_preview" }];
|
|
146
204
|
}
|
|
147
205
|
|
|
148
|
-
const response = await fetch(`${baseUrl}/
|
|
206
|
+
const response = await fetch(`${baseUrl}/responses`, {
|
|
149
207
|
method: "POST",
|
|
150
208
|
headers: {
|
|
151
209
|
"Content-Type": "application/json",
|
|
@@ -160,6 +218,7 @@ export function openai(config: OpenAIConfig): LLMProvider {
|
|
|
160
218
|
}
|
|
161
219
|
|
|
162
220
|
let content = "";
|
|
221
|
+
let responseId = "";
|
|
163
222
|
let finishReason: FinishReason = "stop";
|
|
164
223
|
let usage: TokenUsage = {
|
|
165
224
|
inputTokens: 0,
|
|
@@ -168,11 +227,8 @@ export function openai(config: OpenAIConfig): LLMProvider {
|
|
|
168
227
|
cachedTokens: 0,
|
|
169
228
|
reasoningTokens: 0,
|
|
170
229
|
};
|
|
171
|
-
const toolCallBuffers = new Map<
|
|
172
|
-
number,
|
|
173
|
-
{ id: string; name: string; arguments: string }
|
|
174
|
-
>();
|
|
175
230
|
const completedToolCalls: ToolCall[] = [];
|
|
231
|
+
const toolCallArgBuffers = new Map<string, string>();
|
|
176
232
|
|
|
177
233
|
const reader = response.body!.getReader();
|
|
178
234
|
const decoder = new TextDecoder();
|
|
@@ -189,79 +245,68 @@ export function openai(config: OpenAIConfig): LLMProvider {
|
|
|
189
245
|
for (const line of lines) {
|
|
190
246
|
if (!line.startsWith("data: ")) continue;
|
|
191
247
|
const data = line.slice(6).trim();
|
|
192
|
-
if (data
|
|
248
|
+
if (!data) continue;
|
|
193
249
|
|
|
194
250
|
try {
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
// Usage-only chunk (last chunk)
|
|
198
|
-
if (parsed.usage && !parsed.choices?.length) {
|
|
199
|
-
usage = parseUsage(parsed);
|
|
200
|
-
onChunk({ type: "usage_update", usage });
|
|
201
|
-
continue;
|
|
202
|
-
}
|
|
251
|
+
const event = JSON.parse(data);
|
|
203
252
|
|
|
204
|
-
|
|
205
|
-
|
|
253
|
+
switch (event.type) {
|
|
254
|
+
case "response.output_text.delta":
|
|
255
|
+
if (event.delta) {
|
|
256
|
+
content += event.delta;
|
|
257
|
+
onChunk({ type: "content_delta", content: event.delta });
|
|
258
|
+
}
|
|
259
|
+
break;
|
|
206
260
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
261
|
+
case "response.function_call_arguments.delta":
|
|
262
|
+
if (event.call_id && event.delta) {
|
|
263
|
+
const existing = toolCallArgBuffers.get(event.call_id) ?? "";
|
|
264
|
+
toolCallArgBuffers.set(event.call_id, existing + event.delta);
|
|
265
|
+
}
|
|
266
|
+
break;
|
|
212
267
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const idx = tc.index;
|
|
217
|
-
if (tc.id) {
|
|
218
|
-
// First chunk for this tool call
|
|
219
|
-
toolCallBuffers.set(idx, {
|
|
220
|
-
id: tc.id,
|
|
221
|
-
name: tc.function?.name ?? "",
|
|
222
|
-
arguments: tc.function?.arguments ?? "",
|
|
223
|
-
});
|
|
268
|
+
case "response.output_item.added":
|
|
269
|
+
if (event.item?.type === "function_call") {
|
|
270
|
+
toolCallArgBuffers.set(event.item.call_id, "");
|
|
224
271
|
onChunk({
|
|
225
272
|
type: "tool_call_start",
|
|
226
|
-
toolCall: {
|
|
273
|
+
toolCall: {
|
|
274
|
+
id: event.item.call_id,
|
|
275
|
+
name: event.item.name,
|
|
276
|
+
arguments: {},
|
|
277
|
+
},
|
|
227
278
|
});
|
|
228
|
-
} else {
|
|
229
|
-
// Continuation chunk
|
|
230
|
-
const buf = toolCallBuffers.get(idx);
|
|
231
|
-
if (buf && tc.function?.arguments) {
|
|
232
|
-
buf.arguments += tc.function.arguments;
|
|
233
|
-
onChunk({
|
|
234
|
-
type: "tool_call_delta",
|
|
235
|
-
content: tc.function.arguments,
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
279
|
}
|
|
239
|
-
|
|
240
|
-
|
|
280
|
+
break;
|
|
281
|
+
|
|
282
|
+
case "response.output_item.done":
|
|
283
|
+
if (event.item?.type === "function_call") {
|
|
284
|
+
const argsStr =
|
|
285
|
+
toolCallArgBuffers.get(event.item.call_id) ??
|
|
286
|
+
event.item.arguments ??
|
|
287
|
+
"{}";
|
|
288
|
+
let args: Record<string, unknown> = {};
|
|
289
|
+
try {
|
|
290
|
+
args = JSON.parse(argsStr);
|
|
291
|
+
} catch {}
|
|
292
|
+
completedToolCalls.push({
|
|
293
|
+
id: event.item.call_id,
|
|
294
|
+
name: event.item.name,
|
|
295
|
+
arguments: args,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
break;
|
|
241
299
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
300
|
+
case "response.completed":
|
|
301
|
+
responseId = event.response?.id ?? "";
|
|
302
|
+
usage = parseUsage(event.response?.usage);
|
|
303
|
+
onChunk({ type: "usage_update", usage });
|
|
246
304
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
try {
|
|
251
|
-
completedToolCalls.push({
|
|
252
|
-
id: buf.id,
|
|
253
|
-
name: buf.name,
|
|
254
|
-
arguments: JSON.parse(buf.arguments),
|
|
255
|
-
});
|
|
256
|
-
} catch {
|
|
257
|
-
completedToolCalls.push({
|
|
258
|
-
id: buf.id,
|
|
259
|
-
name: buf.name,
|
|
260
|
-
arguments: {},
|
|
261
|
-
});
|
|
262
|
-
}
|
|
305
|
+
// Extract content from completed response if not streamed
|
|
306
|
+
if (!content && event.response?.output) {
|
|
307
|
+
content = extractContentFromOutput(event.response.output);
|
|
263
308
|
}
|
|
264
|
-
|
|
309
|
+
break;
|
|
265
310
|
}
|
|
266
311
|
} catch {
|
|
267
312
|
// Skip malformed JSON
|
|
@@ -269,17 +314,29 @@ export function openai(config: OpenAIConfig): LLMProvider {
|
|
|
269
314
|
}
|
|
270
315
|
}
|
|
271
316
|
|
|
317
|
+
finishReason = completedToolCalls.length > 0 ? "tool_call" : "stop";
|
|
318
|
+
|
|
272
319
|
return {
|
|
273
320
|
content,
|
|
274
321
|
toolCalls: completedToolCalls,
|
|
275
322
|
usage,
|
|
276
323
|
finishReason,
|
|
324
|
+
responseId,
|
|
277
325
|
};
|
|
278
326
|
}
|
|
279
327
|
|
|
280
328
|
return {
|
|
281
329
|
name: "openai",
|
|
282
|
-
models: [
|
|
330
|
+
models: [
|
|
331
|
+
"gpt-4o",
|
|
332
|
+
"gpt-4o-mini",
|
|
333
|
+
"o3",
|
|
334
|
+
"o3-mini",
|
|
335
|
+
"gpt-4.1",
|
|
336
|
+
"gpt-4.1-mini",
|
|
337
|
+
"gpt-4.1-nano",
|
|
338
|
+
"gpt-5.4-nano",
|
|
339
|
+
],
|
|
283
340
|
complete,
|
|
284
341
|
streamComplete,
|
|
285
342
|
};
|