@arcote.tech/arc-ai-openai 0.5.2 → 0.5.6
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 +215 -166
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.6",
|
|
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.6",
|
|
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 {
|
|
@@ -45,101 +49,89 @@ export function openai(config: OpenAIConfig): LLMProvider {
|
|
|
45
49
|
};
|
|
46
50
|
}
|
|
47
51
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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 {
|
|
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
|
+
{
|
|
71
62
|
type: "message",
|
|
72
|
-
role:
|
|
73
|
-
content:
|
|
74
|
-
}
|
|
75
|
-
|
|
63
|
+
role: "user",
|
|
64
|
+
content: turn.content,
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
}
|
|
76
68
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
+
];
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
//
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
+
}
|
|
90
106
|
|
|
107
|
+
function buildInput(conversation: Conversation): {
|
|
108
|
+
input: unknown[];
|
|
109
|
+
previous_response_id?: string;
|
|
110
|
+
} {
|
|
111
|
+
if (conversation.mode === "full") {
|
|
112
|
+
return {
|
|
113
|
+
input: conversation.turns.flatMap(turnToInputItems),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
91
116
|
return {
|
|
92
|
-
|
|
93
|
-
|
|
117
|
+
input: conversation.newTurns.flatMap(turnToInputItems),
|
|
118
|
+
previous_response_id: conversation.previousResponseId,
|
|
94
119
|
};
|
|
95
120
|
}
|
|
96
121
|
|
|
97
|
-
function
|
|
98
|
-
|
|
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
|
-
});
|
|
111
|
-
}
|
|
112
|
-
|
|
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
|
-
);
|
|
122
|
+
function buildBody(request: CompletionRequest, stream: boolean): Record<string, unknown> {
|
|
123
|
+
const { input, previous_response_id } = buildInput(request.conversation);
|
|
131
124
|
|
|
132
125
|
const body: Record<string, unknown> = {
|
|
133
126
|
model: request.model,
|
|
134
127
|
input,
|
|
135
|
-
|
|
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 } : {}),
|
|
136
132
|
...(previous_response_id ? { previous_response_id } : {}),
|
|
137
|
-
...(request.temperature != null
|
|
138
|
-
|
|
139
|
-
: {}),
|
|
140
|
-
...(request.maxTokens != null
|
|
141
|
-
? { max_output_tokens: request.maxTokens }
|
|
142
|
-
: {}),
|
|
133
|
+
...(request.temperature != null ? { temperature: request.temperature } : {}),
|
|
134
|
+
...(request.maxTokens != null ? { max_output_tokens: request.maxTokens } : {}),
|
|
143
135
|
};
|
|
144
136
|
|
|
145
137
|
const tools = translateTools(request.tools);
|
|
@@ -151,6 +143,43 @@ export function openai(config: OpenAIConfig): LLMProvider {
|
|
|
151
143
|
body.tool_choice = request.toolChoice;
|
|
152
144
|
}
|
|
153
145
|
|
|
146
|
+
return body;
|
|
147
|
+
}
|
|
148
|
+
|
|
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") {
|
|
161
|
+
let args: Record<string, unknown> = {};
|
|
162
|
+
try {
|
|
163
|
+
args = JSON.parse(item.arguments);
|
|
164
|
+
} catch {}
|
|
165
|
+
blocks.push({
|
|
166
|
+
type: "tool_call",
|
|
167
|
+
id: item.call_id,
|
|
168
|
+
name: item.name,
|
|
169
|
+
arguments: args,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return blocks;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ─── complete (non-streaming) ─────────────────────────────────
|
|
177
|
+
|
|
178
|
+
async function complete(
|
|
179
|
+
request: CompletionRequest,
|
|
180
|
+
): Promise<CompletionResult> {
|
|
181
|
+
const body = buildBody(request, false);
|
|
182
|
+
|
|
154
183
|
const response = await fetch(`${baseUrl}/responses`, {
|
|
155
184
|
method: "POST",
|
|
156
185
|
headers: {
|
|
@@ -165,50 +194,25 @@ export function openai(config: OpenAIConfig): LLMProvider {
|
|
|
165
194
|
throw new Error(`OpenAI API error ${response.status}: ${error}`);
|
|
166
195
|
}
|
|
167
196
|
|
|
168
|
-
const data =
|
|
169
|
-
const
|
|
170
|
-
const hasToolCalls =
|
|
197
|
+
const data = await response.json();
|
|
198
|
+
const blocks = blocksFromOutput(data.output ?? []);
|
|
199
|
+
const hasToolCalls = blocks.some((b) => b.type === "tool_call");
|
|
171
200
|
|
|
172
201
|
return {
|
|
173
|
-
|
|
174
|
-
toolCalls,
|
|
202
|
+
blocks,
|
|
175
203
|
usage: parseUsage(data.usage),
|
|
176
204
|
finishReason: hasToolCalls ? "tool_call" : "stop",
|
|
177
205
|
responseId: data.id,
|
|
178
206
|
};
|
|
179
207
|
}
|
|
180
208
|
|
|
209
|
+
// ─── streamComplete ───────────────────────────────────────────
|
|
210
|
+
|
|
181
211
|
async function streamComplete(
|
|
182
212
|
request: CompletionRequest,
|
|
183
213
|
onChunk: (chunk: StreamChunk) => void,
|
|
184
214
|
): Promise<CompletionResult> {
|
|
185
|
-
const
|
|
186
|
-
request.messages,
|
|
187
|
-
request.previousResponseId,
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
const body: Record<string, unknown> = {
|
|
191
|
-
model: request.model,
|
|
192
|
-
input,
|
|
193
|
-
stream: true,
|
|
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
|
-
: {}),
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
const tools = translateTools(request.tools);
|
|
205
|
-
if (tools) body.tools = tools;
|
|
206
|
-
if (request.webSearch) {
|
|
207
|
-
body.tools = [...(tools ?? []), { type: "web_search_preview" }];
|
|
208
|
-
}
|
|
209
|
-
if (request.toolChoice) {
|
|
210
|
-
body.tool_choice = request.toolChoice;
|
|
211
|
-
}
|
|
215
|
+
const body = buildBody(request, true);
|
|
212
216
|
|
|
213
217
|
const response = await fetch(`${baseUrl}/responses`, {
|
|
214
218
|
method: "POST",
|
|
@@ -224,9 +228,15 @@ export function openai(config: OpenAIConfig): LLMProvider {
|
|
|
224
228
|
throw new Error(`OpenAI API error ${response.status}: ${error}`);
|
|
225
229
|
}
|
|
226
230
|
|
|
227
|
-
|
|
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
|
+
|
|
228
239
|
let responseId = "";
|
|
229
|
-
let finishReason: FinishReason = "stop";
|
|
230
240
|
let usage: TokenUsage = {
|
|
231
241
|
inputTokens: 0,
|
|
232
242
|
outputTokens: 0,
|
|
@@ -234,8 +244,6 @@ export function openai(config: OpenAIConfig): LLMProvider {
|
|
|
234
244
|
cachedTokens: 0,
|
|
235
245
|
reasoningTokens: 0,
|
|
236
246
|
};
|
|
237
|
-
const completedToolCalls: ToolCall[] = [];
|
|
238
|
-
const toolCallArgBuffers = new Map<string, string>();
|
|
239
247
|
|
|
240
248
|
const reader = response.body!.getReader();
|
|
241
249
|
const decoder = new TextDecoder();
|
|
@@ -258,62 +266,90 @@ export function openai(config: OpenAIConfig): LLMProvider {
|
|
|
258
266
|
const event = JSON.parse(data);
|
|
259
267
|
|
|
260
268
|
switch (event.type) {
|
|
261
|
-
case "response.
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
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);
|
|
278
287
|
onChunk({
|
|
279
288
|
type: "tool_call_start",
|
|
280
289
|
toolCall: {
|
|
281
|
-
id:
|
|
282
|
-
name:
|
|
290
|
+
id: item.call_id,
|
|
291
|
+
name: item.name,
|
|
283
292
|
arguments: {},
|
|
284
293
|
},
|
|
285
294
|
});
|
|
286
295
|
}
|
|
287
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
|
+
}
|
|
288
309
|
|
|
289
|
-
case "response.
|
|
290
|
-
if (event.
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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);
|
|
322
|
+
const argsStr =
|
|
323
|
+
buffered && buffered.length > 0
|
|
324
|
+
? buffered
|
|
325
|
+
: item.arguments ?? "{}";
|
|
295
326
|
let args: Record<string, unknown> = {};
|
|
296
327
|
try {
|
|
297
328
|
args = JSON.parse(argsStr);
|
|
298
329
|
} catch {}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
+
}
|
|
304
337
|
}
|
|
305
338
|
break;
|
|
339
|
+
}
|
|
306
340
|
|
|
307
|
-
case "response.completed":
|
|
341
|
+
case "response.completed": {
|
|
308
342
|
responseId = event.response?.id ?? "";
|
|
309
343
|
usage = parseUsage(event.response?.usage);
|
|
310
344
|
onChunk({ type: "usage_update", usage });
|
|
311
|
-
|
|
312
|
-
//
|
|
313
|
-
if (
|
|
314
|
-
|
|
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);
|
|
315
350
|
}
|
|
316
351
|
break;
|
|
352
|
+
}
|
|
317
353
|
}
|
|
318
354
|
} catch {
|
|
319
355
|
// Skip malformed JSON
|
|
@@ -321,31 +357,44 @@ export function openai(config: OpenAIConfig): LLMProvider {
|
|
|
321
357
|
}
|
|
322
358
|
}
|
|
323
359
|
|
|
324
|
-
|
|
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");
|
|
325
365
|
|
|
326
366
|
return {
|
|
327
|
-
|
|
328
|
-
toolCalls: completedToolCalls,
|
|
367
|
+
blocks,
|
|
329
368
|
usage,
|
|
330
|
-
finishReason,
|
|
369
|
+
finishReason: hasToolCalls ? "tool_call" : "stop",
|
|
331
370
|
responseId,
|
|
332
371
|
};
|
|
333
372
|
}
|
|
334
373
|
|
|
335
|
-
const pricing: Record<
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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 },
|
|
344
392
|
};
|
|
345
393
|
|
|
346
394
|
return {
|
|
347
395
|
name: "openai",
|
|
348
396
|
models: Object.keys(pricing),
|
|
397
|
+
supportsContinuation: true,
|
|
349
398
|
complete,
|
|
350
399
|
streamComplete,
|
|
351
400
|
getPricing: (model: string) => pricing[model],
|