@arcote.tech/arc-ai-openai 0.5.0 → 0.5.5
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 +219 -160
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.5.
|
|
4
|
+
"version": "0.5.5",
|
|
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.5.
|
|
13
|
+
"@arcote.tech/arc-ai": "^0.5.5",
|
|
14
14
|
"typescript": "^5.0.0"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
package/src/index.ts
CHANGED
|
@@ -2,11 +2,13 @@ import type {
|
|
|
2
2
|
LLMProvider,
|
|
3
3
|
CompletionRequest,
|
|
4
4
|
CompletionResult,
|
|
5
|
+
Conversation,
|
|
6
|
+
ConversationTurn,
|
|
7
|
+
AssistantContentBlock,
|
|
5
8
|
StreamChunk,
|
|
6
9
|
ToolCall,
|
|
7
10
|
TokenUsage,
|
|
8
11
|
FinishReason,
|
|
9
|
-
Message,
|
|
10
12
|
} from "@arcote.tech/arc-ai";
|
|
11
13
|
|
|
12
14
|
// ─── Config ──────────────────────────────────────────────────────
|
|
@@ -22,6 +24,8 @@ export interface OpenAIConfig {
|
|
|
22
24
|
export function openai(config: OpenAIConfig): LLMProvider {
|
|
23
25
|
const baseUrl = config.baseUrl ?? "https://api.openai.com/v1";
|
|
24
26
|
|
|
27
|
+
// ─── Helpers ──────────────────────────────────────────────────
|
|
28
|
+
|
|
25
29
|
function translateTools(
|
|
26
30
|
tools: CompletionRequest["tools"],
|
|
27
31
|
): unknown[] | undefined {
|
|
@@ -31,6 +35,7 @@ export function openai(config: OpenAIConfig): LLMProvider {
|
|
|
31
35
|
name: t.name,
|
|
32
36
|
description: t.description,
|
|
33
37
|
parameters: t.parameters,
|
|
38
|
+
strict: false,
|
|
34
39
|
}));
|
|
35
40
|
}
|
|
36
41
|
|
|
@@ -44,108 +49,136 @@ export function openai(config: OpenAIConfig): LLMProvider {
|
|
|
44
49
|
};
|
|
45
50
|
}
|
|
46
51
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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 {
|
|
52
|
+
/**
|
|
53
|
+
* Translate a single ConversationTurn into one or more OpenAI Responses API
|
|
54
|
+
* input items, preserving block ordering for assistant turns. Adapter is a
|
|
55
|
+
* pure translator — caller already decided what to send via the
|
|
56
|
+
* Conversation discriminated union.
|
|
57
|
+
*/
|
|
58
|
+
function turnToInputItems(turn: ConversationTurn): unknown[] {
|
|
59
|
+
if (turn.role === "user") {
|
|
60
|
+
return [
|
|
61
|
+
{
|
|
70
62
|
type: "message",
|
|
71
|
-
role:
|
|
72
|
-
content:
|
|
73
|
-
}
|
|
74
|
-
|
|
63
|
+
role: "user",
|
|
64
|
+
content: turn.content,
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
}
|
|
75
68
|
|
|
69
|
+
if (turn.role === "tool_result") {
|
|
70
|
+
// OpenAI Responses API requires `output` to be a string. If upstream
|
|
71
|
+
// deserialized our JSON-shaped content into an object, re-stringify.
|
|
72
|
+
const output =
|
|
73
|
+
typeof turn.content === "string"
|
|
74
|
+
? turn.content
|
|
75
|
+
: JSON.stringify(turn.content);
|
|
76
|
+
return [
|
|
77
|
+
{
|
|
78
|
+
type: "function_call_output",
|
|
79
|
+
call_id: turn.toolCallId,
|
|
80
|
+
output,
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// assistant — emit blocks in order, mapping each block to its OpenAI shape
|
|
86
|
+
const items: unknown[] = [];
|
|
87
|
+
for (const block of turn.blocks) {
|
|
88
|
+
if (block.type === "text") {
|
|
89
|
+
if (!block.text) continue;
|
|
90
|
+
items.push({
|
|
91
|
+
type: "message",
|
|
92
|
+
role: "assistant",
|
|
93
|
+
content: block.text,
|
|
94
|
+
});
|
|
95
|
+
} else {
|
|
96
|
+
items.push({
|
|
97
|
+
type: "function_call",
|
|
98
|
+
call_id: block.id,
|
|
99
|
+
name: block.name,
|
|
100
|
+
arguments: JSON.stringify(block.arguments),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return items;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function buildInput(conversation: Conversation): {
|
|
108
|
+
input: unknown[];
|
|
109
|
+
previous_response_id?: string;
|
|
110
|
+
} {
|
|
111
|
+
if (conversation.mode === "full") {
|
|
76
112
|
return {
|
|
77
|
-
|
|
78
|
-
input,
|
|
79
|
-
previous_response_id: previousResponseId,
|
|
113
|
+
input: conversation.turns.flatMap(turnToInputItems),
|
|
80
114
|
};
|
|
81
115
|
}
|
|
116
|
+
return {
|
|
117
|
+
input: conversation.newTurns.flatMap(turnToInputItems),
|
|
118
|
+
previous_response_id: conversation.previousResponseId,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
82
121
|
|
|
83
|
-
|
|
84
|
-
const input =
|
|
85
|
-
type: "message",
|
|
86
|
-
role: m.role === "tool" ? "user" : m.role,
|
|
87
|
-
content: m.content,
|
|
88
|
-
}));
|
|
122
|
+
function buildBody(request: CompletionRequest, stream: boolean): Record<string, unknown> {
|
|
123
|
+
const { input, previous_response_id } = buildInput(request.conversation);
|
|
89
124
|
|
|
90
|
-
|
|
91
|
-
|
|
125
|
+
const body: Record<string, unknown> = {
|
|
126
|
+
model: request.model,
|
|
92
127
|
input,
|
|
128
|
+
// `instructions` is sent on every call. With previous_response_id it
|
|
129
|
+
// replaces the prior server-side instructions for this turn.
|
|
130
|
+
instructions: request.instructions,
|
|
131
|
+
...(stream ? { stream: true } : {}),
|
|
132
|
+
...(previous_response_id ? { previous_response_id } : {}),
|
|
133
|
+
...(request.temperature != null ? { temperature: request.temperature } : {}),
|
|
134
|
+
...(request.maxTokens != null ? { max_output_tokens: request.maxTokens } : {}),
|
|
93
135
|
};
|
|
136
|
+
|
|
137
|
+
const tools = translateTools(request.tools);
|
|
138
|
+
if (tools) body.tools = tools;
|
|
139
|
+
if (request.webSearch) {
|
|
140
|
+
body.tools = [...(tools ?? []), { type: "web_search_preview" }];
|
|
141
|
+
}
|
|
142
|
+
if (request.toolChoice) {
|
|
143
|
+
body.tool_choice = request.toolChoice;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return body;
|
|
94
147
|
}
|
|
95
148
|
|
|
96
|
-
function
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
149
|
+
function blocksFromOutput(output: any[]): AssistantContentBlock[] {
|
|
150
|
+
const blocks: AssistantContentBlock[] = [];
|
|
151
|
+
for (const item of output ?? []) {
|
|
152
|
+
if (item.type === "message") {
|
|
153
|
+
const text = (item.content ?? [])
|
|
154
|
+
.filter((c: any) => c.type === "output_text")
|
|
155
|
+
.map((c: any) => c.text)
|
|
156
|
+
.join("");
|
|
157
|
+
if (text) blocks.push({ type: "text", text });
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (item.type === "function_call") {
|
|
100
161
|
let args: Record<string, unknown> = {};
|
|
101
162
|
try {
|
|
102
163
|
args = JSON.parse(item.arguments);
|
|
103
164
|
} catch {}
|
|
104
|
-
|
|
165
|
+
blocks.push({
|
|
166
|
+
type: "tool_call",
|
|
105
167
|
id: item.call_id,
|
|
106
168
|
name: item.name,
|
|
107
169
|
arguments: args,
|
|
108
|
-
};
|
|
109
|
-
}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return blocks;
|
|
110
174
|
}
|
|
111
175
|
|
|
112
|
-
|
|
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("");
|
|
121
|
-
}
|
|
176
|
+
// ─── complete (non-streaming) ─────────────────────────────────
|
|
122
177
|
|
|
123
178
|
async function complete(
|
|
124
179
|
request: CompletionRequest,
|
|
125
180
|
): Promise<CompletionResult> {
|
|
126
|
-
const
|
|
127
|
-
request.messages,
|
|
128
|
-
request.previousResponseId,
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
const body: Record<string, unknown> = {
|
|
132
|
-
model: request.model,
|
|
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
|
-
: {}),
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
const tools = translateTools(request.tools);
|
|
145
|
-
if (tools) body.tools = tools;
|
|
146
|
-
if (request.webSearch) {
|
|
147
|
-
body.tools = [...(tools ?? []), { type: "web_search_preview" }];
|
|
148
|
-
}
|
|
181
|
+
const body = buildBody(request, false);
|
|
149
182
|
|
|
150
183
|
const response = await fetch(`${baseUrl}/responses`, {
|
|
151
184
|
method: "POST",
|
|
@@ -161,47 +194,25 @@ export function openai(config: OpenAIConfig): LLMProvider {
|
|
|
161
194
|
throw new Error(`OpenAI API error ${response.status}: ${error}`);
|
|
162
195
|
}
|
|
163
196
|
|
|
164
|
-
const data =
|
|
165
|
-
const
|
|
166
|
-
const hasToolCalls =
|
|
197
|
+
const data = await response.json();
|
|
198
|
+
const blocks = blocksFromOutput(data.output ?? []);
|
|
199
|
+
const hasToolCalls = blocks.some((b) => b.type === "tool_call");
|
|
167
200
|
|
|
168
201
|
return {
|
|
169
|
-
|
|
170
|
-
toolCalls,
|
|
202
|
+
blocks,
|
|
171
203
|
usage: parseUsage(data.usage),
|
|
172
204
|
finishReason: hasToolCalls ? "tool_call" : "stop",
|
|
173
205
|
responseId: data.id,
|
|
174
206
|
};
|
|
175
207
|
}
|
|
176
208
|
|
|
209
|
+
// ─── streamComplete ───────────────────────────────────────────
|
|
210
|
+
|
|
177
211
|
async function streamComplete(
|
|
178
212
|
request: CompletionRequest,
|
|
179
213
|
onChunk: (chunk: StreamChunk) => void,
|
|
180
214
|
): Promise<CompletionResult> {
|
|
181
|
-
const
|
|
182
|
-
request.messages,
|
|
183
|
-
request.previousResponseId,
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
const body: Record<string, unknown> = {
|
|
187
|
-
model: request.model,
|
|
188
|
-
input,
|
|
189
|
-
stream: true,
|
|
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
|
-
: {}),
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
const tools = translateTools(request.tools);
|
|
201
|
-
if (tools) body.tools = tools;
|
|
202
|
-
if (request.webSearch) {
|
|
203
|
-
body.tools = [...(tools ?? []), { type: "web_search_preview" }];
|
|
204
|
-
}
|
|
215
|
+
const body = buildBody(request, true);
|
|
205
216
|
|
|
206
217
|
const response = await fetch(`${baseUrl}/responses`, {
|
|
207
218
|
method: "POST",
|
|
@@ -217,9 +228,15 @@ export function openai(config: OpenAIConfig): LLMProvider {
|
|
|
217
228
|
throw new Error(`OpenAI API error ${response.status}: ${error}`);
|
|
218
229
|
}
|
|
219
230
|
|
|
220
|
-
|
|
231
|
+
// Reconstruct ordered blocks from streamed events. The Responses API
|
|
232
|
+
// emits output_item.added/done events in order, so we keep an array
|
|
233
|
+
// anchored by output_index for deterministic placement.
|
|
234
|
+
const orderedBlocks: AssistantContentBlock[] = [];
|
|
235
|
+
const indexToBlock = new Map<number, AssistantContentBlock>();
|
|
236
|
+
const toolCallArgBuffers = new Map<string, string>();
|
|
237
|
+
const toolCallIndex = new Map<string, number>();
|
|
238
|
+
|
|
221
239
|
let responseId = "";
|
|
222
|
-
let finishReason: FinishReason = "stop";
|
|
223
240
|
let usage: TokenUsage = {
|
|
224
241
|
inputTokens: 0,
|
|
225
242
|
outputTokens: 0,
|
|
@@ -227,8 +244,6 @@ export function openai(config: OpenAIConfig): LLMProvider {
|
|
|
227
244
|
cachedTokens: 0,
|
|
228
245
|
reasoningTokens: 0,
|
|
229
246
|
};
|
|
230
|
-
const completedToolCalls: ToolCall[] = [];
|
|
231
|
-
const toolCallArgBuffers = new Map<string, string>();
|
|
232
247
|
|
|
233
248
|
const reader = response.body!.getReader();
|
|
234
249
|
const decoder = new TextDecoder();
|
|
@@ -251,62 +266,90 @@ export function openai(config: OpenAIConfig): LLMProvider {
|
|
|
251
266
|
const event = JSON.parse(data);
|
|
252
267
|
|
|
253
268
|
switch (event.type) {
|
|
254
|
-
case "response.
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
toolCallArgBuffers.set(
|
|
269
|
+
case "response.output_item.added": {
|
|
270
|
+
const item = event.item;
|
|
271
|
+
const idx = event.output_index ?? orderedBlocks.length;
|
|
272
|
+
if (item?.type === "message") {
|
|
273
|
+
const block: AssistantContentBlock = { type: "text", text: "" };
|
|
274
|
+
indexToBlock.set(idx, block);
|
|
275
|
+
orderedBlocks[idx] = block;
|
|
276
|
+
} else if (item?.type === "function_call") {
|
|
277
|
+
const block: AssistantContentBlock = {
|
|
278
|
+
type: "tool_call",
|
|
279
|
+
id: item.call_id,
|
|
280
|
+
name: item.name,
|
|
281
|
+
arguments: {},
|
|
282
|
+
};
|
|
283
|
+
indexToBlock.set(idx, block);
|
|
284
|
+
orderedBlocks[idx] = block;
|
|
285
|
+
toolCallArgBuffers.set(item.call_id, "");
|
|
286
|
+
toolCallIndex.set(item.call_id, idx);
|
|
271
287
|
onChunk({
|
|
272
288
|
type: "tool_call_start",
|
|
273
289
|
toolCall: {
|
|
274
|
-
id:
|
|
275
|
-
name:
|
|
290
|
+
id: item.call_id,
|
|
291
|
+
name: item.name,
|
|
276
292
|
arguments: {},
|
|
277
293
|
},
|
|
278
294
|
});
|
|
279
295
|
}
|
|
280
296
|
break;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
case "response.output_text.delta": {
|
|
300
|
+
if (!event.delta) break;
|
|
301
|
+
const idx = event.output_index;
|
|
302
|
+
const block = indexToBlock.get(idx);
|
|
303
|
+
if (block?.type === "text") {
|
|
304
|
+
block.text += event.delta;
|
|
305
|
+
}
|
|
306
|
+
onChunk({ type: "content_delta", content: event.delta });
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
281
309
|
|
|
282
|
-
case "response.
|
|
283
|
-
if (event.
|
|
310
|
+
case "response.function_call_arguments.delta": {
|
|
311
|
+
if (event.call_id && event.delta) {
|
|
312
|
+
const existing = toolCallArgBuffers.get(event.call_id) ?? "";
|
|
313
|
+
toolCallArgBuffers.set(event.call_id, existing + event.delta);
|
|
314
|
+
}
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
case "response.output_item.done": {
|
|
319
|
+
const item = event.item;
|
|
320
|
+
if (item?.type === "function_call") {
|
|
321
|
+
const buffered = toolCallArgBuffers.get(item.call_id);
|
|
284
322
|
const argsStr =
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
323
|
+
buffered && buffered.length > 0
|
|
324
|
+
? buffered
|
|
325
|
+
: item.arguments ?? "{}";
|
|
288
326
|
let args: Record<string, unknown> = {};
|
|
289
327
|
try {
|
|
290
328
|
args = JSON.parse(argsStr);
|
|
291
329
|
} catch {}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
330
|
+
const idx = toolCallIndex.get(item.call_id);
|
|
331
|
+
if (idx != null) {
|
|
332
|
+
const block = indexToBlock.get(idx);
|
|
333
|
+
if (block?.type === "tool_call") {
|
|
334
|
+
block.arguments = args;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
297
337
|
}
|
|
298
338
|
break;
|
|
339
|
+
}
|
|
299
340
|
|
|
300
|
-
case "response.completed":
|
|
341
|
+
case "response.completed": {
|
|
301
342
|
responseId = event.response?.id ?? "";
|
|
302
343
|
usage = parseUsage(event.response?.usage);
|
|
303
344
|
onChunk({ type: "usage_update", usage });
|
|
304
|
-
|
|
305
|
-
//
|
|
306
|
-
if (
|
|
307
|
-
|
|
345
|
+
// Final reconciliation: if our streaming reconstruction missed
|
|
346
|
+
// anything, fall back to the completed output.
|
|
347
|
+
if (orderedBlocks.length === 0 && event.response?.output) {
|
|
348
|
+
const fallback = blocksFromOutput(event.response.output);
|
|
349
|
+
orderedBlocks.push(...fallback);
|
|
308
350
|
}
|
|
309
351
|
break;
|
|
352
|
+
}
|
|
310
353
|
}
|
|
311
354
|
} catch {
|
|
312
355
|
// Skip malformed JSON
|
|
@@ -314,30 +357,46 @@ export function openai(config: OpenAIConfig): LLMProvider {
|
|
|
314
357
|
}
|
|
315
358
|
}
|
|
316
359
|
|
|
317
|
-
|
|
360
|
+
// Compact: drop any holes in orderedBlocks (defensive)
|
|
361
|
+
const blocks = orderedBlocks.filter(
|
|
362
|
+
(b): b is AssistantContentBlock => b != null,
|
|
363
|
+
);
|
|
364
|
+
const hasToolCalls = blocks.some((b) => b.type === "tool_call");
|
|
318
365
|
|
|
319
366
|
return {
|
|
320
|
-
|
|
321
|
-
toolCalls: completedToolCalls,
|
|
367
|
+
blocks,
|
|
322
368
|
usage,
|
|
323
|
-
finishReason,
|
|
369
|
+
finishReason: hasToolCalls ? "tool_call" : "stop",
|
|
324
370
|
responseId,
|
|
325
371
|
};
|
|
326
372
|
}
|
|
327
373
|
|
|
374
|
+
const pricing: Record<
|
|
375
|
+
string,
|
|
376
|
+
{
|
|
377
|
+
inputPer1M: number;
|
|
378
|
+
outputPer1M: number;
|
|
379
|
+
cachedInputPer1M?: number;
|
|
380
|
+
reasoningPer1M?: number;
|
|
381
|
+
}
|
|
382
|
+
> = {
|
|
383
|
+
"gpt-4o": { inputPer1M: 2.5, outputPer1M: 10.0, cachedInputPer1M: 1.25 },
|
|
384
|
+
"gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6, cachedInputPer1M: 0.075 },
|
|
385
|
+
o3: { inputPer1M: 10.0, outputPer1M: 40.0, reasoningPer1M: 40.0 },
|
|
386
|
+
"o3-mini": { inputPer1M: 1.1, outputPer1M: 4.4, reasoningPer1M: 4.4 },
|
|
387
|
+
"gpt-4.1": { inputPer1M: 2.0, outputPer1M: 8.0, cachedInputPer1M: 0.5 },
|
|
388
|
+
"gpt-4.1-mini": { inputPer1M: 0.4, outputPer1M: 1.6, cachedInputPer1M: 0.1 },
|
|
389
|
+
"gpt-4.1-nano": { inputPer1M: 0.1, outputPer1M: 0.4, cachedInputPer1M: 0.025 },
|
|
390
|
+
"gpt-5.4-nano": { inputPer1M: 0.1, outputPer1M: 0.4, cachedInputPer1M: 0.025 },
|
|
391
|
+
"gpt-5.4-mini": { inputPer1M: 0.4, outputPer1M: 1.6, cachedInputPer1M: 0.1 },
|
|
392
|
+
};
|
|
393
|
+
|
|
328
394
|
return {
|
|
329
395
|
name: "openai",
|
|
330
|
-
models:
|
|
331
|
-
|
|
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
|
-
],
|
|
396
|
+
models: Object.keys(pricing),
|
|
397
|
+
supportsContinuation: true,
|
|
340
398
|
complete,
|
|
341
399
|
streamComplete,
|
|
400
|
+
getPricing: (model: string) => pricing[model],
|
|
342
401
|
};
|
|
343
402
|
}
|