@opencode-ai/ai 0.0.0-next-16134 → 0.0.0-next-16144
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/README.md +1 -1
- package/dist/protocols/index.d.ts +1 -0
- package/dist/protocols/index.js +1 -0
- package/dist/protocols/open-responses.d.ts +613 -0
- package/dist/protocols/open-responses.js +756 -0
- package/dist/protocols/openai-chat.js +6 -7
- package/dist/protocols/openai-compatible-responses.d.ts +9 -21
- package/dist/protocols/openai-compatible-responses.js +8 -9
- package/dist/protocols/openai-responses.d.ts +55 -77
- package/dist/protocols/openai-responses.js +59 -771
- package/dist/protocols/utils/open-responses-options.d.ts +22 -0
- package/dist/protocols/utils/open-responses-options.js +42 -0
- package/dist/protocols/utils/openai-options.d.ts +8 -16
- package/dist/protocols/utils/openai-options.js +9 -59
- package/dist/protocols/utils/tool-schema.d.ts +1 -0
- package/dist/protocols/utils/tool-schema.js +2 -0
- package/dist/providers/azure.d.ts +4 -4
- package/dist/providers/github-copilot.d.ts +4 -4
- package/dist/providers/google-vertex-responses.d.ts +6 -18
- package/dist/providers/google-vertex-responses.js +1 -0
- package/dist/providers/open-responses-options.d.ts +17 -0
- package/dist/providers/open-responses-options.js +1 -0
- package/dist/providers/openai-compatible-responses.d.ts +10 -20
- package/dist/providers/openai-options.d.ts +3 -12
- package/dist/providers/openai.d.ts +8 -8
- package/dist/providers/xai.d.ts +4 -4
- package/dist/route/protocol.d.ts +2 -1
- package/package.json +3 -3
|
@@ -2,97 +2,18 @@ import { Effect, Encoding, Schema } from "effect";
|
|
|
2
2
|
import { Route } from "../route/client";
|
|
3
3
|
import { Auth } from "../route/auth";
|
|
4
4
|
import { Endpoint } from "../route/endpoint";
|
|
5
|
-
import { HttpTransport, WebSocketTransport } from "../route/transport";
|
|
6
5
|
import { Protocol } from "../route/protocol";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
6
|
+
import { HttpTransport, WebSocketTransport } from "../route/transport";
|
|
7
|
+
import { LLMEvent, LLMRequest } from "../schema";
|
|
8
|
+
import { OpenResponses } from "./open-responses";
|
|
9
|
+
import { optionalArray, ProviderShared } from "./shared";
|
|
11
10
|
import { Lifecycle } from "./utils/lifecycle";
|
|
12
|
-
import { ToolSchemaProjection } from "./utils/tool-schema";
|
|
13
|
-
import { ToolStream } from "./utils/tool-stream";
|
|
14
11
|
import { OpenAIImage } from "./utils/openai-image";
|
|
12
|
+
import { ToolSchemaProjection } from "./utils/tool-schema";
|
|
15
13
|
const ADAPTER = "openai-responses";
|
|
16
|
-
const
|
|
14
|
+
const NAME = "OpenAI Responses";
|
|
17
15
|
export const DEFAULT_BASE_URL = "https://api.openai.com/v1";
|
|
18
|
-
export const PATH =
|
|
19
|
-
// =============================================================================
|
|
20
|
-
// Request Body Schema
|
|
21
|
-
// =============================================================================
|
|
22
|
-
const OpenAIResponsesInputText = Schema.Struct({
|
|
23
|
-
type: Schema.tag("input_text"),
|
|
24
|
-
text: Schema.String,
|
|
25
|
-
});
|
|
26
|
-
const OpenAIResponsesInputImage = Schema.Struct({
|
|
27
|
-
type: Schema.tag("input_image"),
|
|
28
|
-
image_url: Schema.String,
|
|
29
|
-
});
|
|
30
|
-
const OpenAIResponsesInputFile = Schema.Struct({
|
|
31
|
-
type: Schema.tag("input_file"),
|
|
32
|
-
filename: Schema.String,
|
|
33
|
-
file_data: Schema.String,
|
|
34
|
-
mime_type: Schema.optional(Schema.String),
|
|
35
|
-
});
|
|
36
|
-
const OpenAIResponsesInputContent = Schema.Union([
|
|
37
|
-
OpenAIResponsesInputText,
|
|
38
|
-
OpenAIResponsesInputImage,
|
|
39
|
-
OpenAIResponsesInputFile,
|
|
40
|
-
]);
|
|
41
|
-
const OpenAIResponsesOutputText = Schema.Struct({
|
|
42
|
-
type: Schema.tag("output_text"),
|
|
43
|
-
text: Schema.String,
|
|
44
|
-
});
|
|
45
|
-
const OpenAIResponsesReasoningSummaryText = Schema.Struct({
|
|
46
|
-
type: Schema.tag("summary_text"),
|
|
47
|
-
text: Schema.String,
|
|
48
|
-
});
|
|
49
|
-
const OpenAIResponsesReasoningItem = Schema.Struct({
|
|
50
|
-
type: Schema.tag("reasoning"),
|
|
51
|
-
id: Schema.optionalKey(Schema.String),
|
|
52
|
-
summary: Schema.Array(OpenAIResponsesReasoningSummaryText),
|
|
53
|
-
encrypted_content: optionalNull(Schema.String),
|
|
54
|
-
});
|
|
55
|
-
const OpenAIResponsesItemReference = Schema.Struct({
|
|
56
|
-
type: Schema.tag("item_reference"),
|
|
57
|
-
id: Schema.String,
|
|
58
|
-
});
|
|
59
|
-
// `function_call_output.output` accepts either a plain string or an ordered
|
|
60
|
-
// array of content items so tools can return images and files in addition to text.
|
|
61
|
-
// https://platform.openai.com/docs/api-reference/responses/object
|
|
62
|
-
const OpenAIResponsesFunctionCallOutputContent = Schema.Union([
|
|
63
|
-
OpenAIResponsesInputText,
|
|
64
|
-
OpenAIResponsesInputImage,
|
|
65
|
-
OpenAIResponsesInputFile,
|
|
66
|
-
]);
|
|
67
|
-
const OpenAIResponsesFunctionCallOutput = Schema.Union([
|
|
68
|
-
Schema.String,
|
|
69
|
-
Schema.Array(OpenAIResponsesFunctionCallOutputContent),
|
|
70
|
-
]);
|
|
71
|
-
const OpenAIResponsesInputItem = Schema.Union([
|
|
72
|
-
Schema.Struct({ role: Schema.tag("system"), content: Schema.String }),
|
|
73
|
-
Schema.Struct({ role: Schema.tag("user"), content: Schema.Array(OpenAIResponsesInputContent) }),
|
|
74
|
-
Schema.Struct({ role: Schema.tag("assistant"), content: Schema.Array(OpenAIResponsesOutputText) }),
|
|
75
|
-
OpenAIResponsesReasoningItem,
|
|
76
|
-
OpenAIResponsesItemReference,
|
|
77
|
-
Schema.Struct({
|
|
78
|
-
type: Schema.tag("function_call"),
|
|
79
|
-
call_id: Schema.String,
|
|
80
|
-
name: Schema.String,
|
|
81
|
-
arguments: Schema.String,
|
|
82
|
-
}),
|
|
83
|
-
Schema.Struct({
|
|
84
|
-
type: Schema.tag("function_call_output"),
|
|
85
|
-
call_id: Schema.String,
|
|
86
|
-
output: OpenAIResponsesFunctionCallOutput,
|
|
87
|
-
}),
|
|
88
|
-
]);
|
|
89
|
-
const OpenAIResponsesTool = Schema.Struct({
|
|
90
|
-
type: Schema.tag("function"),
|
|
91
|
-
name: Schema.String,
|
|
92
|
-
description: Schema.String,
|
|
93
|
-
parameters: JsonObject,
|
|
94
|
-
strict: Schema.optional(Schema.Boolean),
|
|
95
|
-
});
|
|
16
|
+
export const PATH = OpenResponses.PATH;
|
|
96
17
|
const OpenAIResponsesImageGenerationTool = Schema.Struct({
|
|
97
18
|
type: Schema.tag("image_generation"),
|
|
98
19
|
action: Schema.optional(Schema.Literals(["auto", "generate", "edit"])),
|
|
@@ -104,36 +25,15 @@ const OpenAIResponsesImageGenerationTool = Schema.Struct({
|
|
|
104
25
|
quality: Schema.optional(Schema.Literals(["auto", "low", "medium", "high"])),
|
|
105
26
|
size: Schema.optional(OpenAIImage.Size),
|
|
106
27
|
});
|
|
107
|
-
const OpenAIResponsesTools = Schema.Union([
|
|
28
|
+
const OpenAIResponsesTools = Schema.Union([OpenResponses.Tool, OpenAIResponsesImageGenerationTool]);
|
|
108
29
|
const OpenAIResponsesToolChoice = Schema.Union([
|
|
109
|
-
|
|
110
|
-
Schema.Struct({ type: Schema.tag("function"), name: Schema.String }),
|
|
30
|
+
OpenResponses.ToolChoice,
|
|
111
31
|
Schema.Struct({ type: Schema.tag("image_generation") }),
|
|
112
32
|
]);
|
|
113
|
-
// Fields shared between the HTTP body and the WebSocket `response.create`
|
|
114
|
-
// message. The HTTP body adds `stream: true`; the WebSocket message adds
|
|
115
|
-
// `type: "response.create"`. Defining the shared shape once keeps the two
|
|
116
|
-
// transports in sync without a destructure-and-strip dance.
|
|
117
33
|
const OpenAIResponsesCoreFields = {
|
|
118
|
-
|
|
119
|
-
input: Schema.Array(OpenAIResponsesInputItem),
|
|
120
|
-
instructions: Schema.optional(Schema.String),
|
|
34
|
+
...OpenResponses.coreFields,
|
|
121
35
|
tools: optionalArray(OpenAIResponsesTools),
|
|
122
36
|
tool_choice: Schema.optional(OpenAIResponsesToolChoice),
|
|
123
|
-
store: Schema.optional(Schema.Boolean),
|
|
124
|
-
service_tier: Schema.optional(OpenAIOptions.OpenAIServiceTier),
|
|
125
|
-
prompt_cache_key: Schema.optional(Schema.String),
|
|
126
|
-
include: optionalArray(OpenAIOptions.OpenAIResponseIncludable),
|
|
127
|
-
reasoning: Schema.optional(Schema.Struct({
|
|
128
|
-
effort: Schema.optional(OpenAIOptions.OpenAIReasoningEffort),
|
|
129
|
-
summary: Schema.optional(Schema.Literal("auto")),
|
|
130
|
-
})),
|
|
131
|
-
text: Schema.optional(Schema.Struct({
|
|
132
|
-
verbosity: Schema.optional(OpenAIOptions.OpenAITextVerbosity),
|
|
133
|
-
})),
|
|
134
|
-
max_output_tokens: Schema.optional(Schema.Number),
|
|
135
|
-
temperature: Schema.optional(Schema.Number),
|
|
136
|
-
top_p: Schema.optional(Schema.Number),
|
|
137
37
|
};
|
|
138
38
|
const OpenAIResponsesBody = Schema.Struct({
|
|
139
39
|
...OpenAIResponsesCoreFields,
|
|
@@ -144,69 +44,20 @@ const OpenAIResponsesWebSocketMessage = Schema.StructWithRest(Schema.Struct({
|
|
|
144
44
|
...OpenAIResponsesCoreFields,
|
|
145
45
|
}), [Schema.Record(Schema.String, Schema.Unknown)]);
|
|
146
46
|
const encodeWebSocketMessage = Schema.encodeSync(Schema.fromJsonString(OpenAIResponsesWebSocketMessage));
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
// own subset of these — we capture them generically so we can surface the
|
|
162
|
-
// call's typed input portion and round-trip the full result payload without
|
|
163
|
-
// hand-rolling a per-tool schema.
|
|
164
|
-
status: Schema.optional(Schema.String),
|
|
165
|
-
action: Schema.optional(Schema.Unknown),
|
|
166
|
-
queries: Schema.optional(Schema.Unknown),
|
|
167
|
-
results: Schema.optional(Schema.Unknown),
|
|
168
|
-
code: Schema.optional(Schema.String),
|
|
169
|
-
container_id: Schema.optional(Schema.String),
|
|
170
|
-
outputs: Schema.optional(Schema.Unknown),
|
|
171
|
-
server_label: Schema.optional(Schema.String),
|
|
172
|
-
output: Schema.optional(Schema.Unknown),
|
|
173
|
-
result: Schema.optional(Schema.String),
|
|
174
|
-
output_format: Schema.optional(Schema.Literals(["png", "jpeg", "webp"])),
|
|
175
|
-
error: Schema.optional(Schema.Unknown),
|
|
176
|
-
encrypted_content: optionalNull(Schema.String),
|
|
177
|
-
});
|
|
178
|
-
// The Responses schema puts streaming error details at the top level and
|
|
179
|
-
// response failures under `response.error`. The official SDK also recognizes
|
|
180
|
-
// an event-level HTTP-style `error` envelope, so accept all three shapes here.
|
|
181
|
-
// https://github.com/openai/openai-openapi/blob/5162af98d3147432c14680df789e8e12d4891e6b/openapi.yaml#L67234-L67382
|
|
182
|
-
// https://github.com/openai/openai-node/blob/61539248cbe04665de68a71e6fd878127ae4db87/src/core/streaming.ts#L58-L85
|
|
183
|
-
const OpenAIResponsesErrorPayload = Schema.Struct({
|
|
184
|
-
code: optionalNull(Schema.String),
|
|
185
|
-
message: optionalNull(Schema.String),
|
|
186
|
-
param: optionalNull(Schema.String),
|
|
187
|
-
});
|
|
188
|
-
const OpenAIResponsesEvent = Schema.Struct({
|
|
189
|
-
type: Schema.String,
|
|
190
|
-
delta: Schema.optional(Schema.String),
|
|
191
|
-
item_id: Schema.optional(Schema.String),
|
|
192
|
-
summary_index: Schema.optional(Schema.Number),
|
|
193
|
-
item: Schema.optional(OpenAIResponsesStreamItem),
|
|
194
|
-
response: Schema.optional(Schema.StructWithRest(Schema.Struct({
|
|
195
|
-
id: Schema.optional(Schema.String),
|
|
196
|
-
service_tier: optionalNull(Schema.String),
|
|
197
|
-
incomplete_details: optionalNull(Schema.Struct({ reason: Schema.optional(Schema.String) })),
|
|
198
|
-
usage: optionalNull(OpenAIResponsesUsage),
|
|
199
|
-
error: optionalNull(OpenAIResponsesErrorPayload),
|
|
200
|
-
}), [Schema.Record(Schema.String, Schema.Unknown)])),
|
|
201
|
-
code: optionalNull(Schema.String),
|
|
202
|
-
message: Schema.optional(Schema.String),
|
|
203
|
-
param: optionalNull(Schema.String),
|
|
204
|
-
error: optionalNull(OpenAIResponsesErrorPayload),
|
|
205
|
-
});
|
|
206
|
-
const invalid = ProviderShared.invalidRequest;
|
|
207
|
-
// =============================================================================
|
|
208
|
-
// Request Lowering
|
|
209
|
-
// =============================================================================
|
|
47
|
+
const extension = {
|
|
48
|
+
id: ADAPTER,
|
|
49
|
+
name: NAME,
|
|
50
|
+
lowerMedia: ({ part, media, request }) => {
|
|
51
|
+
if (request.model.provider !== "xai" || media.mime !== "application/pdf")
|
|
52
|
+
return undefined;
|
|
53
|
+
return {
|
|
54
|
+
type: "input_file",
|
|
55
|
+
filename: part.filename ?? "document.pdf",
|
|
56
|
+
file_data: media.base64,
|
|
57
|
+
mime_type: media.mime,
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
};
|
|
210
61
|
const nativeImageToolInput = (tool) => {
|
|
211
62
|
const native = tool.native?.openai;
|
|
212
63
|
return ProviderShared.isRecord(native) && native.type === "image_generation" ? native : undefined;
|
|
@@ -220,18 +71,11 @@ const lowerTool = Effect.fn("OpenAIResponses.lowerTool")(function* (tool, inputS
|
|
|
220
71
|
if (native !== undefined) {
|
|
221
72
|
if (Schema.is(OpenAIResponsesImageGenerationTool)(native))
|
|
222
73
|
return native;
|
|
223
|
-
return yield*
|
|
74
|
+
return yield* ProviderShared.invalidRequest("OpenAI Responses image generation tool options are invalid");
|
|
224
75
|
}
|
|
225
|
-
return
|
|
226
|
-
type: "function",
|
|
227
|
-
name: tool.name,
|
|
228
|
-
description: tool.description,
|
|
229
|
-
parameters: ToolSchemaProjection.openAI(inputSchema),
|
|
230
|
-
// TODO: Read this from OpenAI-specific tool options so direct LLM callers can opt into strict schemas.
|
|
231
|
-
strict: false,
|
|
232
|
-
};
|
|
76
|
+
return yield* OpenResponses.lowerTool(NAME, tool, inputSchema);
|
|
233
77
|
});
|
|
234
|
-
const lowerToolChoice = (toolChoice, tools) => ProviderShared.matchToolChoice(
|
|
78
|
+
const lowerToolChoice = (toolChoice, tools) => ProviderShared.matchToolChoice(NAME, toolChoice, {
|
|
235
79
|
auto: () => "auto",
|
|
236
80
|
none: () => "none",
|
|
237
81
|
required: () => "required",
|
|
@@ -239,274 +83,17 @@ const lowerToolChoice = (toolChoice, tools) => ProviderShared.matchToolChoice("O
|
|
|
239
83
|
? { type: "image_generation" }
|
|
240
84
|
: { type: "function", name },
|
|
241
85
|
});
|
|
242
|
-
const lowerToolCall = (part) => ({
|
|
243
|
-
type: "function_call",
|
|
244
|
-
call_id: part.id,
|
|
245
|
-
name: part.name,
|
|
246
|
-
arguments: ProviderShared.encodeJson(part.input),
|
|
247
|
-
});
|
|
248
|
-
const lowerReasoning = (part) => {
|
|
249
|
-
const openai = part.providerMetadata?.openai;
|
|
250
|
-
if (!ProviderShared.isRecord(openai) || typeof openai.itemId !== "string" || openai.itemId.length === 0)
|
|
251
|
-
return undefined;
|
|
252
|
-
const encryptedContent = typeof openai.reasoningEncryptedContent === "string"
|
|
253
|
-
? openai.reasoningEncryptedContent
|
|
254
|
-
: openai.reasoningEncryptedContent === null
|
|
255
|
-
? null
|
|
256
|
-
: undefined;
|
|
257
|
-
return {
|
|
258
|
-
type: "reasoning",
|
|
259
|
-
id: openai.itemId,
|
|
260
|
-
summary: part.text.length > 0 ? [{ type: "summary_text", text: part.text }] : [],
|
|
261
|
-
encrypted_content: encryptedContent,
|
|
262
|
-
};
|
|
263
|
-
};
|
|
264
|
-
const hostedToolItemID = (part) => {
|
|
265
|
-
const openai = part.providerMetadata?.openai;
|
|
266
|
-
return ProviderShared.isRecord(openai) && typeof openai.itemId === "string" && openai.itemId.length > 0
|
|
267
|
-
? openai.itemId
|
|
268
|
-
: undefined;
|
|
269
|
-
};
|
|
270
|
-
const lowerMedia = Effect.fn("OpenAIResponses.lowerMedia")(function* (part, provider) {
|
|
271
|
-
const media = yield* ProviderShared.validateMedia("OpenAI Responses", part, MEDIA_MIMES);
|
|
272
|
-
if (media.mime === "application/pdf") {
|
|
273
|
-
// xAI models inline bytes and MIME separately; OpenAI uses a data URL in file_data.
|
|
274
|
-
if (provider === "xai")
|
|
275
|
-
return {
|
|
276
|
-
type: "input_file",
|
|
277
|
-
filename: part.filename ?? "document.pdf",
|
|
278
|
-
file_data: media.base64,
|
|
279
|
-
mime_type: media.mime,
|
|
280
|
-
};
|
|
281
|
-
return {
|
|
282
|
-
type: "input_file",
|
|
283
|
-
filename: part.filename ?? "document.pdf",
|
|
284
|
-
file_data: media.dataUrl,
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
return { type: "input_image", image_url: media.dataUrl };
|
|
288
|
-
});
|
|
289
|
-
const lowerUserContent = Effect.fn("OpenAIResponses.lowerUserContent")(function* (part, provider) {
|
|
290
|
-
if (part.type === "text")
|
|
291
|
-
return { type: "input_text", text: part.text };
|
|
292
|
-
if (part.type === "media")
|
|
293
|
-
return yield* lowerMedia(part, provider);
|
|
294
|
-
return yield* ProviderShared.unsupportedContent("OpenAI Responses", "user", ["text", "media"]);
|
|
295
|
-
});
|
|
296
|
-
// Tool results may carry structured text, images, and files. Keep media as provider-native
|
|
297
|
-
// content instead of JSON-stringifying base64 into a prompt string.
|
|
298
|
-
const lowerToolResultContentItem = Effect.fn("OpenAIResponses.lowerToolResultContentItem")(function* (item, provider) {
|
|
299
|
-
if (item.type === "text")
|
|
300
|
-
return { type: "input_text", text: item.text };
|
|
301
|
-
return yield* lowerMedia({ type: "media", mediaType: item.mime, data: item.uri, filename: item.name }, provider);
|
|
302
|
-
});
|
|
303
|
-
const lowerToolResultOutput = Effect.fn("OpenAIResponses.lowerToolResultOutput")(function* (part, provider) {
|
|
304
|
-
// Text/json/error results are encoded as a plain string for backward
|
|
305
|
-
// compatibility with existing cassettes and provider expectations.
|
|
306
|
-
if (part.result.type !== "content")
|
|
307
|
-
return ProviderShared.toolResultText(part);
|
|
308
|
-
// Preserve the narrowed array element type when compiled through a consumer package.
|
|
309
|
-
const content = part.result.value;
|
|
310
|
-
return yield* Effect.forEach(content, (item) => lowerToolResultContentItem(item, provider));
|
|
311
|
-
});
|
|
312
|
-
const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (request) {
|
|
313
|
-
const system = request.system.length === 0 ? [] : [{ role: "system", content: ProviderShared.joinText(request.system) }];
|
|
314
|
-
const input = [...system];
|
|
315
|
-
const store = OpenAIOptions.store(request);
|
|
316
|
-
for (const message of request.messages) {
|
|
317
|
-
if (message.role === "system") {
|
|
318
|
-
const part = yield* ProviderShared.wrappedSystemUpdate("OpenAI Responses", message);
|
|
319
|
-
const previous = input.at(-1);
|
|
320
|
-
if (previous && "role" in previous && previous.role === "user")
|
|
321
|
-
input[input.length - 1] = {
|
|
322
|
-
role: "user",
|
|
323
|
-
content: [...previous.content, { type: "input_text", text: part.text }],
|
|
324
|
-
};
|
|
325
|
-
else
|
|
326
|
-
input.push({ role: "user", content: [{ type: "input_text", text: part.text }] });
|
|
327
|
-
continue;
|
|
328
|
-
}
|
|
329
|
-
if (message.role === "user") {
|
|
330
|
-
input.push({
|
|
331
|
-
role: "user",
|
|
332
|
-
content: yield* Effect.forEach(message.content, (part) => lowerUserContent(part, request.model.provider)),
|
|
333
|
-
});
|
|
334
|
-
continue;
|
|
335
|
-
}
|
|
336
|
-
if (message.role === "assistant") {
|
|
337
|
-
const content = [];
|
|
338
|
-
const reasoningItems = {};
|
|
339
|
-
const reasoningReferences = new Set();
|
|
340
|
-
const hostedToolReferences = new Set();
|
|
341
|
-
const flushText = () => {
|
|
342
|
-
if (content.length === 0)
|
|
343
|
-
return;
|
|
344
|
-
input.push({ role: "assistant", content: content.map((part) => ({ type: "output_text", text: part.text })) });
|
|
345
|
-
content.splice(0, content.length);
|
|
346
|
-
};
|
|
347
|
-
for (const part of message.content) {
|
|
348
|
-
if (part.type === "text") {
|
|
349
|
-
content.push(part);
|
|
350
|
-
continue;
|
|
351
|
-
}
|
|
352
|
-
if (part.type === "reasoning") {
|
|
353
|
-
flushText();
|
|
354
|
-
const reasoning = lowerReasoning(part);
|
|
355
|
-
if (!reasoning)
|
|
356
|
-
continue;
|
|
357
|
-
if (store !== false) {
|
|
358
|
-
if (!reasoningReferences.has(reasoning.id))
|
|
359
|
-
input.push({ type: "item_reference", id: reasoning.id });
|
|
360
|
-
reasoningReferences.add(reasoning.id);
|
|
361
|
-
continue;
|
|
362
|
-
}
|
|
363
|
-
const existing = reasoningItems[reasoning.id];
|
|
364
|
-
if (existing) {
|
|
365
|
-
existing.summary.push(...reasoning.summary);
|
|
366
|
-
if (typeof reasoning.encrypted_content === "string")
|
|
367
|
-
existing.encrypted_content = reasoning.encrypted_content;
|
|
368
|
-
continue;
|
|
369
|
-
}
|
|
370
|
-
const replay = {
|
|
371
|
-
type: reasoning.type,
|
|
372
|
-
summary: reasoning.summary,
|
|
373
|
-
encrypted_content: reasoning.encrypted_content,
|
|
374
|
-
};
|
|
375
|
-
reasoningItems[reasoning.id] = replay;
|
|
376
|
-
input.push(replay);
|
|
377
|
-
continue;
|
|
378
|
-
}
|
|
379
|
-
if (part.type === "tool-call") {
|
|
380
|
-
flushText();
|
|
381
|
-
if (part.providerExecuted === true)
|
|
382
|
-
continue;
|
|
383
|
-
input.push(lowerToolCall(part));
|
|
384
|
-
continue;
|
|
385
|
-
}
|
|
386
|
-
if (part.type === "tool-result" && part.providerExecuted === true) {
|
|
387
|
-
flushText();
|
|
388
|
-
const itemID = hostedToolItemID(part);
|
|
389
|
-
if (store !== false && itemID && !hostedToolReferences.has(itemID))
|
|
390
|
-
input.push({ type: "item_reference", id: itemID });
|
|
391
|
-
if (store === false && part.name === "image_generation" && part.result.type === "content") {
|
|
392
|
-
const content = part.result.value;
|
|
393
|
-
input.push({
|
|
394
|
-
role: "user",
|
|
395
|
-
content: yield* Effect.forEach(content, (item) => lowerToolResultContentItem(item, request.model.provider)),
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
|
-
if (itemID)
|
|
399
|
-
hostedToolReferences.add(itemID);
|
|
400
|
-
continue;
|
|
401
|
-
}
|
|
402
|
-
return yield* ProviderShared.unsupportedContent("OpenAI Responses", "assistant", [
|
|
403
|
-
"text",
|
|
404
|
-
"reasoning",
|
|
405
|
-
"tool-call",
|
|
406
|
-
"tool-result",
|
|
407
|
-
]);
|
|
408
|
-
}
|
|
409
|
-
flushText();
|
|
410
|
-
continue;
|
|
411
|
-
}
|
|
412
|
-
for (const part of message.content) {
|
|
413
|
-
if (!ProviderShared.supportsContent(part, ["tool-result"]))
|
|
414
|
-
return yield* ProviderShared.unsupportedContent("OpenAI Responses", "tool", ["tool-result"]);
|
|
415
|
-
input.push({
|
|
416
|
-
type: "function_call_output",
|
|
417
|
-
call_id: part.id,
|
|
418
|
-
output: yield* lowerToolResultOutput(part, request.model.provider),
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
// With store:false, OpenAI only accepts previous reasoning items when the
|
|
423
|
-
// complete item has encrypted state. Summary blocks for one item may carry
|
|
424
|
-
// that state only on the last block, so filter after they have been joined.
|
|
425
|
-
return store === false
|
|
426
|
-
? input.filter((item) => !("type" in item) || item.type !== "reasoning" || typeof item.encrypted_content === "string")
|
|
427
|
-
: input;
|
|
428
|
-
});
|
|
429
|
-
const lowerOptions = Effect.fn("OpenAIResponses.lowerOptions")(function* (request) {
|
|
430
|
-
const store = OpenAIOptions.store(request);
|
|
431
|
-
const promptCacheKey = OpenAIOptions.promptCacheKey(request);
|
|
432
|
-
const effort = OpenAIOptions.reasoningEffort(request);
|
|
433
|
-
const summary = OpenAIOptions.reasoningSummary(request);
|
|
434
|
-
const include = OpenAIOptions.include(request);
|
|
435
|
-
const verbosity = OpenAIOptions.textVerbosity(request);
|
|
436
|
-
const instructions = OpenAIOptions.instructions(request);
|
|
437
|
-
const serviceTier = OpenAIOptions.serviceTier(request);
|
|
438
|
-
return {
|
|
439
|
-
...(instructions ? { instructions } : {}),
|
|
440
|
-
...(store !== undefined ? { store } : {}),
|
|
441
|
-
...(promptCacheKey ? { prompt_cache_key: promptCacheKey } : {}),
|
|
442
|
-
...(include ? { include } : {}),
|
|
443
|
-
...(effort || summary ? { reasoning: { effort, summary } } : {}),
|
|
444
|
-
...(verbosity ? { text: { verbosity } } : {}),
|
|
445
|
-
...(serviceTier ? { service_tier: serviceTier } : {}),
|
|
446
|
-
};
|
|
447
|
-
});
|
|
448
86
|
const fromRequest = Effect.fn("OpenAIResponses.fromRequest")(function* (request) {
|
|
449
|
-
const
|
|
450
|
-
const options = yield* lowerOptions(request);
|
|
87
|
+
const body = yield* OpenResponses.fromRequest(LLMRequest.update(request, { tools: [], toolChoice: undefined }), extension);
|
|
451
88
|
const toolSchemaCompatibility = request.model.compatibility?.toolSchema;
|
|
452
89
|
return {
|
|
453
|
-
|
|
454
|
-
input: yield* lowerMessages(request),
|
|
90
|
+
...body,
|
|
455
91
|
tools: request.tools.length === 0
|
|
456
92
|
? undefined
|
|
457
93
|
: yield* Effect.forEach(request.tools, (tool) => lowerTool(tool, ToolSchemaProjection.modelCompatibility(tool.inputSchema, toolSchemaCompatibility))),
|
|
458
94
|
tool_choice: request.toolChoice ? yield* lowerToolChoice(request.toolChoice, request.tools) : undefined,
|
|
459
|
-
stream: true,
|
|
460
|
-
max_output_tokens: generation?.maxTokens,
|
|
461
|
-
temperature: generation?.temperature,
|
|
462
|
-
top_p: generation?.topP,
|
|
463
|
-
...options,
|
|
464
95
|
};
|
|
465
96
|
});
|
|
466
|
-
// =============================================================================
|
|
467
|
-
// Stream Parsing
|
|
468
|
-
// =============================================================================
|
|
469
|
-
// OpenAI Responses reports `input_tokens` (inclusive total) with a
|
|
470
|
-
// `cached_tokens` subset, and `output_tokens` (inclusive total) with a
|
|
471
|
-
// `reasoning_tokens` subset. Pass the totals through and derive the
|
|
472
|
-
// non-cached breakdown.
|
|
473
|
-
const mapUsage = (usage) => {
|
|
474
|
-
if (!usage)
|
|
475
|
-
return undefined;
|
|
476
|
-
const cached = usage.input_tokens_details?.cached_tokens;
|
|
477
|
-
const reasoning = usage.output_tokens_details?.reasoning_tokens;
|
|
478
|
-
const nonCached = ProviderShared.subtractTokens(usage.input_tokens, cached);
|
|
479
|
-
return new Usage({
|
|
480
|
-
inputTokens: usage.input_tokens,
|
|
481
|
-
outputTokens: usage.output_tokens,
|
|
482
|
-
nonCachedInputTokens: nonCached,
|
|
483
|
-
cacheReadInputTokens: cached,
|
|
484
|
-
reasoningTokens: reasoning,
|
|
485
|
-
totalTokens: ProviderShared.totalTokens(usage.input_tokens, usage.output_tokens, usage.total_tokens),
|
|
486
|
-
providerMetadata: { openai: usage },
|
|
487
|
-
});
|
|
488
|
-
};
|
|
489
|
-
const mapFinishReason = (event, hasFunctionCall) => {
|
|
490
|
-
const reason = event.response?.incomplete_details?.reason;
|
|
491
|
-
if (reason === undefined || reason === null)
|
|
492
|
-
return hasFunctionCall ? "tool-calls" : event.type === "response.incomplete" ? "unknown" : "stop";
|
|
493
|
-
if (reason === "max_output_tokens")
|
|
494
|
-
return "length";
|
|
495
|
-
if (reason === "content_filter")
|
|
496
|
-
return "content-filter";
|
|
497
|
-
return hasFunctionCall ? "tool-calls" : "unknown";
|
|
498
|
-
};
|
|
499
|
-
const openaiMetadata = (metadata) => ({ openai: metadata });
|
|
500
|
-
// Hosted tool items (provider-executed) ship their typed input + status +
|
|
501
|
-
// result fields all in one item. We expose them as a `tool-call` +
|
|
502
|
-
// `tool-result` pair so consumers can treat them uniformly with client tools,
|
|
503
|
-
// only differentiated by `providerExecuted: true`.
|
|
504
|
-
//
|
|
505
|
-
// One record per OpenAI Responses item type that represents a hosted
|
|
506
|
-
// (provider-executed) tool call: the common name we surface, plus an `input`
|
|
507
|
-
// extractor that picks the fields the model actually populated for that tool.
|
|
508
|
-
// Falling back to `{}` when an entry isn't fully typed keeps unknown tools
|
|
509
|
-
// observable without rolling a per-tool schema.
|
|
510
97
|
const HOSTED_TOOLS = {
|
|
511
98
|
web_search_call: { name: "web_search", input: (item) => item.action ?? {} },
|
|
512
99
|
web_search_preview_call: { name: "web_search_preview", input: (item) => item.action ?? {} },
|
|
@@ -524,346 +111,53 @@ const HOSTED_TOOLS = {
|
|
|
524
111
|
local_shell_call: { name: "local_shell", input: (item) => item.action ?? {} },
|
|
525
112
|
};
|
|
526
113
|
const isHostedToolItem = (item) => item.type in HOSTED_TOOLS && typeof item.id === "string" && item.id.length > 0;
|
|
527
|
-
const isReasoningItem = (item) => item.type === "reasoning" && typeof item.id === "string" && item.id.length > 0;
|
|
528
|
-
// Round-trip the full item as the structured result so consumers can extract
|
|
529
|
-
// outputs / sources / status without re-decoding.
|
|
530
114
|
const hostedToolResult = Effect.fn("OpenAIResponses.hostedToolResult")(function* (item) {
|
|
531
|
-
const isError =
|
|
115
|
+
const isError = item.error !== undefined && item.error !== null;
|
|
532
116
|
if (item.type === "image_generation_call" && item.result) {
|
|
533
117
|
yield* Effect.fromResult(Encoding.decodeBase64(item.result)).pipe(Effect.mapError(() => ProviderShared.eventError(ADAPTER, "OpenAI Responses returned invalid image base64")));
|
|
118
|
+
const format = item.output_format ?? "png";
|
|
534
119
|
return {
|
|
535
120
|
type: "content",
|
|
536
121
|
value: [
|
|
537
122
|
{
|
|
538
123
|
type: "file",
|
|
539
|
-
uri: `data:image/${
|
|
540
|
-
mime: `image/${
|
|
124
|
+
uri: `data:image/${format};base64,${item.result}`,
|
|
125
|
+
mime: `image/${format}`,
|
|
541
126
|
},
|
|
542
127
|
],
|
|
543
128
|
};
|
|
544
129
|
}
|
|
545
130
|
return isError ? { type: "error", value: item.error } : { type: "json", value: item };
|
|
546
131
|
});
|
|
547
|
-
const
|
|
132
|
+
const onHostedToolDone = Effect.fn("OpenAIResponses.onHostedToolDone")(function* (state, item) {
|
|
548
133
|
const tool = HOSTED_TOOLS[item.type];
|
|
549
|
-
const providerMetadata =
|
|
550
|
-
return [
|
|
551
|
-
LLMEvent.toolCall({
|
|
552
|
-
id: item.id,
|
|
553
|
-
name: tool.name,
|
|
554
|
-
input: tool.input(item),
|
|
555
|
-
providerExecuted: true,
|
|
556
|
-
providerMetadata,
|
|
557
|
-
}),
|
|
558
|
-
LLMEvent.toolResult({
|
|
559
|
-
id: item.id,
|
|
560
|
-
name: tool.name,
|
|
561
|
-
result: yield* hostedToolResult(item),
|
|
562
|
-
providerExecuted: true,
|
|
563
|
-
providerMetadata,
|
|
564
|
-
}),
|
|
565
|
-
];
|
|
566
|
-
});
|
|
567
|
-
const NO_EVENTS = [];
|
|
568
|
-
// `response.completed` / `response.incomplete` are clean finishes that emit a
|
|
569
|
-
// `finish` event; `response.failed` is a hard failure. All three end the stream,
|
|
570
|
-
// so keep this set aligned with `step` and the protocol's terminal predicate.
|
|
571
|
-
const TERMINAL_TYPES = new Set(["response.completed", "response.incomplete", "response.failed"]);
|
|
572
|
-
const onOutputTextDelta = (state, event) => {
|
|
573
|
-
if (!event.delta)
|
|
574
|
-
return [state, NO_EVENTS];
|
|
575
|
-
const events = [];
|
|
576
|
-
return [
|
|
577
|
-
{ ...state, lifecycle: Lifecycle.textDelta(state.lifecycle, events, event.item_id ?? "text-0", event.delta) },
|
|
578
|
-
events,
|
|
579
|
-
];
|
|
580
|
-
};
|
|
581
|
-
const onOutputTextDone = (state, event) => {
|
|
582
|
-
const events = [];
|
|
583
|
-
return [{ ...state, lifecycle: Lifecycle.textEnd(state.lifecycle, events, event.item_id ?? "text-0") }, events];
|
|
584
|
-
};
|
|
585
|
-
const onReasoningDelta = (state, event) => {
|
|
586
|
-
if (!event.delta)
|
|
587
|
-
return [state, NO_EVENTS];
|
|
588
|
-
const events = [];
|
|
589
|
-
const itemID = event.item_id ?? "reasoning-0";
|
|
590
|
-
const id = event.summary_index !== undefined || state.reasoningItems[itemID] ? `${itemID}:${event.summary_index ?? 0}` : itemID;
|
|
591
|
-
return [
|
|
592
|
-
{
|
|
593
|
-
...state,
|
|
594
|
-
lifecycle: Lifecycle.reasoningDelta(state.lifecycle, events, id, event.delta),
|
|
595
|
-
},
|
|
596
|
-
events,
|
|
597
|
-
];
|
|
598
|
-
};
|
|
599
|
-
const onReasoningDone = (state, _event) => [state, NO_EVENTS];
|
|
600
|
-
const reasoningMetadata = (item) => openaiMetadata({ itemId: item.id, reasoningEncryptedContent: item.encrypted_content ?? null });
|
|
601
|
-
// OpenAI Responses streams reasoning items in a stable order:
|
|
602
|
-
// `output_item.added` (reasoning) →
|
|
603
|
-
// `reasoning_summary_part.added` (index=0) →
|
|
604
|
-
// `reasoning_summary_text.delta` →
|
|
605
|
-
// `reasoning_summary_part.done` (index=0) →
|
|
606
|
-
// (repeat for index>0) →
|
|
607
|
-
// `output_item.done` (reasoning).
|
|
608
|
-
// The handlers below rely on this ordering: `onOutputItemAdded` seeds the
|
|
609
|
-
// per-item entry, `onReasoningSummaryPartAdded` for `summary_index === 0`
|
|
610
|
-
// short-circuits when the entry already exists, and higher-index handlers
|
|
611
|
-
// fold against the same entry. Behaviour for out-of-order events is
|
|
612
|
-
// best-effort, not guaranteed.
|
|
613
|
-
const onOutputItemAdded = (state, event) => {
|
|
614
|
-
const item = event.item;
|
|
615
|
-
if (item && isReasoningItem(item)) {
|
|
616
|
-
const events = [];
|
|
617
|
-
return [
|
|
618
|
-
{
|
|
619
|
-
...state,
|
|
620
|
-
lifecycle: Lifecycle.reasoningStart(state.lifecycle, events, `${item.id}:0`, reasoningMetadata(item)),
|
|
621
|
-
reasoningItems: {
|
|
622
|
-
...state.reasoningItems,
|
|
623
|
-
[item.id]: { encryptedContent: item.encrypted_content, summaryParts: { 0: "active" } },
|
|
624
|
-
},
|
|
625
|
-
},
|
|
626
|
-
events,
|
|
627
|
-
];
|
|
628
|
-
}
|
|
629
|
-
if (item?.type !== "function_call" || !item.id)
|
|
630
|
-
return [state, NO_EVENTS];
|
|
631
|
-
const providerMetadata = openaiMetadata({ itemId: item.id });
|
|
134
|
+
const providerMetadata = OpenResponses.providerMetadata(state, { itemId: item.id });
|
|
632
135
|
const events = [];
|
|
633
136
|
const lifecycle = Lifecycle.stepStart(state.lifecycle, events);
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
];
|
|
648
|
-
};
|
|
649
|
-
const onReasoningSummaryPartAdded = (state, event) => {
|
|
650
|
-
if (!event.item_id || event.summary_index === undefined)
|
|
651
|
-
return [state, NO_EVENTS];
|
|
652
|
-
const item = state.reasoningItems[event.item_id] ?? { encryptedContent: undefined, summaryParts: {} };
|
|
653
|
-
if (event.summary_index === 0) {
|
|
654
|
-
if (state.reasoningItems[event.item_id])
|
|
655
|
-
return [state, NO_EVENTS];
|
|
656
|
-
const events = [];
|
|
657
|
-
return [
|
|
658
|
-
{
|
|
659
|
-
...state,
|
|
660
|
-
lifecycle: Lifecycle.reasoningStart(state.lifecycle, events, `${event.item_id}:0`, openaiMetadata({ itemId: event.item_id, reasoningEncryptedContent: null })),
|
|
661
|
-
reasoningItems: {
|
|
662
|
-
...state.reasoningItems,
|
|
663
|
-
[event.item_id]: { ...item, summaryParts: { 0: "active" } },
|
|
664
|
-
},
|
|
665
|
-
},
|
|
666
|
-
events,
|
|
667
|
-
];
|
|
668
|
-
}
|
|
669
|
-
const events = [];
|
|
670
|
-
const closed = Object.entries(item.summaryParts)
|
|
671
|
-
.filter((entry) => entry[1] === "can-conclude")
|
|
672
|
-
.reduce((lifecycle, entry) => Lifecycle.reasoningEnd(lifecycle, events, `${event.item_id}:${entry[0]}`, openaiMetadata({ itemId: event.item_id })), state.lifecycle);
|
|
673
|
-
return [
|
|
674
|
-
{
|
|
675
|
-
...state,
|
|
676
|
-
lifecycle: Lifecycle.reasoningStart(closed, events, `${event.item_id}:${event.summary_index}`, openaiMetadata({ itemId: event.item_id, reasoningEncryptedContent: item.encryptedContent ?? null })),
|
|
677
|
-
reasoningItems: {
|
|
678
|
-
...state.reasoningItems,
|
|
679
|
-
[event.item_id]: {
|
|
680
|
-
...item,
|
|
681
|
-
summaryParts: {
|
|
682
|
-
...Object.fromEntries(Object.entries(item.summaryParts).map((entry) => entry[1] === "can-conclude" ? [entry[0], "concluded"] : entry)),
|
|
683
|
-
[event.summary_index]: "active",
|
|
684
|
-
},
|
|
685
|
-
},
|
|
686
|
-
},
|
|
687
|
-
},
|
|
688
|
-
events,
|
|
689
|
-
];
|
|
690
|
-
};
|
|
691
|
-
const onReasoningSummaryPartDone = (state, event) => {
|
|
692
|
-
if (!event.item_id || event.summary_index === undefined)
|
|
693
|
-
return [state, NO_EVENTS];
|
|
694
|
-
const item = state.reasoningItems[event.item_id];
|
|
695
|
-
if (!item)
|
|
696
|
-
return [state, NO_EVENTS];
|
|
697
|
-
const events = [];
|
|
698
|
-
return [
|
|
699
|
-
{
|
|
700
|
-
...state,
|
|
701
|
-
lifecycle: state.store !== false
|
|
702
|
-
? Lifecycle.reasoningEnd(state.lifecycle, events, `${event.item_id}:${event.summary_index}`, openaiMetadata({ itemId: event.item_id }))
|
|
703
|
-
: state.lifecycle,
|
|
704
|
-
reasoningItems: {
|
|
705
|
-
...state.reasoningItems,
|
|
706
|
-
[event.item_id]: {
|
|
707
|
-
...item,
|
|
708
|
-
summaryParts: {
|
|
709
|
-
...item.summaryParts,
|
|
710
|
-
[event.summary_index]: state.store !== false ? "concluded" : "can-conclude",
|
|
711
|
-
},
|
|
712
|
-
},
|
|
713
|
-
},
|
|
714
|
-
},
|
|
715
|
-
events,
|
|
716
|
-
];
|
|
717
|
-
};
|
|
718
|
-
const onFunctionCallArgumentsDelta = Effect.fn("OpenAIResponses.onFunctionCallArgumentsDelta")(function* (state, event) {
|
|
719
|
-
if (!event.item_id || !event.delta)
|
|
720
|
-
return [state, NO_EVENTS];
|
|
721
|
-
const result = ToolStream.appendExisting(ADAPTER, state.tools, event.item_id, event.delta, "OpenAI Responses tool argument delta is missing its tool call");
|
|
722
|
-
if (ToolStream.isError(result))
|
|
723
|
-
return yield* result;
|
|
724
|
-
const events = [];
|
|
725
|
-
const lifecycle = result.events.length ? Lifecycle.stepStart(state.lifecycle, events) : state.lifecycle;
|
|
726
|
-
events.push(...result.events);
|
|
727
|
-
return [{ ...state, lifecycle, tools: result.tools }, events];
|
|
728
|
-
});
|
|
729
|
-
const onOutputItemDone = Effect.fn("OpenAIResponses.onOutputItemDone")(function* (state, event) {
|
|
730
|
-
const item = event.item;
|
|
731
|
-
if (!item)
|
|
732
|
-
return [state, NO_EVENTS];
|
|
733
|
-
if (item.type === "message" && item.id)
|
|
734
|
-
return onOutputTextDone(state, { ...event, item_id: item.id });
|
|
735
|
-
if (item.type === "function_call") {
|
|
736
|
-
if (!item.id || !item.call_id || !item.name)
|
|
737
|
-
return [state, NO_EVENTS];
|
|
738
|
-
const tools = state.tools[item.id]
|
|
739
|
-
? state.tools
|
|
740
|
-
: ToolStream.start(state.tools, item.id, { id: item.call_id, name: item.name });
|
|
741
|
-
const result = item.arguments === undefined
|
|
742
|
-
? yield* ToolStream.finish(ADAPTER, tools, item.id)
|
|
743
|
-
: yield* ToolStream.finishWithInput(ADAPTER, tools, item.id, item.arguments);
|
|
744
|
-
const events = [];
|
|
745
|
-
const resultEvents = result.events ?? [];
|
|
746
|
-
const lifecycle = resultEvents.length ? Lifecycle.stepStart(state.lifecycle, events) : state.lifecycle;
|
|
747
|
-
events.push(...resultEvents);
|
|
748
|
-
return [
|
|
749
|
-
{
|
|
750
|
-
...state,
|
|
751
|
-
lifecycle,
|
|
752
|
-
hasFunctionCall: resultEvents.some((event) => LLMEvent.is.toolCall(event) || LLMEvent.is.toolInputError(event)) ||
|
|
753
|
-
state.hasFunctionCall,
|
|
754
|
-
tools: result.tools,
|
|
755
|
-
},
|
|
756
|
-
events,
|
|
757
|
-
];
|
|
758
|
-
}
|
|
759
|
-
if (isHostedToolItem(item)) {
|
|
760
|
-
const events = [];
|
|
761
|
-
const lifecycle = Lifecycle.stepStart(state.lifecycle, events);
|
|
762
|
-
events.push(...(yield* hostedToolEvents(item)));
|
|
763
|
-
return [{ ...state, lifecycle }, events];
|
|
764
|
-
}
|
|
765
|
-
if (isReasoningItem(item)) {
|
|
766
|
-
const events = [];
|
|
767
|
-
const providerMetadata = reasoningMetadata(item);
|
|
768
|
-
const reasoningItem = state.reasoningItems[item.id];
|
|
769
|
-
if (reasoningItem) {
|
|
770
|
-
const lifecycle = Object.entries(reasoningItem.summaryParts)
|
|
771
|
-
.filter((entry) => entry[1] === "active" || entry[1] === "can-conclude")
|
|
772
|
-
.reduce((lifecycle, entry) => Lifecycle.reasoningEnd(lifecycle, events, `${item.id}:${entry[0]}`, providerMetadata), state.lifecycle);
|
|
773
|
-
const { [item.id]: _removed, ...reasoningItems } = state.reasoningItems;
|
|
774
|
-
return [{ ...state, lifecycle, reasoningItems }, events];
|
|
775
|
-
}
|
|
776
|
-
if (!state.lifecycle.reasoning.has(item.id)) {
|
|
777
|
-
const lifecycle = Lifecycle.stepStart(state.lifecycle, events);
|
|
778
|
-
events.push(LLMEvent.reasoningStart({ id: item.id, providerMetadata }));
|
|
779
|
-
events.push(LLMEvent.reasoningEnd({ id: item.id, providerMetadata }));
|
|
780
|
-
return [{ ...state, lifecycle }, events];
|
|
781
|
-
}
|
|
782
|
-
return [
|
|
783
|
-
{ ...state, lifecycle: Lifecycle.reasoningEnd(state.lifecycle, events, item.id, providerMetadata) },
|
|
784
|
-
events,
|
|
785
|
-
];
|
|
786
|
-
}
|
|
787
|
-
return [state, NO_EVENTS];
|
|
788
|
-
});
|
|
789
|
-
const onResponseFinish = (state, event) => {
|
|
790
|
-
const events = [];
|
|
791
|
-
const lifecycle = Lifecycle.finish(state.lifecycle, events, {
|
|
792
|
-
reason: {
|
|
793
|
-
normalized: mapFinishReason(event, state.hasFunctionCall),
|
|
794
|
-
raw: event.response?.incomplete_details?.reason,
|
|
795
|
-
},
|
|
796
|
-
usage: mapUsage(event.response?.usage),
|
|
797
|
-
providerMetadata: event.response?.id || event.response?.service_tier
|
|
798
|
-
? openaiMetadata({
|
|
799
|
-
responseId: event.response.id,
|
|
800
|
-
serviceTier: event.response.service_tier,
|
|
801
|
-
})
|
|
802
|
-
: undefined,
|
|
803
|
-
});
|
|
137
|
+
events.push(LLMEvent.toolCall({
|
|
138
|
+
id: item.id,
|
|
139
|
+
name: tool.name,
|
|
140
|
+
input: tool.input(item),
|
|
141
|
+
providerExecuted: true,
|
|
142
|
+
providerMetadata,
|
|
143
|
+
}), LLMEvent.toolResult({
|
|
144
|
+
id: item.id,
|
|
145
|
+
name: tool.name,
|
|
146
|
+
result: yield* hostedToolResult(item),
|
|
147
|
+
providerExecuted: true,
|
|
148
|
+
providerMetadata,
|
|
149
|
+
}));
|
|
804
150
|
return [{ ...state, lifecycle }, events];
|
|
805
|
-
};
|
|
806
|
-
// Build a single human-readable message from whatever the provider supplied.
|
|
807
|
-
// When both code and message are present, prefix the code so consumers see
|
|
808
|
-
// the failure mode (e.g. `rate_limit_exceeded: Slow down`) instead of just
|
|
809
|
-
// the bare message — production rate limits and context-length failures used
|
|
810
|
-
// to be indistinguishable from generic stream drops.
|
|
811
|
-
const providerErrorMessage = (event, fallback) => {
|
|
812
|
-
const nested = event.error ?? event.response?.error ?? undefined;
|
|
813
|
-
const message = event.message || nested?.message || undefined;
|
|
814
|
-
const code = event.code || nested?.code || undefined;
|
|
815
|
-
if (message && code)
|
|
816
|
-
return `${code}: ${message}`;
|
|
817
|
-
return message || code || fallback;
|
|
818
|
-
};
|
|
819
|
-
const providerError = (event, fallback) => {
|
|
820
|
-
const code = event.code || event.error?.code || event.response?.error?.code || undefined;
|
|
821
|
-
const message = providerErrorMessage(event, fallback);
|
|
822
|
-
return new LLMError({
|
|
823
|
-
module: ADAPTER,
|
|
824
|
-
method: "stream",
|
|
825
|
-
reason: classifyProviderFailure({ message, code }),
|
|
826
|
-
});
|
|
827
|
-
};
|
|
151
|
+
});
|
|
828
152
|
const step = (state, event) => {
|
|
829
|
-
if (event.type === "response.
|
|
830
|
-
return Effect.succeed(
|
|
831
|
-
if (event.type === "response.
|
|
832
|
-
return Effect.succeed(
|
|
833
|
-
if (event.type === "response.
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
return Effect.succeed(onReasoningDelta(state, event));
|
|
837
|
-
if (event.type === "response.reasoning_text.done" ||
|
|
838
|
-
event.type === "response.reasoning_summary.done" ||
|
|
839
|
-
event.type === "response.reasoning_summary_text.done")
|
|
840
|
-
return Effect.succeed(onReasoningDone(state, event));
|
|
841
|
-
if (event.type === "response.reasoning_summary_part.added")
|
|
842
|
-
return Effect.succeed(onReasoningSummaryPartAdded(state, event));
|
|
843
|
-
if (event.type === "response.reasoning_summary_part.done")
|
|
844
|
-
return Effect.succeed(onReasoningSummaryPartDone(state, event));
|
|
845
|
-
if (event.type === "response.output_item.added")
|
|
846
|
-
return Effect.succeed(onOutputItemAdded(state, event));
|
|
847
|
-
if (event.type === "response.function_call_arguments.delta")
|
|
848
|
-
return onFunctionCallArgumentsDelta(state, event);
|
|
849
|
-
if (event.type === "response.output_item.done")
|
|
850
|
-
return onOutputItemDone(state, event);
|
|
851
|
-
if (event.type === "response.completed" || event.type === "response.incomplete")
|
|
852
|
-
return Effect.succeed(onResponseFinish(state, event));
|
|
853
|
-
if (event.type === "response.failed")
|
|
854
|
-
return providerError(event, "OpenAI Responses response failed");
|
|
855
|
-
if (event.type === "error")
|
|
856
|
-
return providerError(event, "OpenAI Responses stream error");
|
|
857
|
-
return Effect.succeed([state, NO_EVENTS]);
|
|
153
|
+
if (event.type === "response.reasoning_text.delta" || event.type === "response.reasoning_summary.delta")
|
|
154
|
+
return Effect.succeed(OpenResponses.onReasoningDelta(state, event));
|
|
155
|
+
if (event.type === "response.reasoning_text.done" || event.type === "response.reasoning_summary.done")
|
|
156
|
+
return Effect.succeed(OpenResponses.onReasoningDone(state, event));
|
|
157
|
+
if (event.type === "response.output_item.done" && event.item && isHostedToolItem(event.item))
|
|
158
|
+
return onHostedToolDone(state, event.item);
|
|
159
|
+
return OpenResponses.step(state, event);
|
|
858
160
|
};
|
|
859
|
-
// =============================================================================
|
|
860
|
-
// Protocol And OpenAI Route
|
|
861
|
-
// =============================================================================
|
|
862
|
-
/**
|
|
863
|
-
* The OpenAI Responses protocol — request body construction, body schema, and
|
|
864
|
-
* the streaming-event state machine. Used by native OpenAI and (once
|
|
865
|
-
* registered) Azure OpenAI Responses.
|
|
866
|
-
*/
|
|
867
161
|
export const protocol = Protocol.make({
|
|
868
162
|
id: ADAPTER,
|
|
869
163
|
body: {
|
|
@@ -871,16 +165,10 @@ export const protocol = Protocol.make({
|
|
|
871
165
|
from: fromRequest,
|
|
872
166
|
},
|
|
873
167
|
stream: {
|
|
874
|
-
event:
|
|
875
|
-
initial: (request) => (
|
|
876
|
-
hasFunctionCall: false,
|
|
877
|
-
tools: ToolStream.empty(),
|
|
878
|
-
lifecycle: Lifecycle.initial(),
|
|
879
|
-
reasoningItems: {},
|
|
880
|
-
store: OpenAIOptions.store(request),
|
|
881
|
-
}),
|
|
168
|
+
event: OpenResponses.protocol.stream.event,
|
|
169
|
+
initial: (request) => OpenResponses.initial(request, extension),
|
|
882
170
|
step,
|
|
883
|
-
terminal:
|
|
171
|
+
terminal: OpenResponses.terminal,
|
|
884
172
|
},
|
|
885
173
|
});
|
|
886
174
|
const endpoint = Endpoint.path(PATH, { baseURL: DEFAULT_BASE_URL });
|