@arcote.tech/arc-ai-openai 0.4.9 → 0.5.2
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 +200 -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.2",
|
|
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.2",
|
|
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,130 @@ 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
|
-
|
|
34
|
-
},
|
|
31
|
+
name: t.name,
|
|
32
|
+
description: t.description,
|
|
33
|
+
parameters: t.parameters,
|
|
34
|
+
strict: false,
|
|
35
35
|
}));
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
function parseUsage(
|
|
39
|
-
const usage = raw.usage ?? {};
|
|
38
|
+
function parseUsage(usage: any): TokenUsage {
|
|
40
39
|
return {
|
|
41
|
-
inputTokens: usage
|
|
42
|
-
outputTokens: usage
|
|
43
|
-
totalTokens: usage
|
|
44
|
-
cachedTokens: usage
|
|
45
|
-
reasoningTokens:
|
|
46
|
-
usage.completion_tokens_details?.reasoning_tokens ?? 0,
|
|
40
|
+
inputTokens: usage?.input_tokens ?? 0,
|
|
41
|
+
outputTokens: usage?.output_tokens ?? 0,
|
|
42
|
+
totalTokens: (usage?.input_tokens ?? 0) + (usage?.output_tokens ?? 0),
|
|
43
|
+
cachedTokens: usage?.input_tokens_details?.cached_tokens ?? 0,
|
|
44
|
+
reasoningTokens: usage?.output_tokens_details?.reasoning_tokens ?? 0,
|
|
47
45
|
};
|
|
48
46
|
}
|
|
49
47
|
|
|
50
|
-
function
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
48
|
+
function buildInput(
|
|
49
|
+
messages: Message[],
|
|
50
|
+
previousResponseId?: string,
|
|
51
|
+
): { instructions?: string; input: unknown; previous_response_id?: string } {
|
|
52
|
+
// Extract system message as instructions
|
|
53
|
+
const systemMsg = messages.find((m) => m.role === "system");
|
|
54
|
+
const nonSystemMessages = messages.filter((m) => m.role !== "system");
|
|
55
|
+
|
|
56
|
+
if (previousResponseId) {
|
|
57
|
+
// Continuation — send only new messages (tool results or new user message)
|
|
58
|
+
const newMessages = nonSystemMessages.filter(
|
|
59
|
+
(m) => m.role === "tool" || m === nonSystemMessages[nonSystemMessages.length - 1],
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const input = newMessages.map((m) => {
|
|
63
|
+
if (m.role === "tool" && m.toolCallId) {
|
|
64
|
+
return {
|
|
65
|
+
type: "function_call_output",
|
|
66
|
+
call_id: m.toolCallId,
|
|
67
|
+
output: m.content,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
type: "message",
|
|
72
|
+
role: m.role,
|
|
73
|
+
content: m.content,
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
instructions: systemMsg?.content,
|
|
79
|
+
input,
|
|
80
|
+
previous_response_id: previousResponseId,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// First call — send all messages
|
|
85
|
+
const input = nonSystemMessages.map((m) => ({
|
|
86
|
+
type: "message",
|
|
87
|
+
role: m.role === "tool" ? "user" : m.role,
|
|
88
|
+
content: m.content,
|
|
56
89
|
}));
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
instructions: systemMsg?.content,
|
|
93
|
+
input,
|
|
94
|
+
};
|
|
57
95
|
}
|
|
58
96
|
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
97
|
+
function extractToolCallsFromOutput(output: any[]): ToolCall[] {
|
|
98
|
+
return output
|
|
99
|
+
.filter((item: any) => item.type === "function_call")
|
|
100
|
+
.map((item: any) => {
|
|
101
|
+
let args: Record<string, unknown> = {};
|
|
102
|
+
try {
|
|
103
|
+
args = JSON.parse(item.arguments);
|
|
104
|
+
} catch {}
|
|
105
|
+
return {
|
|
106
|
+
id: item.call_id,
|
|
107
|
+
name: item.name,
|
|
108
|
+
arguments: args,
|
|
109
|
+
};
|
|
110
|
+
});
|
|
70
111
|
}
|
|
71
112
|
|
|
72
|
-
|
|
113
|
+
function extractContentFromOutput(output: any[]): string {
|
|
114
|
+
return output
|
|
115
|
+
.filter((item: any) => item.type === "message")
|
|
116
|
+
.flatMap((item: any) =>
|
|
117
|
+
(item.content ?? [])
|
|
118
|
+
.filter((c: any) => c.type === "output_text")
|
|
119
|
+
.map((c: any) => c.text),
|
|
120
|
+
)
|
|
121
|
+
.join("");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function complete(
|
|
125
|
+
request: CompletionRequest,
|
|
126
|
+
): Promise<CompletionResult> {
|
|
127
|
+
const { instructions, input, previous_response_id } = buildInput(
|
|
128
|
+
request.messages,
|
|
129
|
+
request.previousResponseId,
|
|
130
|
+
);
|
|
131
|
+
|
|
73
132
|
const body: Record<string, unknown> = {
|
|
74
133
|
model: request.model,
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
134
|
+
input,
|
|
135
|
+
...(instructions ? { instructions } : {}),
|
|
136
|
+
...(previous_response_id ? { previous_response_id } : {}),
|
|
137
|
+
...(request.temperature != null
|
|
138
|
+
? { temperature: request.temperature }
|
|
139
|
+
: {}),
|
|
140
|
+
...(request.maxTokens != null
|
|
141
|
+
? { max_output_tokens: request.maxTokens }
|
|
142
|
+
: {}),
|
|
83
143
|
};
|
|
84
144
|
|
|
85
145
|
const tools = translateTools(request.tools);
|
|
86
146
|
if (tools) body.tools = tools;
|
|
87
|
-
|
|
88
147
|
if (request.webSearch) {
|
|
89
|
-
body.tools = [
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
148
|
+
body.tools = [...(tools ?? []), { type: "web_search_preview" }];
|
|
149
|
+
}
|
|
150
|
+
if (request.toolChoice) {
|
|
151
|
+
body.tool_choice = request.toolChoice;
|
|
93
152
|
}
|
|
94
153
|
|
|
95
|
-
const response = await fetch(`${baseUrl}/
|
|
154
|
+
const response = await fetch(`${baseUrl}/responses`, {
|
|
96
155
|
method: "POST",
|
|
97
156
|
headers: {
|
|
98
157
|
"Content-Type": "application/json",
|
|
@@ -106,14 +165,16 @@ export function openai(config: OpenAIConfig): LLMProvider {
|
|
|
106
165
|
throw new Error(`OpenAI API error ${response.status}: ${error}`);
|
|
107
166
|
}
|
|
108
167
|
|
|
109
|
-
const data = await response.json()
|
|
110
|
-
const
|
|
168
|
+
const data = (await response.json());
|
|
169
|
+
const toolCalls = extractToolCallsFromOutput(data.output ?? []);
|
|
170
|
+
const hasToolCalls = toolCalls.length > 0;
|
|
111
171
|
|
|
112
172
|
return {
|
|
113
|
-
content:
|
|
114
|
-
toolCalls
|
|
115
|
-
usage: parseUsage(data),
|
|
116
|
-
finishReason:
|
|
173
|
+
content: extractContentFromOutput(data.output ?? []),
|
|
174
|
+
toolCalls,
|
|
175
|
+
usage: parseUsage(data.usage),
|
|
176
|
+
finishReason: hasToolCalls ? "tool_call" : "stop",
|
|
177
|
+
responseId: data.id,
|
|
117
178
|
};
|
|
118
179
|
}
|
|
119
180
|
|
|
@@ -121,31 +182,35 @@ export function openai(config: OpenAIConfig): LLMProvider {
|
|
|
121
182
|
request: CompletionRequest,
|
|
122
183
|
onChunk: (chunk: StreamChunk) => void,
|
|
123
184
|
): Promise<CompletionResult> {
|
|
185
|
+
const { instructions, input, previous_response_id } = buildInput(
|
|
186
|
+
request.messages,
|
|
187
|
+
request.previousResponseId,
|
|
188
|
+
);
|
|
189
|
+
|
|
124
190
|
const body: Record<string, unknown> = {
|
|
125
191
|
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,
|
|
192
|
+
input,
|
|
134
193
|
stream: true,
|
|
135
|
-
|
|
194
|
+
...(instructions ? { instructions } : {}),
|
|
195
|
+
...(previous_response_id ? { previous_response_id } : {}),
|
|
196
|
+
...(request.temperature != null
|
|
197
|
+
? { temperature: request.temperature }
|
|
198
|
+
: {}),
|
|
199
|
+
...(request.maxTokens != null
|
|
200
|
+
? { max_output_tokens: request.maxTokens }
|
|
201
|
+
: {}),
|
|
136
202
|
};
|
|
137
203
|
|
|
138
204
|
const tools = translateTools(request.tools);
|
|
139
205
|
if (tools) body.tools = tools;
|
|
140
|
-
|
|
141
206
|
if (request.webSearch) {
|
|
142
|
-
body.tools = [
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
207
|
+
body.tools = [...(tools ?? []), { type: "web_search_preview" }];
|
|
208
|
+
}
|
|
209
|
+
if (request.toolChoice) {
|
|
210
|
+
body.tool_choice = request.toolChoice;
|
|
146
211
|
}
|
|
147
212
|
|
|
148
|
-
const response = await fetch(`${baseUrl}/
|
|
213
|
+
const response = await fetch(`${baseUrl}/responses`, {
|
|
149
214
|
method: "POST",
|
|
150
215
|
headers: {
|
|
151
216
|
"Content-Type": "application/json",
|
|
@@ -160,6 +225,7 @@ export function openai(config: OpenAIConfig): LLMProvider {
|
|
|
160
225
|
}
|
|
161
226
|
|
|
162
227
|
let content = "";
|
|
228
|
+
let responseId = "";
|
|
163
229
|
let finishReason: FinishReason = "stop";
|
|
164
230
|
let usage: TokenUsage = {
|
|
165
231
|
inputTokens: 0,
|
|
@@ -168,11 +234,8 @@ export function openai(config: OpenAIConfig): LLMProvider {
|
|
|
168
234
|
cachedTokens: 0,
|
|
169
235
|
reasoningTokens: 0,
|
|
170
236
|
};
|
|
171
|
-
const toolCallBuffers = new Map<
|
|
172
|
-
number,
|
|
173
|
-
{ id: string; name: string; arguments: string }
|
|
174
|
-
>();
|
|
175
237
|
const completedToolCalls: ToolCall[] = [];
|
|
238
|
+
const toolCallArgBuffers = new Map<string, string>();
|
|
176
239
|
|
|
177
240
|
const reader = response.body!.getReader();
|
|
178
241
|
const decoder = new TextDecoder();
|
|
@@ -189,79 +252,68 @@ export function openai(config: OpenAIConfig): LLMProvider {
|
|
|
189
252
|
for (const line of lines) {
|
|
190
253
|
if (!line.startsWith("data: ")) continue;
|
|
191
254
|
const data = line.slice(6).trim();
|
|
192
|
-
if (data
|
|
255
|
+
if (!data) continue;
|
|
193
256
|
|
|
194
257
|
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
|
-
}
|
|
258
|
+
const event = JSON.parse(data);
|
|
203
259
|
|
|
204
|
-
|
|
205
|
-
|
|
260
|
+
switch (event.type) {
|
|
261
|
+
case "response.output_text.delta":
|
|
262
|
+
if (event.delta) {
|
|
263
|
+
content += event.delta;
|
|
264
|
+
onChunk({ type: "content_delta", content: event.delta });
|
|
265
|
+
}
|
|
266
|
+
break;
|
|
206
267
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
268
|
+
case "response.function_call_arguments.delta":
|
|
269
|
+
if (event.call_id && event.delta) {
|
|
270
|
+
const existing = toolCallArgBuffers.get(event.call_id) ?? "";
|
|
271
|
+
toolCallArgBuffers.set(event.call_id, existing + event.delta);
|
|
272
|
+
}
|
|
273
|
+
break;
|
|
212
274
|
|
|
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
|
-
});
|
|
275
|
+
case "response.output_item.added":
|
|
276
|
+
if (event.item?.type === "function_call") {
|
|
277
|
+
toolCallArgBuffers.set(event.item.call_id, "");
|
|
224
278
|
onChunk({
|
|
225
279
|
type: "tool_call_start",
|
|
226
|
-
toolCall: {
|
|
280
|
+
toolCall: {
|
|
281
|
+
id: event.item.call_id,
|
|
282
|
+
name: event.item.name,
|
|
283
|
+
arguments: {},
|
|
284
|
+
},
|
|
227
285
|
});
|
|
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
286
|
}
|
|
239
|
-
|
|
240
|
-
|
|
287
|
+
break;
|
|
288
|
+
|
|
289
|
+
case "response.output_item.done":
|
|
290
|
+
if (event.item?.type === "function_call") {
|
|
291
|
+
const buffered = toolCallArgBuffers.get(event.item.call_id);
|
|
292
|
+
const argsStr = (buffered && buffered.length > 0)
|
|
293
|
+
? buffered
|
|
294
|
+
: (event.item.arguments ?? "{}");
|
|
295
|
+
let args: Record<string, unknown> = {};
|
|
296
|
+
try {
|
|
297
|
+
args = JSON.parse(argsStr);
|
|
298
|
+
} catch {}
|
|
299
|
+
completedToolCalls.push({
|
|
300
|
+
id: event.item.call_id,
|
|
301
|
+
name: event.item.name,
|
|
302
|
+
arguments: args,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
break;
|
|
241
306
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
307
|
+
case "response.completed":
|
|
308
|
+
responseId = event.response?.id ?? "";
|
|
309
|
+
usage = parseUsage(event.response?.usage);
|
|
310
|
+
onChunk({ type: "usage_update", usage });
|
|
246
311
|
|
|
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
|
-
}
|
|
312
|
+
// Extract content from completed response if not streamed
|
|
313
|
+
if (!content && event.response?.output) {
|
|
314
|
+
content = extractContentFromOutput(event.response.output);
|
|
263
315
|
}
|
|
264
|
-
|
|
316
|
+
break;
|
|
265
317
|
}
|
|
266
318
|
} catch {
|
|
267
319
|
// Skip malformed JSON
|
|
@@ -269,18 +321,33 @@ export function openai(config: OpenAIConfig): LLMProvider {
|
|
|
269
321
|
}
|
|
270
322
|
}
|
|
271
323
|
|
|
324
|
+
finishReason = completedToolCalls.length > 0 ? "tool_call" : "stop";
|
|
325
|
+
|
|
272
326
|
return {
|
|
273
327
|
content,
|
|
274
328
|
toolCalls: completedToolCalls,
|
|
275
329
|
usage,
|
|
276
330
|
finishReason,
|
|
331
|
+
responseId,
|
|
277
332
|
};
|
|
278
333
|
}
|
|
279
334
|
|
|
335
|
+
const pricing: Record<string, { inputPer1M: number; outputPer1M: number; cachedInputPer1M?: number; reasoningPer1M?: number }> = {
|
|
336
|
+
"gpt-4o": { inputPer1M: 2.50, outputPer1M: 10.00, cachedInputPer1M: 1.25 },
|
|
337
|
+
"gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.60, cachedInputPer1M: 0.075 },
|
|
338
|
+
"o3": { inputPer1M: 10.00, outputPer1M: 40.00, reasoningPer1M: 40.00 },
|
|
339
|
+
"o3-mini": { inputPer1M: 1.10, outputPer1M: 4.40, reasoningPer1M: 4.40 },
|
|
340
|
+
"gpt-4.1": { inputPer1M: 2.00, outputPer1M: 8.00, cachedInputPer1M: 0.50 },
|
|
341
|
+
"gpt-4.1-mini": { inputPer1M: 0.40, outputPer1M: 1.60, cachedInputPer1M: 0.10 },
|
|
342
|
+
"gpt-4.1-nano": { inputPer1M: 0.10, outputPer1M: 0.40, cachedInputPer1M: 0.025 },
|
|
343
|
+
"gpt-5.4-nano": { inputPer1M: 0.10, outputPer1M: 0.40, cachedInputPer1M: 0.025 },
|
|
344
|
+
};
|
|
345
|
+
|
|
280
346
|
return {
|
|
281
347
|
name: "openai",
|
|
282
|
-
models:
|
|
348
|
+
models: Object.keys(pricing),
|
|
283
349
|
complete,
|
|
284
350
|
streamComplete,
|
|
351
|
+
getPricing: (model: string) => pricing[model],
|
|
285
352
|
};
|
|
286
353
|
}
|