@opencode-ai/ai 0.0.0-beta-17492
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 +392 -0
- package/dist/cache-policy.d.ts +2 -0
- package/dist/cache-policy.js +121 -0
- package/dist/image-client.d.ts +19 -0
- package/dist/image-client.js +19 -0
- package/dist/image.d.ts +110 -0
- package/dist/image.js +102 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +12 -0
- package/dist/llm.d.ts +230 -0
- package/dist/llm.js +77 -0
- package/dist/protocols/anthropic-messages.d.ts +633 -0
- package/dist/protocols/anthropic-messages.js +860 -0
- package/dist/protocols/bedrock-converse.d.ts +533 -0
- package/dist/protocols/bedrock-converse.js +584 -0
- package/dist/protocols/bedrock-event-stream.d.ts +9 -0
- package/dist/protocols/bedrock-event-stream.js +73 -0
- package/dist/protocols/gemini.d.ts +300 -0
- package/dist/protocols/gemini.js +512 -0
- package/dist/protocols/google-images.d.ts +30 -0
- package/dist/protocols/google-images.js +191 -0
- package/dist/protocols/index.d.ts +10 -0
- package/dist/protocols/index.js +10 -0
- package/dist/protocols/open-responses-channel.d.ts +26 -0
- package/dist/protocols/open-responses-channel.js +120 -0
- package/dist/protocols/open-responses.d.ts +951 -0
- package/dist/protocols/open-responses.js +873 -0
- package/dist/protocols/openai-chat.d.ts +795 -0
- package/dist/protocols/openai-chat.js +718 -0
- package/dist/protocols/openai-compatible-chat.d.ts +107 -0
- package/dist/protocols/openai-compatible-chat.js +20 -0
- package/dist/protocols/openai-compatible-responses.d.ts +96 -0
- package/dist/protocols/openai-compatible-responses.js +17 -0
- package/dist/protocols/openai-images.d.ts +32 -0
- package/dist/protocols/openai-images.js +188 -0
- package/dist/protocols/openai-responses-channel.d.ts +12 -0
- package/dist/protocols/openai-responses-channel.js +142 -0
- package/dist/protocols/openai-responses.d.ts +596 -0
- package/dist/protocols/openai-responses.js +208 -0
- package/dist/protocols/shared.d.ts +194 -0
- package/dist/protocols/shared.js +238 -0
- package/dist/protocols/utils/bedrock-auth.d.ts +21 -0
- package/dist/protocols/utils/bedrock-auth.js +43 -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 +70 -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 +103 -0
- package/dist/protocols/utils/image-input.d.ts +21 -0
- package/dist/protocols/utils/image-input.js +22 -0
- package/dist/protocols/utils/lifecycle.d.ts +20 -0
- package/dist/protocols/utils/lifecycle.js +68 -0
- package/dist/protocols/utils/open-responses-options.d.ts +21 -0
- package/dist/protocols/utils/open-responses-options.js +41 -0
- package/dist/protocols/utils/openai-image.d.ts +5 -0
- package/dist/protocols/utils/openai-image.js +18 -0
- package/dist/protocols/utils/openai-options.d.ts +14 -0
- package/dist/protocols/utils/openai-options.js +14 -0
- package/dist/protocols/utils/tool-schema.d.ts +8 -0
- package/dist/protocols/utils/tool-schema.js +81 -0
- package/dist/protocols/utils/tool-stream.d.ts +639 -0
- package/dist/protocols/utils/tool-stream.js +141 -0
- package/dist/protocols/xai-images.d.ts +26 -0
- package/dist/protocols/xai-images.js +111 -0
- package/dist/protocols/zai-images.d.ts +22 -0
- package/dist/protocols/zai-images.js +84 -0
- package/dist/protocols.d.ts +1 -0
- package/dist/protocols.js +1 -0
- package/dist/provider-error.d.ts +14 -0
- package/dist/provider-error.js +130 -0
- package/dist/provider-package.d.ts +15 -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/mantle/chat.d.ts +2 -0
- package/dist/providers/amazon-bedrock/mantle/chat.js +1 -0
- package/dist/providers/amazon-bedrock/mantle/responses.d.ts +2 -0
- package/dist/providers/amazon-bedrock/mantle/responses.js +1 -0
- package/dist/providers/amazon-bedrock/mantle.d.ts +2 -0
- package/dist/providers/amazon-bedrock/mantle.js +1 -0
- package/dist/providers/amazon-bedrock-mantle.d.ts +247 -0
- package/dist/providers/amazon-bedrock-mantle.js +68 -0
- package/dist/providers/amazon-bedrock.d.ts +172 -0
- package/dist/providers/amazon-bedrock.js +41 -0
- package/dist/providers/anthropic-compatible.d.ts +218 -0
- package/dist/providers/anthropic-compatible.js +45 -0
- package/dist/providers/anthropic.d.ts +216 -0
- package/dist/providers/anthropic.js +40 -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 +248 -0
- package/dist/providers/azure.js +90 -0
- package/dist/providers/cloudflare.d.ts +338 -0
- package/dist/providers/cloudflare.js +87 -0
- package/dist/providers/google-vertex/chat.d.ts +2 -0
- package/dist/providers/google-vertex/chat.js +1 -0
- package/dist/providers/google-vertex/gemini.d.ts +2 -0
- package/dist/providers/google-vertex/gemini.js +1 -0
- package/dist/providers/google-vertex/messages.d.ts +2 -0
- package/dist/providers/google-vertex/messages.js +1 -0
- package/dist/providers/google-vertex/responses.d.ts +2 -0
- package/dist/providers/google-vertex/responses.js +1 -0
- package/dist/providers/google-vertex-chat.d.ts +131 -0
- package/dist/providers/google-vertex-chat.js +50 -0
- package/dist/providers/google-vertex-messages.d.ts +215 -0
- package/dist/providers/google-vertex-messages.js +75 -0
- package/dist/providers/google-vertex-responses.d.ts +122 -0
- package/dist/providers/google-vertex-responses.js +51 -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 +115 -0
- package/dist/providers/google-vertex.js +64 -0
- package/dist/providers/google.d.ts +112 -0
- package/dist/providers/google.js +43 -0
- package/dist/providers/index.d.ts +18 -0
- package/dist/providers/index.js +18 -0
- package/dist/providers/open-responses-options.d.ts +16 -0
- package/dist/providers/open-responses-options.js +1 -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 +120 -0
- package/dist/providers/openai-compatible-responses.js +33 -0
- package/dist/providers/openai-compatible.d.ts +167 -0
- package/dist/providers/openai-compatible.js +55 -0
- package/dist/providers/openai-options.d.ts +21 -0
- package/dist/providers/openai-options.js +41 -0
- package/dist/providers/openai.d.ts +264 -0
- package/dist/providers/openai.js +83 -0
- package/dist/providers/openrouter.d.ts +537 -0
- package/dist/providers/openrouter.js +117 -0
- package/dist/providers/xai.d.ts +249 -0
- package/dist/providers/xai.js +79 -0
- package/dist/providers/zai.d.ts +24 -0
- package/dist/providers/zai.js +21 -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 +51 -0
- package/dist/route/auth.js +89 -0
- package/dist/route/client.d.ts +348 -0
- package/dist/route/client.js +214 -0
- package/dist/route/endpoint.d.ts +28 -0
- package/dist/route/endpoint.js +21 -0
- package/dist/route/executor.d.ts +23 -0
- package/dist/route/executor.js +227 -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 +77 -0
- package/dist/route/protocol.js +17 -0
- package/dist/route/transport/http.d.ts +34 -0
- package/dist/route/transport/http.js +63 -0
- package/dist/route/transport/index.d.ts +39 -0
- package/dist/route/transport/index.js +4 -0
- package/dist/route/transport/websocket-channel.d.ts +56 -0
- package/dist/route/transport/websocket-channel.js +1 -0
- package/dist/route/transport/websocket.d.ts +55 -0
- package/dist/route/transport/websocket.js +326 -0
- package/dist/route.d.ts +1 -0
- package/dist/route.js +1 -0
- package/dist/schema/errors.d.ts +159 -0
- package/dist/schema/errors.js +155 -0
- package/dist/schema/events.d.ts +4587 -0
- package/dist/schema/events.js +492 -0
- package/dist/schema/ids.d.ts +30 -0
- package/dist/schema/ids.js +19 -0
- package/dist/schema/index.d.ts +5 -0
- package/dist/schema/index.js +5 -0
- package/dist/schema/messages.d.ts +402 -0
- package/dist/schema/messages.js +244 -0
- package/dist/schema/options.d.ts +159 -0
- package/dist/schema/options.js +202 -0
- package/dist/testing.d.ts +1493 -0
- package/dist/testing.js +88 -0
- package/dist/tool-runtime.d.ts +15 -0
- package/dist/tool-runtime.js +54 -0
- package/dist/tool.d.ts +135 -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 +47 -0
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { Tool } from "@opencode-ai/schema/tool";
|
|
3
|
+
import { Route } from "../route/client.js";
|
|
4
|
+
import { Auth } from "../route/auth.js";
|
|
5
|
+
import { Endpoint } from "../route/endpoint.js";
|
|
6
|
+
import { HttpTransport } from "../route/transport/index.js";
|
|
7
|
+
import { Protocol } from "../route/protocol.js";
|
|
8
|
+
import { AIError, LLMEvent, Usage, } from "../schema/index.js";
|
|
9
|
+
import { classifyProviderFailure } from "../provider-error.js";
|
|
10
|
+
import { isRecord, JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js";
|
|
11
|
+
import { OpenAIOptions } from "./utils/openai-options.js";
|
|
12
|
+
import { Lifecycle } from "./utils/lifecycle.js";
|
|
13
|
+
import { ToolSchemaProjection } from "./utils/tool-schema.js";
|
|
14
|
+
import { ToolStream } from "./utils/tool-stream.js";
|
|
15
|
+
const ADAPTER = "openai-chat";
|
|
16
|
+
const IMAGE_MIMES = new Set(ProviderShared.IMAGE_MIMES);
|
|
17
|
+
const RESERVED_REASONING_FIELDS = new Set(["role", "content", "tool_calls"]);
|
|
18
|
+
export const DEFAULT_BASE_URL = "https://api.openai.com/v1";
|
|
19
|
+
export const PATH = "/chat/completions";
|
|
20
|
+
// =============================================================================
|
|
21
|
+
// Request Body Schema
|
|
22
|
+
// =============================================================================
|
|
23
|
+
// The body schema is the provider-native JSON body. `fromRequest` below builds
|
|
24
|
+
// this shape from the common `LLMRequest`, then `Route.make` validates and
|
|
25
|
+
// JSON-encodes it before transport.
|
|
26
|
+
const OpenAIChatCacheControl = Schema.Struct({
|
|
27
|
+
type: Schema.Literal("ephemeral"),
|
|
28
|
+
ttl: Schema.optional(Schema.String),
|
|
29
|
+
});
|
|
30
|
+
const OpenAIChatFunction = Schema.Struct({
|
|
31
|
+
name: Schema.String,
|
|
32
|
+
description: Schema.String,
|
|
33
|
+
parameters: JsonObject,
|
|
34
|
+
});
|
|
35
|
+
const OpenAIChatTool = Schema.Struct({
|
|
36
|
+
type: Schema.tag("function"),
|
|
37
|
+
function: OpenAIChatFunction,
|
|
38
|
+
cache_control: Schema.optional(OpenAIChatCacheControl),
|
|
39
|
+
});
|
|
40
|
+
const OpenAIChatAssistantToolCall = Schema.Struct({
|
|
41
|
+
id: Schema.String,
|
|
42
|
+
type: Schema.tag("function"),
|
|
43
|
+
function: Schema.Struct({
|
|
44
|
+
name: Schema.String,
|
|
45
|
+
arguments: Schema.String,
|
|
46
|
+
}),
|
|
47
|
+
});
|
|
48
|
+
// Intentionally omit Gemini's provider-specific `extra_content.google.thought_signature`
|
|
49
|
+
// extension until direct Google OpenAI-compatible routing is supported here:
|
|
50
|
+
// https://github.com/vercel/ai/issues/11590
|
|
51
|
+
// https://github.com/vercel/ai/pull/11745
|
|
52
|
+
// https://ai.google.dev/gemini-api/docs/thought-signatures#openai
|
|
53
|
+
const OpenAIChatUserContent = Schema.Union([
|
|
54
|
+
Schema.Struct({
|
|
55
|
+
type: Schema.Literal("text"),
|
|
56
|
+
text: Schema.String,
|
|
57
|
+
cache_control: Schema.optional(OpenAIChatCacheControl),
|
|
58
|
+
}),
|
|
59
|
+
Schema.Struct({
|
|
60
|
+
type: Schema.Literal("image_url"),
|
|
61
|
+
image_url: Schema.Struct({ url: Schema.String }),
|
|
62
|
+
}),
|
|
63
|
+
]);
|
|
64
|
+
const OpenAIChatMessage = Schema.Union([
|
|
65
|
+
Schema.Struct({
|
|
66
|
+
role: Schema.Literal("system"),
|
|
67
|
+
content: Schema.Union([Schema.String, Schema.Array(OpenAIChatUserContent)]),
|
|
68
|
+
}),
|
|
69
|
+
Schema.Struct({
|
|
70
|
+
role: Schema.Literal("user"),
|
|
71
|
+
content: Schema.Union([Schema.String, Schema.Array(OpenAIChatUserContent)]),
|
|
72
|
+
}),
|
|
73
|
+
Schema.StructWithRest(Schema.Struct({
|
|
74
|
+
role: Schema.Literal("assistant"),
|
|
75
|
+
content: Schema.NullOr(Schema.String),
|
|
76
|
+
tool_calls: optionalArray(OpenAIChatAssistantToolCall),
|
|
77
|
+
reasoning_content: Schema.optional(Schema.String),
|
|
78
|
+
reasoning: Schema.optional(Schema.String),
|
|
79
|
+
reasoning_text: Schema.optional(Schema.String),
|
|
80
|
+
reasoning_details: Schema.optional(Schema.Unknown),
|
|
81
|
+
cache_control: Schema.optional(OpenAIChatCacheControl),
|
|
82
|
+
}), [Schema.Record(Schema.String, Schema.Unknown)]),
|
|
83
|
+
Schema.Struct({
|
|
84
|
+
role: Schema.Literal("tool"),
|
|
85
|
+
tool_call_id: Schema.String,
|
|
86
|
+
content: Schema.String,
|
|
87
|
+
cache_control: Schema.optional(OpenAIChatCacheControl),
|
|
88
|
+
}),
|
|
89
|
+
]).pipe(Schema.toTaggedUnion("role"));
|
|
90
|
+
const OpenAIChatToolChoice = Schema.Union([
|
|
91
|
+
Schema.Literals(["auto", "none", "required"]),
|
|
92
|
+
Schema.Struct({
|
|
93
|
+
type: Schema.tag("function"),
|
|
94
|
+
function: Schema.Struct({ name: Schema.String }),
|
|
95
|
+
}),
|
|
96
|
+
]);
|
|
97
|
+
export const bodyFields = {
|
|
98
|
+
model: Schema.String,
|
|
99
|
+
messages: Schema.Array(OpenAIChatMessage),
|
|
100
|
+
tools: optionalArray(OpenAIChatTool),
|
|
101
|
+
tool_choice: Schema.optional(OpenAIChatToolChoice),
|
|
102
|
+
stream: Schema.Literal(true),
|
|
103
|
+
stream_options: Schema.optional(Schema.Struct({ include_usage: Schema.Boolean })),
|
|
104
|
+
store: Schema.optional(Schema.Boolean),
|
|
105
|
+
prompt_cache_key: Schema.optional(Schema.String),
|
|
106
|
+
reasoning_effort: Schema.optional(OpenAIOptions.OpenAIReasoningEffort),
|
|
107
|
+
max_completion_tokens: Schema.optional(Schema.Number),
|
|
108
|
+
max_tokens: Schema.optional(Schema.Number),
|
|
109
|
+
temperature: Schema.optional(Schema.Number),
|
|
110
|
+
top_p: Schema.optional(Schema.Number),
|
|
111
|
+
frequency_penalty: Schema.optional(Schema.Number),
|
|
112
|
+
presence_penalty: Schema.optional(Schema.Number),
|
|
113
|
+
seed: Schema.optional(Schema.Number),
|
|
114
|
+
stop: optionalArray(Schema.String),
|
|
115
|
+
};
|
|
116
|
+
const OpenAIChatBody = Schema.Struct(bodyFields);
|
|
117
|
+
// =============================================================================
|
|
118
|
+
// Streaming Event Schema
|
|
119
|
+
// =============================================================================
|
|
120
|
+
// The event schema is one decoded SSE `data:` payload. `Framing.sse` splits the
|
|
121
|
+
// byte stream into strings, then `Protocol.jsonEvent` decodes each string into
|
|
122
|
+
// this provider-native event shape.
|
|
123
|
+
const OpenAIChatUsage = Schema.StructWithRest(Schema.Struct({
|
|
124
|
+
prompt_tokens: optionalNull(Schema.Number),
|
|
125
|
+
completion_tokens: optionalNull(Schema.Number),
|
|
126
|
+
total_tokens: optionalNull(Schema.Number),
|
|
127
|
+
prompt_tokens_details: optionalNull(Schema.StructWithRest(Schema.Struct({
|
|
128
|
+
cached_tokens: optionalNull(Schema.Number),
|
|
129
|
+
cache_write_tokens: optionalNull(Schema.Number),
|
|
130
|
+
}), [Schema.Record(Schema.String, Schema.Unknown)])),
|
|
131
|
+
completion_tokens_details: optionalNull(Schema.StructWithRest(Schema.Struct({
|
|
132
|
+
reasoning_tokens: optionalNull(Schema.Number),
|
|
133
|
+
accepted_prediction_tokens: optionalNull(Schema.Number),
|
|
134
|
+
rejected_prediction_tokens: optionalNull(Schema.Number),
|
|
135
|
+
}), [Schema.Record(Schema.String, Schema.Unknown)])),
|
|
136
|
+
}), [Schema.Record(Schema.String, Schema.Unknown)]);
|
|
137
|
+
const OpenAIChatToolCallDeltaFunction = Schema.Struct({
|
|
138
|
+
name: optionalNull(Schema.String),
|
|
139
|
+
arguments: optionalNull(Schema.String),
|
|
140
|
+
});
|
|
141
|
+
const OpenAIChatToolCallDelta = Schema.Struct({
|
|
142
|
+
index: optionalNull(Schema.Number),
|
|
143
|
+
id: optionalNull(Schema.String),
|
|
144
|
+
function: optionalNull(OpenAIChatToolCallDeltaFunction),
|
|
145
|
+
});
|
|
146
|
+
const OpenAIChatDelta = Schema.StructWithRest(Schema.Struct({
|
|
147
|
+
content: optionalNull(Schema.String),
|
|
148
|
+
reasoning_content: optionalNull(Schema.String),
|
|
149
|
+
reasoning: optionalNull(Schema.String),
|
|
150
|
+
reasoning_text: optionalNull(Schema.String),
|
|
151
|
+
reasoning_details: optionalNull(Schema.Unknown),
|
|
152
|
+
tool_calls: optionalNull(Schema.Array(OpenAIChatToolCallDelta)),
|
|
153
|
+
}), [Schema.Record(Schema.String, Schema.Unknown)]);
|
|
154
|
+
const OpenAIChatChoice = Schema.Struct({
|
|
155
|
+
delta: optionalNull(OpenAIChatDelta),
|
|
156
|
+
finish_reason: optionalNull(Schema.String),
|
|
157
|
+
native_finish_reason: optionalNull(Schema.String),
|
|
158
|
+
});
|
|
159
|
+
const OpenAIChatError = Schema.Struct({
|
|
160
|
+
code: optionalNull(Schema.Union([Schema.String, Schema.Number])),
|
|
161
|
+
message: Schema.String,
|
|
162
|
+
});
|
|
163
|
+
export const OpenAIChatEvent = Schema.Struct({
|
|
164
|
+
choices: optionalNull(Schema.Array(OpenAIChatChoice)),
|
|
165
|
+
usage: optionalNull(OpenAIChatUsage),
|
|
166
|
+
error: optionalNull(OpenAIChatError),
|
|
167
|
+
});
|
|
168
|
+
const lowerTool = (tool, inputSchema, options) => ({
|
|
169
|
+
type: "function",
|
|
170
|
+
function: {
|
|
171
|
+
name: tool.name,
|
|
172
|
+
description: tool.description,
|
|
173
|
+
parameters: ToolSchemaProjection.openAI(inputSchema),
|
|
174
|
+
},
|
|
175
|
+
cache_control: options.cacheControl?.(tool.cache),
|
|
176
|
+
});
|
|
177
|
+
const lowerToolChoice = (toolChoice) => ProviderShared.matchToolChoice("OpenAI Chat", toolChoice, {
|
|
178
|
+
auto: () => "auto",
|
|
179
|
+
none: () => "none",
|
|
180
|
+
required: () => "required",
|
|
181
|
+
tool: (name) => ({ type: "function", function: { name } }),
|
|
182
|
+
});
|
|
183
|
+
const lowerToolCall = (part) => ({
|
|
184
|
+
id: part.id,
|
|
185
|
+
type: "function",
|
|
186
|
+
function: {
|
|
187
|
+
name: part.name,
|
|
188
|
+
arguments: ProviderShared.encodeJson(part.input),
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
const lowerMedia = Effect.fn("OpenAIChat.lowerMedia")(function* (part) {
|
|
192
|
+
const media = yield* ProviderShared.validateMedia("OpenAI Chat", part, IMAGE_MIMES);
|
|
193
|
+
return { type: "image_url", image_url: { url: media.dataUrl } };
|
|
194
|
+
});
|
|
195
|
+
const openAICompatibleReasoningContent = (native) => isRecord(native) && typeof native.reasoning_content === "string" ? native.reasoning_content : undefined;
|
|
196
|
+
const reasoningField = (part) => {
|
|
197
|
+
const field = part.providerMetadata?.openai?.reasoningField;
|
|
198
|
+
return typeof field === "string" ? field : undefined;
|
|
199
|
+
};
|
|
200
|
+
const reasoningDetails = (parts, native) => {
|
|
201
|
+
const observed = parts.flatMap((part) => {
|
|
202
|
+
const details = part.providerMetadata?.openai?.reasoningDetails;
|
|
203
|
+
return Array.isArray(details) ? details : [];
|
|
204
|
+
});
|
|
205
|
+
if (parts.some((part) => Array.isArray(part.providerMetadata?.openai?.reasoningDetails)))
|
|
206
|
+
return observed;
|
|
207
|
+
if (isRecord(native) && Array.isArray(native.reasoning_details))
|
|
208
|
+
return native.reasoning_details;
|
|
209
|
+
};
|
|
210
|
+
const lowerUserMessage = Effect.fn("OpenAIChat.lowerUserMessage")(function* (message, options) {
|
|
211
|
+
const content = [];
|
|
212
|
+
for (const part of message.content) {
|
|
213
|
+
if (part.type === "text") {
|
|
214
|
+
content.push({ type: "text", text: part.text, cache_control: options.cacheControl?.(part.cache) });
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (part.type === "media") {
|
|
218
|
+
content.push(yield* lowerMedia(part));
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
return yield* ProviderShared.unsupportedContent("OpenAI Chat", "user", ["text", "media"]);
|
|
222
|
+
}
|
|
223
|
+
if (content.every((part) => part.type === "text" && part.cache_control === undefined))
|
|
224
|
+
return {
|
|
225
|
+
role: "user",
|
|
226
|
+
content: content.map((part) => (part.type === "text" ? part.text : "")).join(""),
|
|
227
|
+
};
|
|
228
|
+
return { role: "user", content };
|
|
229
|
+
});
|
|
230
|
+
const lowerAssistantMessage = Effect.fn("OpenAIChat.lowerAssistantMessage")(function* (message, configuredField, options = {}) {
|
|
231
|
+
const content = [];
|
|
232
|
+
const reasoning = [];
|
|
233
|
+
const toolCalls = [];
|
|
234
|
+
for (const part of message.content) {
|
|
235
|
+
if (!ProviderShared.supportsContent(part, ["text", "reasoning", "tool-call"]))
|
|
236
|
+
return yield* ProviderShared.unsupportedContent("OpenAI Chat", "assistant", ["text", "reasoning", "tool-call"]);
|
|
237
|
+
if (part.type === "text") {
|
|
238
|
+
content.push(part);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (part.type === "reasoning") {
|
|
242
|
+
reasoning.push(part);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (part.type === "tool-call") {
|
|
246
|
+
toolCalls.push(lowerToolCall(part));
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const text = reasoning.map((part) => part.text).join("");
|
|
251
|
+
const details = reasoningDetails(reasoning, message.native?.openaiCompatible);
|
|
252
|
+
const observedField = reasoning.map(reasoningField).find((value) => value !== undefined);
|
|
253
|
+
const nativeReasoning = openAICompatibleReasoningContent(message.native?.openaiCompatible);
|
|
254
|
+
const fullyStructured = reasoning.every((part) => Array.isArray(part.providerMetadata?.openai?.reasoningDetails));
|
|
255
|
+
const field = (() => {
|
|
256
|
+
if (configuredField !== undefined)
|
|
257
|
+
return configuredField;
|
|
258
|
+
if (reasoning.length === 0)
|
|
259
|
+
return undefined;
|
|
260
|
+
if (observedField !== undefined)
|
|
261
|
+
return observedField;
|
|
262
|
+
if (nativeReasoning !== undefined)
|
|
263
|
+
return "reasoning_content";
|
|
264
|
+
if (!fullyStructured)
|
|
265
|
+
return "reasoning_content";
|
|
266
|
+
})();
|
|
267
|
+
const reasoningText = (() => {
|
|
268
|
+
if (configuredField !== undefined)
|
|
269
|
+
return reasoning.length === 0 ? (nativeReasoning ?? "") : text;
|
|
270
|
+
if (reasoning.length === 0)
|
|
271
|
+
return nativeReasoning;
|
|
272
|
+
return text;
|
|
273
|
+
})();
|
|
274
|
+
const cached = message.content.findLast((part) => "cache" in part && part.cache !== undefined);
|
|
275
|
+
const cacheControl = options.cacheControl?.(cached && "cache" in cached ? cached.cache : undefined);
|
|
276
|
+
const result = {
|
|
277
|
+
role: "assistant",
|
|
278
|
+
content: content.length > 0 ? content.map((part) => part.text).join("") : toolCalls.length > 0 ? null : "",
|
|
279
|
+
...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
|
|
280
|
+
...(details !== undefined ? { reasoning_details: details } : {}),
|
|
281
|
+
...(cacheControl !== undefined ? { cache_control: cacheControl } : {}),
|
|
282
|
+
};
|
|
283
|
+
if (field === undefined || reasoningText === undefined)
|
|
284
|
+
return result;
|
|
285
|
+
return { ...result, [field]: reasoningText };
|
|
286
|
+
});
|
|
287
|
+
const lowerToolMessages = Effect.fn("OpenAIChat.lowerToolMessages")(function* (message, options) {
|
|
288
|
+
const messages = [];
|
|
289
|
+
const images = [];
|
|
290
|
+
for (const part of message.content) {
|
|
291
|
+
if (!ProviderShared.supportsContent(part, ["tool-result"]))
|
|
292
|
+
return yield* ProviderShared.unsupportedContent("OpenAI Chat", "tool", ["tool-result"]);
|
|
293
|
+
if (part.result.type !== "content") {
|
|
294
|
+
messages.push({
|
|
295
|
+
role: "tool",
|
|
296
|
+
tool_call_id: part.id,
|
|
297
|
+
content: ProviderShared.toolResultText(part),
|
|
298
|
+
cache_control: options.cacheControl?.(part.cache),
|
|
299
|
+
});
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
const content = part.result.value;
|
|
303
|
+
const text = content.filter((item) => item.type === "text").map((item) => item.text);
|
|
304
|
+
messages.push({
|
|
305
|
+
role: "tool",
|
|
306
|
+
tool_call_id: part.id,
|
|
307
|
+
content: text.join("\n"),
|
|
308
|
+
cache_control: options.cacheControl?.(part.cache),
|
|
309
|
+
});
|
|
310
|
+
const files = content.filter((item) => item.type === "file");
|
|
311
|
+
images.push(...(yield* Effect.forEach(files, (item) => lowerMedia({ type: "media", mediaType: item.mime, data: item.uri, filename: item.name }))));
|
|
312
|
+
}
|
|
313
|
+
return { messages, images };
|
|
314
|
+
});
|
|
315
|
+
const lowerMessage = Effect.fn("OpenAIChat.lowerMessage")(function* (message, reasoningField, options = {}) {
|
|
316
|
+
if (message.role === "user")
|
|
317
|
+
return [yield* lowerUserMessage(message, options)];
|
|
318
|
+
if (message.role === "assistant")
|
|
319
|
+
return [yield* lowerAssistantMessage(message, reasoningField, options)];
|
|
320
|
+
return (yield* lowerToolMessages(message, options)).messages;
|
|
321
|
+
});
|
|
322
|
+
const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request, options) {
|
|
323
|
+
const system = request.system.length === 0
|
|
324
|
+
? []
|
|
325
|
+
: request.system.some((part) => part.cache !== undefined) && options.cacheControl !== undefined
|
|
326
|
+
? [
|
|
327
|
+
{
|
|
328
|
+
role: "system",
|
|
329
|
+
content: request.system.map((part) => ({
|
|
330
|
+
type: "text",
|
|
331
|
+
text: part.text,
|
|
332
|
+
cache_control: options.cacheControl?.(part.cache),
|
|
333
|
+
})),
|
|
334
|
+
},
|
|
335
|
+
]
|
|
336
|
+
: [{ role: "system", content: ProviderShared.joinText(request.system) }];
|
|
337
|
+
const messages = [...system];
|
|
338
|
+
const pendingImages = [];
|
|
339
|
+
const flushImages = () => {
|
|
340
|
+
if (pendingImages.length === 0)
|
|
341
|
+
return;
|
|
342
|
+
messages.push({ role: "user", content: pendingImages.splice(0) });
|
|
343
|
+
};
|
|
344
|
+
for (const message of request.messages) {
|
|
345
|
+
if (message.role === "system") {
|
|
346
|
+
const part = yield* ProviderShared.wrappedSystemUpdate("OpenAI Chat", message);
|
|
347
|
+
if (pendingImages.length > 0) {
|
|
348
|
+
messages.push({
|
|
349
|
+
role: "user",
|
|
350
|
+
content: [
|
|
351
|
+
...pendingImages.splice(0),
|
|
352
|
+
{ type: "text", text: part.text, cache_control: options.cacheControl?.(part.cache) },
|
|
353
|
+
],
|
|
354
|
+
});
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
const previous = messages.at(-1);
|
|
358
|
+
if (previous?.role === "user" && typeof previous.content === "string")
|
|
359
|
+
messages[messages.length - 1] = options.cacheControl?.(part.cache)
|
|
360
|
+
? {
|
|
361
|
+
role: "user",
|
|
362
|
+
content: [
|
|
363
|
+
{ type: "text", text: previous.content },
|
|
364
|
+
{ type: "text", text: part.text, cache_control: options.cacheControl(part.cache) },
|
|
365
|
+
],
|
|
366
|
+
}
|
|
367
|
+
: { role: "user", content: `${previous.content}\n${part.text}` };
|
|
368
|
+
else if (previous?.role === "user" && Array.isArray(previous.content))
|
|
369
|
+
messages[messages.length - 1] = {
|
|
370
|
+
role: "user",
|
|
371
|
+
content: [
|
|
372
|
+
...previous.content,
|
|
373
|
+
{ type: "text", text: part.text, cache_control: options.cacheControl?.(part.cache) },
|
|
374
|
+
],
|
|
375
|
+
};
|
|
376
|
+
else
|
|
377
|
+
messages.push(options.cacheControl?.(part.cache)
|
|
378
|
+
? {
|
|
379
|
+
role: "user",
|
|
380
|
+
content: [{ type: "text", text: part.text, cache_control: options.cacheControl(part.cache) }],
|
|
381
|
+
}
|
|
382
|
+
: { role: "user", content: part.text });
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
if (message.role === "tool") {
|
|
386
|
+
const lowered = yield* lowerToolMessages(message, options);
|
|
387
|
+
messages.push(...lowered.messages);
|
|
388
|
+
pendingImages.push(...lowered.images);
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
flushImages();
|
|
392
|
+
messages.push(...(yield* lowerMessage(message, request.model.compatibility?.reasoningField, options)));
|
|
393
|
+
}
|
|
394
|
+
flushImages();
|
|
395
|
+
return messages;
|
|
396
|
+
});
|
|
397
|
+
const lowerOptions = (request) => {
|
|
398
|
+
const options = OpenAIOptions.resolve(request);
|
|
399
|
+
return {
|
|
400
|
+
...(options.store !== undefined ? { store: options.store } : {}),
|
|
401
|
+
...(request.promptCacheKey ? { prompt_cache_key: request.promptCacheKey } : {}),
|
|
402
|
+
...(options.reasoningEffort ? { reasoning_effort: options.reasoningEffort } : {}),
|
|
403
|
+
};
|
|
404
|
+
};
|
|
405
|
+
export const fromRequest = Effect.fn("OpenAIChat.fromRequest")(function* (request, options = {}) {
|
|
406
|
+
// `fromRequest` returns the provider body only. Endpoint, auth, framing,
|
|
407
|
+
// validation, and HTTP execution are composed by `Route.make`.
|
|
408
|
+
const reasoningField = request.model.compatibility?.reasoningField;
|
|
409
|
+
if (reasoningField && RESERVED_REASONING_FIELDS.has(reasoningField))
|
|
410
|
+
return yield* ProviderShared.invalidRequest(`OpenAI Chat reasoning field conflicts with reserved field ${reasoningField}`);
|
|
411
|
+
const generation = request.generation;
|
|
412
|
+
const toolSchemaCompatibility = request.model.compatibility?.toolSchema;
|
|
413
|
+
const maxTokensField = request.model.compatibility?.maxTokensField ?? "max_tokens";
|
|
414
|
+
return {
|
|
415
|
+
model: request.model.id,
|
|
416
|
+
messages: yield* lowerMessages(request, options),
|
|
417
|
+
tools: request.tools.length === 0
|
|
418
|
+
? undefined
|
|
419
|
+
: request.tools.map((tool) => lowerTool(tool, ToolSchemaProjection.modelCompatibility(tool.inputSchema, toolSchemaCompatibility), options)),
|
|
420
|
+
tool_choice: request.toolChoice ? yield* lowerToolChoice(request.toolChoice) : undefined,
|
|
421
|
+
stream: true,
|
|
422
|
+
stream_options: { include_usage: true },
|
|
423
|
+
...(maxTokensField === "max_completion_tokens"
|
|
424
|
+
? { max_completion_tokens: generation?.maxTokens }
|
|
425
|
+
: { max_tokens: generation?.maxTokens }),
|
|
426
|
+
temperature: generation?.temperature,
|
|
427
|
+
top_p: generation?.topP,
|
|
428
|
+
frequency_penalty: generation?.frequencyPenalty,
|
|
429
|
+
presence_penalty: generation?.presencePenalty,
|
|
430
|
+
seed: generation?.seed,
|
|
431
|
+
stop: generation?.stop,
|
|
432
|
+
...lowerOptions(request),
|
|
433
|
+
};
|
|
434
|
+
});
|
|
435
|
+
// =============================================================================
|
|
436
|
+
// Stream Parsing
|
|
437
|
+
// =============================================================================
|
|
438
|
+
// Streaming parsers are small state machines: every event returns a new state
|
|
439
|
+
// plus the common `LLMEvent`s produced by that event. Tool calls are accumulated
|
|
440
|
+
// because OpenAI streams JSON arguments across multiple deltas.
|
|
441
|
+
const mapFinishReason = (reason) => {
|
|
442
|
+
if (reason === "stop")
|
|
443
|
+
return "stop";
|
|
444
|
+
if (reason === "length")
|
|
445
|
+
return "length";
|
|
446
|
+
if (reason === "content_filter")
|
|
447
|
+
return "content-filter";
|
|
448
|
+
if (reason === "function_call" || reason === "tool_calls")
|
|
449
|
+
return "tool-calls";
|
|
450
|
+
if (reason === "error")
|
|
451
|
+
return "error";
|
|
452
|
+
return "unknown";
|
|
453
|
+
};
|
|
454
|
+
// OpenAI Chat reports `prompt_tokens` (inclusive total) with a
|
|
455
|
+
// cached-read and cache-write subsets, and `completion_tokens` (inclusive
|
|
456
|
+
// total) with a `reasoning_tokens` subset. We pass the inclusive totals
|
|
457
|
+
// through and derive the non-cached breakdown so the `AI.Usage` contract is
|
|
458
|
+
// satisfied on both sides.
|
|
459
|
+
const mapUsage = (usage) => {
|
|
460
|
+
if (!usage)
|
|
461
|
+
return undefined;
|
|
462
|
+
const input = usage.prompt_tokens ?? undefined;
|
|
463
|
+
const output = usage.completion_tokens ?? undefined;
|
|
464
|
+
const cached = usage.prompt_tokens_details?.cached_tokens ?? undefined;
|
|
465
|
+
const cacheWrite = usage.prompt_tokens_details?.cache_write_tokens ?? undefined;
|
|
466
|
+
const reasoning = usage.completion_tokens_details?.reasoning_tokens ?? undefined;
|
|
467
|
+
const nonCached = ProviderShared.subtractTokens(input, ProviderShared.sumTokens(cached, cacheWrite));
|
|
468
|
+
return new Usage({
|
|
469
|
+
inputTokens: input,
|
|
470
|
+
outputTokens: output,
|
|
471
|
+
nonCachedInputTokens: nonCached,
|
|
472
|
+
cacheReadInputTokens: cached,
|
|
473
|
+
cacheWriteInputTokens: cacheWrite,
|
|
474
|
+
reasoningTokens: reasoning,
|
|
475
|
+
totalTokens: ProviderShared.totalTokens(input, output, usage.total_tokens ?? undefined),
|
|
476
|
+
providerMetadata: { openai: usage },
|
|
477
|
+
});
|
|
478
|
+
};
|
|
479
|
+
const toolIndexByID = (tools, pendingTools, id) => {
|
|
480
|
+
if (!id)
|
|
481
|
+
return undefined;
|
|
482
|
+
const entry = Object.entries({ ...pendingTools, ...tools }).find(([, tool]) => tool?.id === id);
|
|
483
|
+
return entry ? Number(entry[0]) : undefined;
|
|
484
|
+
};
|
|
485
|
+
const reasoningDelta = (delta, configuredField) => {
|
|
486
|
+
if (!delta)
|
|
487
|
+
return undefined;
|
|
488
|
+
const fields = new Set([configuredField, "reasoning_content", "reasoning", "reasoning_text"]);
|
|
489
|
+
for (const field of fields) {
|
|
490
|
+
if (field === undefined)
|
|
491
|
+
continue;
|
|
492
|
+
const text = delta[field];
|
|
493
|
+
if (typeof text === "string" && text.length > 0)
|
|
494
|
+
return { field, text };
|
|
495
|
+
}
|
|
496
|
+
return undefined;
|
|
497
|
+
};
|
|
498
|
+
const detailText = (details) => {
|
|
499
|
+
const text = details.flatMap((detail) => {
|
|
500
|
+
if (!isRecord(detail))
|
|
501
|
+
return [];
|
|
502
|
+
if (detail.type === "reasoning.text" && typeof detail.text === "string" && detail.text)
|
|
503
|
+
return [detail.text];
|
|
504
|
+
if (detail.type === "reasoning.summary" && typeof detail.summary === "string" && detail.summary)
|
|
505
|
+
return [detail.summary];
|
|
506
|
+
return [];
|
|
507
|
+
});
|
|
508
|
+
if (text.length > 0)
|
|
509
|
+
return text.join("");
|
|
510
|
+
};
|
|
511
|
+
const appendReasoningDetails = (result, details) => {
|
|
512
|
+
for (const detail of details) {
|
|
513
|
+
const previous = result.at(-1);
|
|
514
|
+
if (!isRecord(previous) ||
|
|
515
|
+
previous.type !== "reasoning.text" ||
|
|
516
|
+
!isRecord(detail) ||
|
|
517
|
+
detail.type !== "reasoning.text" ||
|
|
518
|
+
conflictingReasoningTextDetails(previous, detail)) {
|
|
519
|
+
result.push(detail);
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
result[result.length - 1] = {
|
|
523
|
+
...previous,
|
|
524
|
+
...Object.fromEntries(Object.entries(detail).filter((entry) => entry[1] !== undefined)),
|
|
525
|
+
text: `${typeof previous.text === "string" ? previous.text : ""}${typeof detail.text === "string" ? detail.text : ""}`,
|
|
526
|
+
signature: mergeDetailValue(previous.signature, detail.signature),
|
|
527
|
+
format: mergeDetailValue(previous.format, detail.format),
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
const mergeDetailValue = (previous, current) => previous || current || (previous !== undefined ? previous : current);
|
|
532
|
+
const conflictingReasoningTextDetails = (previous, current) => conflictingDetailValue(previous.id, current.id) ||
|
|
533
|
+
conflictingDetailValue(previous.index, current.index) ||
|
|
534
|
+
conflictingDetailValue(previous.format, current.format) ||
|
|
535
|
+
(Boolean(previous.signature) && Boolean(current.signature) && previous.signature !== current.signature);
|
|
536
|
+
const conflictingDetailValue = (previous, current) => previous !== undefined && previous !== null && current !== undefined && current !== null && previous !== current;
|
|
537
|
+
const reasoningMetadata = (field, details) => ({
|
|
538
|
+
openai: {
|
|
539
|
+
...(field ? { reasoningField: field } : {}),
|
|
540
|
+
...(details ? { reasoningDetails: details } : {}),
|
|
541
|
+
},
|
|
542
|
+
});
|
|
543
|
+
const step = (state, event) => Effect.gen(function* () {
|
|
544
|
+
if (event.error)
|
|
545
|
+
return yield* new AIError({
|
|
546
|
+
module: ADAPTER,
|
|
547
|
+
method: "stream",
|
|
548
|
+
reason: classifyProviderFailure({
|
|
549
|
+
message: event.error.message,
|
|
550
|
+
code: event.error.code === undefined || event.error.code === null ? undefined : String(event.error.code),
|
|
551
|
+
status: typeof event.error.code === "number" ? event.error.code : undefined,
|
|
552
|
+
}),
|
|
553
|
+
});
|
|
554
|
+
const events = [];
|
|
555
|
+
const usage = mapUsage(event.usage) ?? state.usage;
|
|
556
|
+
const choice = event.choices?.[0];
|
|
557
|
+
const rawFinishReason = choice?.finish_reason;
|
|
558
|
+
const finishReason = rawFinishReason !== undefined && rawFinishReason !== null
|
|
559
|
+
? { normalized: mapFinishReason(rawFinishReason), raw: choice?.native_finish_reason ?? rawFinishReason }
|
|
560
|
+
: state.finishReason;
|
|
561
|
+
const delta = choice?.delta;
|
|
562
|
+
const toolDeltas = delta?.tool_calls ?? [];
|
|
563
|
+
let tools = state.tools;
|
|
564
|
+
let pendingTools = state.pendingTools;
|
|
565
|
+
let latestToolIndex = state.latestToolIndex;
|
|
566
|
+
let nextToolIndex = state.nextToolIndex;
|
|
567
|
+
let lifecycle = state.lifecycle;
|
|
568
|
+
const reasoning = reasoningDelta(delta, state.reasoningField);
|
|
569
|
+
const hasLateContent = Boolean(delta?.content) ||
|
|
570
|
+
reasoning !== undefined ||
|
|
571
|
+
(Array.isArray(delta?.reasoning_details) && delta.reasoning_details.length > 0) ||
|
|
572
|
+
toolDeltas.some((tool) => Boolean(tool.id) || Boolean(tool.function?.name) || Boolean(tool.function?.arguments));
|
|
573
|
+
if (state.finishReason !== undefined) {
|
|
574
|
+
if (hasLateContent)
|
|
575
|
+
return yield* ProviderShared.eventError(ADAPTER, "OpenAI Chat received content after the finish reason");
|
|
576
|
+
return [{ ...state, usage }, events];
|
|
577
|
+
}
|
|
578
|
+
const reasoningField = state.reasoningField ?? reasoning?.field;
|
|
579
|
+
const detailDelta = Array.isArray(delta?.reasoning_details) ? delta.reasoning_details : undefined;
|
|
580
|
+
if (detailDelta !== undefined)
|
|
581
|
+
appendReasoningDetails(state.reasoningDetails, detailDelta);
|
|
582
|
+
const reasoningDetailsObserved = state.reasoningDetailsObserved || detailDelta !== undefined;
|
|
583
|
+
const deltaMetadata = reasoningMetadata(reasoningField);
|
|
584
|
+
const text = detailDelta?.length ? (detailText(detailDelta) ?? reasoning?.text) : reasoning?.text;
|
|
585
|
+
if (text !== undefined)
|
|
586
|
+
lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", text, deltaMetadata);
|
|
587
|
+
else if (reasoningDetailsObserved &&
|
|
588
|
+
!lifecycle.reasoning.has("reasoning-0") &&
|
|
589
|
+
(Boolean(delta?.content) || toolDeltas.length > 0))
|
|
590
|
+
lifecycle = Lifecycle.reasoningStart(lifecycle, events, "reasoning-0", deltaMetadata);
|
|
591
|
+
const reasoningEmitted = state.reasoningEmitted || lifecycle.reasoning.has("reasoning-0");
|
|
592
|
+
if (delta?.content) {
|
|
593
|
+
lifecycle = Lifecycle.reasoningEnd(lifecycle, events, "reasoning-0", reasoningMetadata(reasoningField, reasoningDetailsObserved ? state.reasoningDetails : undefined));
|
|
594
|
+
lifecycle = Lifecycle.textDelta(lifecycle, events, "text-0", delta.content);
|
|
595
|
+
}
|
|
596
|
+
// Compatible providers may omit indexes. Prefer durable identity, then use
|
|
597
|
+
// batch position for parallel deltas or the latest call for sparse chunks.
|
|
598
|
+
for (const [position, tool] of toolDeltas.entries()) {
|
|
599
|
+
const matched = toolIndexByID(tools, pendingTools, tool.id || undefined);
|
|
600
|
+
const fallback = toolDeltas.length > 1 ? position : (latestToolIndex ?? position);
|
|
601
|
+
const fallbackTool = tools[fallback] ?? pendingTools[fallback];
|
|
602
|
+
const index = tool.index ?? matched ?? (tool.id && fallbackTool?.id && fallbackTool.id !== tool.id ? nextToolIndex : fallback);
|
|
603
|
+
const current = tools[index];
|
|
604
|
+
const pending = pendingTools[index];
|
|
605
|
+
const id = current?.id ?? pending?.id ?? (tool.id || undefined);
|
|
606
|
+
const name = current?.name ?? pending?.name ?? (tool.function?.name || undefined);
|
|
607
|
+
const text = `${pending?.input ?? ""}${tool.function?.arguments ?? ""}`;
|
|
608
|
+
latestToolIndex = index;
|
|
609
|
+
nextToolIndex = Math.max(nextToolIndex, index + 1);
|
|
610
|
+
if (!current && (!id || !name)) {
|
|
611
|
+
pendingTools = {
|
|
612
|
+
...pendingTools,
|
|
613
|
+
[index]: { id: id || undefined, name: name || undefined, input: text },
|
|
614
|
+
};
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
if (pending) {
|
|
618
|
+
pendingTools = { ...pendingTools };
|
|
619
|
+
delete pendingTools[index];
|
|
620
|
+
}
|
|
621
|
+
const result = ToolStream.appendOrStart(ADAPTER, tools, index, { id: id || undefined, name: name || undefined, text }, "OpenAI Chat tool call delta is missing id or name");
|
|
622
|
+
if (ToolStream.isError(result))
|
|
623
|
+
return yield* result;
|
|
624
|
+
tools = result.tools;
|
|
625
|
+
if (result.events.length)
|
|
626
|
+
lifecycle = Lifecycle.stepStart(lifecycle, events);
|
|
627
|
+
events.push(...result.events);
|
|
628
|
+
}
|
|
629
|
+
if (finishReason !== undefined && state.finishReason === undefined && Object.keys(pendingTools).length > 0)
|
|
630
|
+
return yield* ProviderShared.eventError(ADAPTER, "OpenAI Chat tool call delta is missing id or name");
|
|
631
|
+
// Finalize accumulated tool inputs eagerly when finish_reason arrives so
|
|
632
|
+
// valid calls and malformed local calls settle independently.
|
|
633
|
+
const finished = finishReason !== undefined && state.finishReason === undefined && Object.keys(tools).length > 0
|
|
634
|
+
? yield* ToolStream.finishAll(ADAPTER, tools)
|
|
635
|
+
: undefined;
|
|
636
|
+
return [
|
|
637
|
+
{
|
|
638
|
+
tools: finished?.tools ?? tools,
|
|
639
|
+
pendingTools,
|
|
640
|
+
toolCallEvents: finished?.events ?? state.toolCallEvents,
|
|
641
|
+
usage,
|
|
642
|
+
finishReason,
|
|
643
|
+
lifecycle,
|
|
644
|
+
reasoningField,
|
|
645
|
+
reasoningDetails: state.reasoningDetails,
|
|
646
|
+
reasoningDetailsObserved,
|
|
647
|
+
reasoningEmitted,
|
|
648
|
+
latestToolIndex,
|
|
649
|
+
nextToolIndex,
|
|
650
|
+
},
|
|
651
|
+
events,
|
|
652
|
+
];
|
|
653
|
+
});
|
|
654
|
+
const finishEvents = (state) => {
|
|
655
|
+
const events = [];
|
|
656
|
+
const toolCallEvents = state.finishReason === undefined && Object.keys(state.tools).length > 0
|
|
657
|
+
? Effect.runSync(ToolStream.finishAll(ADAPTER, state.tools)).events
|
|
658
|
+
: state.toolCallEvents;
|
|
659
|
+
const hasToolCalls = toolCallEvents.length > 0;
|
|
660
|
+
const reason = state.finishReason
|
|
661
|
+
? {
|
|
662
|
+
...state.finishReason,
|
|
663
|
+
normalized: state.finishReason.normalized === "stop" && hasToolCalls ? "tool-calls" : state.finishReason.normalized,
|
|
664
|
+
}
|
|
665
|
+
: { normalized: hasToolCalls ? "tool-calls" : "unknown" };
|
|
666
|
+
const metadata = reasoningMetadata(state.reasoningField, state.reasoningDetailsObserved ? state.reasoningDetails : undefined);
|
|
667
|
+
const started = state.reasoningDetailsObserved && !state.reasoningEmitted
|
|
668
|
+
? Lifecycle.reasoningStart(state.lifecycle, events, "reasoning-0", reasoningMetadata(state.reasoningField))
|
|
669
|
+
: state.lifecycle;
|
|
670
|
+
const ended = Lifecycle.reasoningEnd(started, events, "reasoning-0", metadata);
|
|
671
|
+
const lifecycle = toolCallEvents.length ? Lifecycle.stepStart(ended, events) : ended;
|
|
672
|
+
events.push(...toolCallEvents);
|
|
673
|
+
Lifecycle.finish(lifecycle, events, { reason, usage: state.usage });
|
|
674
|
+
return events;
|
|
675
|
+
};
|
|
676
|
+
// =============================================================================
|
|
677
|
+
// Protocol And OpenAI Route
|
|
678
|
+
// =============================================================================
|
|
679
|
+
/**
|
|
680
|
+
* The OpenAI Chat protocol — request body construction, body schema, and the
|
|
681
|
+
* streaming-event state machine. Reused by every route that speaks OpenAI Chat
|
|
682
|
+
* over HTTP+SSE: native OpenAI, DeepSeek, TogetherAI, Cerebras, Baseten,
|
|
683
|
+
* Fireworks, DeepInfra, and (once added) Azure OpenAI Chat.
|
|
684
|
+
*/
|
|
685
|
+
export const protocol = Protocol.make({
|
|
686
|
+
id: ADAPTER,
|
|
687
|
+
body: {
|
|
688
|
+
schema: OpenAIChatBody,
|
|
689
|
+
from: fromRequest,
|
|
690
|
+
},
|
|
691
|
+
stream: {
|
|
692
|
+
event: Protocol.jsonEvent(OpenAIChatEvent),
|
|
693
|
+
initial: (request) => ({
|
|
694
|
+
tools: ToolStream.empty(),
|
|
695
|
+
pendingTools: {},
|
|
696
|
+
toolCallEvents: [],
|
|
697
|
+
lifecycle: Lifecycle.initial(),
|
|
698
|
+
reasoningField: request.model.compatibility?.reasoningField,
|
|
699
|
+
reasoningDetails: [],
|
|
700
|
+
reasoningDetailsObserved: false,
|
|
701
|
+
reasoningEmitted: false,
|
|
702
|
+
nextToolIndex: 0,
|
|
703
|
+
}),
|
|
704
|
+
step,
|
|
705
|
+
onHalt: finishEvents,
|
|
706
|
+
},
|
|
707
|
+
});
|
|
708
|
+
export const httpTransport = HttpTransport.sseJson.with();
|
|
709
|
+
export const route = Route.make({
|
|
710
|
+
id: ADAPTER,
|
|
711
|
+
provider: "openai",
|
|
712
|
+
providerMetadataKey: "openai",
|
|
713
|
+
protocol,
|
|
714
|
+
endpoint: Endpoint.path(PATH, { baseURL: DEFAULT_BASE_URL }),
|
|
715
|
+
auth: Auth.none,
|
|
716
|
+
transport: httpTransport,
|
|
717
|
+
});
|
|
718
|
+
export * as OpenAIChat from "./openai-chat.js";
|