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