@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,208 @@
|
|
|
1
|
+
import { Effect, Encoding, Schema } from "effect";
|
|
2
|
+
import { Headers } from "effect/unstable/http";
|
|
3
|
+
import { Route } from "../route/client.js";
|
|
4
|
+
import { Auth } from "../route/auth.js";
|
|
5
|
+
import { Endpoint } from "../route/endpoint.js";
|
|
6
|
+
import { Protocol } from "../route/protocol.js";
|
|
7
|
+
import { HttpTransport } from "../route/transport/index.js";
|
|
8
|
+
import { LLMEvent, LLMRequest } from "../schema/index.js";
|
|
9
|
+
import { OpenResponses } from "./open-responses.js";
|
|
10
|
+
import { optionalArray, ProviderShared } from "./shared.js";
|
|
11
|
+
import { Lifecycle } from "./utils/lifecycle.js";
|
|
12
|
+
import { OpenAIImage } from "./utils/openai-image.js";
|
|
13
|
+
import { ToolSchemaProjection } from "./utils/tool-schema.js";
|
|
14
|
+
import { OpenResponsesChannel } from "./open-responses-channel.js";
|
|
15
|
+
import { OpenAIResponsesChannel } from "./openai-responses-channel.js";
|
|
16
|
+
const ADAPTER = "openai-responses";
|
|
17
|
+
const NAME = "OpenAI Responses";
|
|
18
|
+
const WEBSOCKET_PROTOCOL_HEADER = "responses_websockets=2026-02-06";
|
|
19
|
+
const WEBSOCKET_ROTATE_AFTER_MS = 55 * 60 * 1000;
|
|
20
|
+
export const DEFAULT_BASE_URL = "https://api.openai.com/v1";
|
|
21
|
+
export const PATH = OpenResponses.PATH;
|
|
22
|
+
const OpenAIResponsesImageGenerationTool = Schema.Struct({
|
|
23
|
+
type: Schema.tag("image_generation"),
|
|
24
|
+
action: Schema.optional(Schema.Literals(["auto", "generate", "edit"])),
|
|
25
|
+
background: Schema.optional(Schema.Literals(["auto", "opaque", "transparent"])),
|
|
26
|
+
input_fidelity: Schema.optional(Schema.Literals(["low", "high"])),
|
|
27
|
+
output_compression: Schema.optional(Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 100 }))),
|
|
28
|
+
output_format: Schema.optional(Schema.Literals(["png", "jpeg", "webp"])),
|
|
29
|
+
partial_images: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))),
|
|
30
|
+
quality: Schema.optional(Schema.Literals(["auto", "low", "medium", "high"])),
|
|
31
|
+
size: Schema.optional(OpenAIImage.Size),
|
|
32
|
+
});
|
|
33
|
+
const OpenAIResponsesTools = Schema.Union([OpenResponses.Tool, OpenAIResponsesImageGenerationTool]);
|
|
34
|
+
const OpenAIResponsesToolChoice = Schema.Union([
|
|
35
|
+
OpenResponses.ToolChoice,
|
|
36
|
+
Schema.Struct({ type: Schema.tag("image_generation") }),
|
|
37
|
+
]);
|
|
38
|
+
const OpenAIResponsesInputItem = Schema.Union([
|
|
39
|
+
Schema.Struct({
|
|
40
|
+
role: Schema.tag("assistant"),
|
|
41
|
+
content: Schema.Array(Schema.Struct({ type: Schema.tag("output_text"), text: Schema.String })),
|
|
42
|
+
phase: Schema.optionalKey(Schema.NullOr(OpenResponses.MessagePhase)),
|
|
43
|
+
}),
|
|
44
|
+
OpenResponses.InputItem,
|
|
45
|
+
]);
|
|
46
|
+
const OpenAIResponsesCoreFields = {
|
|
47
|
+
...OpenResponses.coreFields,
|
|
48
|
+
input: Schema.Array(OpenAIResponsesInputItem),
|
|
49
|
+
tools: optionalArray(OpenAIResponsesTools),
|
|
50
|
+
tool_choice: Schema.optional(OpenAIResponsesToolChoice),
|
|
51
|
+
};
|
|
52
|
+
const OpenAIResponsesBody = Schema.Struct({
|
|
53
|
+
...OpenAIResponsesCoreFields,
|
|
54
|
+
stream: Schema.Literal(true),
|
|
55
|
+
});
|
|
56
|
+
const extension = {
|
|
57
|
+
id: ADAPTER,
|
|
58
|
+
name: NAME,
|
|
59
|
+
messagePhase: (value) => (value === null ? null : undefined),
|
|
60
|
+
lowerMedia: ({ part, media, request }) => {
|
|
61
|
+
if (request.model.provider !== "xai" || media.mime !== "application/pdf")
|
|
62
|
+
return undefined;
|
|
63
|
+
return {
|
|
64
|
+
type: "input_file",
|
|
65
|
+
filename: part.filename ?? "document.pdf",
|
|
66
|
+
file_data: media.base64,
|
|
67
|
+
mime_type: media.mime,
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
const nativeImageToolInput = (tool) => {
|
|
72
|
+
const native = tool.native?.openai;
|
|
73
|
+
return ProviderShared.isRecord(native) && native.type === "image_generation" ? native : undefined;
|
|
74
|
+
};
|
|
75
|
+
const nativeImageTool = (tool) => {
|
|
76
|
+
const native = nativeImageToolInput(tool);
|
|
77
|
+
return Schema.is(OpenAIResponsesImageGenerationTool)(native) ? native : undefined;
|
|
78
|
+
};
|
|
79
|
+
const lowerTool = Effect.fn("OpenAIResponses.lowerTool")(function* (tool, inputSchema) {
|
|
80
|
+
const native = nativeImageToolInput(tool);
|
|
81
|
+
if (native !== undefined) {
|
|
82
|
+
if (Schema.is(OpenAIResponsesImageGenerationTool)(native))
|
|
83
|
+
return native;
|
|
84
|
+
return yield* ProviderShared.invalidRequest("OpenAI Responses image generation tool options are invalid");
|
|
85
|
+
}
|
|
86
|
+
return yield* OpenResponses.lowerTool(NAME, tool, inputSchema);
|
|
87
|
+
});
|
|
88
|
+
const lowerToolChoice = (toolChoice, tools) => ProviderShared.matchToolChoice(NAME, toolChoice, {
|
|
89
|
+
auto: () => "auto",
|
|
90
|
+
none: () => "none",
|
|
91
|
+
required: () => "required",
|
|
92
|
+
tool: (name) => tools.some((tool) => tool.name === name && nativeImageTool(tool) !== undefined)
|
|
93
|
+
? { type: "image_generation" }
|
|
94
|
+
: { type: "function", name },
|
|
95
|
+
});
|
|
96
|
+
const fromRequest = Effect.fn("OpenAIResponses.fromRequest")(function* (request) {
|
|
97
|
+
const body = yield* OpenResponses.fromRequestWithExtension(LLMRequest.update(request, { tools: [], toolChoice: undefined }), extension);
|
|
98
|
+
const toolSchemaCompatibility = request.model.compatibility?.toolSchema;
|
|
99
|
+
return {
|
|
100
|
+
...body,
|
|
101
|
+
tools: request.tools.length === 0
|
|
102
|
+
? undefined
|
|
103
|
+
: yield* Effect.forEach(request.tools, (tool) => lowerTool(tool, ToolSchemaProjection.modelCompatibility(tool.inputSchema, toolSchemaCompatibility))),
|
|
104
|
+
tool_choice: request.toolChoice ? yield* lowerToolChoice(request.toolChoice, request.tools) : undefined,
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
const HOSTED_TOOLS = {
|
|
108
|
+
web_search_call: { name: "web_search", input: (item) => item.action ?? {} },
|
|
109
|
+
web_search_preview_call: { name: "web_search_preview", input: (item) => item.action ?? {} },
|
|
110
|
+
file_search_call: { name: "file_search", input: (item) => ({ queries: item.queries ?? [] }) },
|
|
111
|
+
code_interpreter_call: {
|
|
112
|
+
name: "code_interpreter",
|
|
113
|
+
input: (item) => ({ code: item.code, container_id: item.container_id }),
|
|
114
|
+
},
|
|
115
|
+
computer_use_call: { name: "computer_use", input: (item) => item.action ?? {} },
|
|
116
|
+
image_generation_call: { name: "image_generation", input: () => ({}) },
|
|
117
|
+
mcp_call: {
|
|
118
|
+
name: "mcp",
|
|
119
|
+
input: (item) => ({ server_label: item.server_label, name: item.name, arguments: item.arguments }),
|
|
120
|
+
},
|
|
121
|
+
local_shell_call: { name: "local_shell", input: (item) => item.action ?? {} },
|
|
122
|
+
};
|
|
123
|
+
const isHostedToolItem = (item) => item.type in HOSTED_TOOLS && typeof item.id === "string" && item.id.length > 0;
|
|
124
|
+
const hostedToolResult = Effect.fn("OpenAIResponses.hostedToolResult")(function* (item) {
|
|
125
|
+
const isError = item.error !== undefined && item.error !== null;
|
|
126
|
+
if (item.type === "image_generation_call" && item.result) {
|
|
127
|
+
yield* Effect.fromResult(Encoding.decodeBase64(item.result)).pipe(Effect.mapError(() => ProviderShared.eventError(ADAPTER, "OpenAI Responses returned invalid image base64")));
|
|
128
|
+
const format = item.output_format ?? "png";
|
|
129
|
+
return {
|
|
130
|
+
type: "content",
|
|
131
|
+
value: [
|
|
132
|
+
{
|
|
133
|
+
type: "file",
|
|
134
|
+
uri: `data:image/${format};base64,${item.result}`,
|
|
135
|
+
mime: `image/${format}`,
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return isError ? { type: "error", value: item.error } : { type: "json", value: item };
|
|
141
|
+
});
|
|
142
|
+
const onHostedToolDone = Effect.fn("OpenAIResponses.onHostedToolDone")(function* (state, item) {
|
|
143
|
+
const tool = HOSTED_TOOLS[item.type];
|
|
144
|
+
const providerMetadata = OpenResponses.providerMetadata(state, { itemId: item.id });
|
|
145
|
+
const events = [];
|
|
146
|
+
const lifecycle = Lifecycle.stepStart(state.lifecycle, events);
|
|
147
|
+
events.push(LLMEvent.toolCall({
|
|
148
|
+
id: item.id,
|
|
149
|
+
name: tool.name,
|
|
150
|
+
input: tool.input(item),
|
|
151
|
+
providerExecuted: true,
|
|
152
|
+
providerMetadata,
|
|
153
|
+
}), LLMEvent.toolResult({
|
|
154
|
+
id: item.id,
|
|
155
|
+
name: tool.name,
|
|
156
|
+
result: yield* hostedToolResult(item),
|
|
157
|
+
providerExecuted: true,
|
|
158
|
+
providerMetadata,
|
|
159
|
+
}));
|
|
160
|
+
return [{ ...state, lifecycle }, events];
|
|
161
|
+
});
|
|
162
|
+
const step = (state, event) => {
|
|
163
|
+
if (event.type === "response.reasoning_text.delta" || event.type === "response.reasoning_summary.delta")
|
|
164
|
+
return event.item_id
|
|
165
|
+
? Effect.succeed(OpenResponses.onReasoningDelta(state, event, event.item_id))
|
|
166
|
+
: ProviderShared.eventError(ADAPTER, `${event.type} is missing item_id`);
|
|
167
|
+
if (event.type === "response.reasoning_text.done" || event.type === "response.reasoning_summary.done")
|
|
168
|
+
return event.item_id
|
|
169
|
+
? Effect.succeed(OpenResponses.onReasoningDone(state, event))
|
|
170
|
+
: ProviderShared.eventError(ADAPTER, `${event.type} is missing item_id`);
|
|
171
|
+
if (event.type === "response.output_item.done" && event.item && isHostedToolItem(event.item))
|
|
172
|
+
return onHostedToolDone(state, event.item);
|
|
173
|
+
return OpenResponses.step(state, event);
|
|
174
|
+
};
|
|
175
|
+
export const protocol = Protocol.make({
|
|
176
|
+
id: ADAPTER,
|
|
177
|
+
body: {
|
|
178
|
+
schema: OpenAIResponsesBody,
|
|
179
|
+
from: fromRequest,
|
|
180
|
+
},
|
|
181
|
+
stream: {
|
|
182
|
+
event: OpenResponses.protocol.stream.event,
|
|
183
|
+
initial: (request) => OpenResponses.initial(request, extension),
|
|
184
|
+
step,
|
|
185
|
+
terminal: OpenResponses.terminal,
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
const endpoint = Endpoint.path(PATH, { baseURL: DEFAULT_BASE_URL });
|
|
189
|
+
const auth = Auth.none;
|
|
190
|
+
export const httpTransport = HttpTransport.sseJson.with();
|
|
191
|
+
export const transport = OpenResponsesChannel.transport({
|
|
192
|
+
id: ADAPTER,
|
|
193
|
+
name: NAME,
|
|
194
|
+
rotateAfterMs: WEBSOCKET_ROTATE_AFTER_MS,
|
|
195
|
+
headers: (headers) => Headers.set(headers, "openai-beta", headers["openai-beta"] ?? WEBSOCKET_PROTOCOL_HEADER),
|
|
196
|
+
driver: (input) => OpenAIResponsesChannel.driver({ id: ADAPTER, name: NAME, ...input }),
|
|
197
|
+
});
|
|
198
|
+
export const route = Route.make({
|
|
199
|
+
id: ADAPTER,
|
|
200
|
+
provider: "openai",
|
|
201
|
+
providerMetadataKey: "openai",
|
|
202
|
+
protocol,
|
|
203
|
+
endpoint,
|
|
204
|
+
auth,
|
|
205
|
+
transport,
|
|
206
|
+
defaults: { providerOptions: { openai: { store: false } } },
|
|
207
|
+
});
|
|
208
|
+
export * as OpenAIResponses from "./openai-responses.js";
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { Tool } from "@opencode-ai/schema/tool";
|
|
3
|
+
import { Effect, Schema, Stream } from "effect";
|
|
4
|
+
import { Headers, HttpClientRequest } from "effect/unstable/http";
|
|
5
|
+
import { AIError, type ContentPart, type LLMRequest, type ToolResultPart } from "../schema/index.js";
|
|
6
|
+
import { isRecord } from "../utils/record.js";
|
|
7
|
+
export { isRecord };
|
|
8
|
+
export declare const Json: Schema.fromJsonString<Schema.Unknown>;
|
|
9
|
+
export declare const decodeJson: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => unknown;
|
|
10
|
+
export declare const encodeJson: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => string;
|
|
11
|
+
export declare const JsonObject: Schema.$Record<Schema.String, Schema.Unknown>;
|
|
12
|
+
export declare const optionalArray: <const S extends Schema.Top>(schema: S) => Schema.optional<Schema.$Array<S>>;
|
|
13
|
+
export declare const optionalNull: <const S extends Schema.Top>(schema: S) => Schema.optional<Schema.NullOr<S>>;
|
|
14
|
+
/**
|
|
15
|
+
* Streaming tool-call accumulator. Adapters that build a tool call across
|
|
16
|
+
* multiple `tool-input-delta` chunks store the partial JSON input string here
|
|
17
|
+
* and finalize it with `parseToolInput` once the call completes.
|
|
18
|
+
*/
|
|
19
|
+
export interface ToolAccumulator {
|
|
20
|
+
readonly id: string;
|
|
21
|
+
readonly name: string;
|
|
22
|
+
readonly input: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* `Usage.totalTokens` policy shared by every route. Honors a provider-
|
|
26
|
+
* supplied total; otherwise falls back to `inputTokens + outputTokens` only
|
|
27
|
+
* when at least one is defined. Returns `undefined` when neither input nor
|
|
28
|
+
* output is known so routes don't publish a misleading `0`.
|
|
29
|
+
*
|
|
30
|
+
* Under the additive `AI.Usage` contract, `inputTokens` and `outputTokens`
|
|
31
|
+
* are the non-cached input and visible output only. The provider-supplied
|
|
32
|
+
* `total` is the source of truth when present; the computed fallback
|
|
33
|
+
* under-counts cache and reasoning by design and exists mainly so
|
|
34
|
+
* Anthropic-style providers (which don't surface a total) still get a
|
|
35
|
+
* sensible aggregate on the input + output axes.
|
|
36
|
+
*/
|
|
37
|
+
export declare const totalTokens: (inputTokens: number | undefined, outputTokens: number | undefined, total: number | undefined) => number | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* Subtract `subtrahend` from `total`, clamping to zero if the provider
|
|
40
|
+
* reports a non-sensical breakdown (e.g. `cached_tokens > prompt_tokens`).
|
|
41
|
+
* Used by protocol mappers when deriving a non-overlapping breakdown field
|
|
42
|
+
* from a provider's inclusive total — `nonCachedInputTokens` from
|
|
43
|
+
* `inputTokens - cacheReadInputTokens - cacheWriteInputTokens`.
|
|
44
|
+
*
|
|
45
|
+
* If `total` is `undefined`, returns `undefined` (we don't fabricate
|
|
46
|
+
* counts). If `subtrahend` is `undefined`, returns `total` unchanged. The
|
|
47
|
+
* provider-native breakdown stays available on `Usage.native` for debugging.
|
|
48
|
+
*/
|
|
49
|
+
export declare const subtractTokens: (total: number | undefined, subtrahend: number | undefined) => number | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* Sum a list of optional token counts, returning `undefined` only when
|
|
52
|
+
* every value is `undefined` (so we don't fabricate a `0`). Used by
|
|
53
|
+
* protocol mappers to derive the inclusive `inputTokens` total from a
|
|
54
|
+
* provider that natively reports a non-overlapping breakdown
|
|
55
|
+
* (e.g. Anthropic, whose `input_tokens` is already non-cached only).
|
|
56
|
+
*/
|
|
57
|
+
export declare const sumTokens: (...values: ReadonlyArray<number | undefined>) => number | undefined;
|
|
58
|
+
export declare const eventError: (route: string, message: string, raw?: string) => AIError;
|
|
59
|
+
export declare const parseJson: (route: string, input: string, message: string) => Effect.Effect<unknown, AIError, never>;
|
|
60
|
+
/**
|
|
61
|
+
* Join the `text` field of a list of parts with newlines. Used by routes
|
|
62
|
+
* that flatten system / message content arrays into a single provider string
|
|
63
|
+
* (OpenAI Chat `system` content, OpenAI Responses `system` content, Gemini
|
|
64
|
+
* `systemInstruction.parts[].text`).
|
|
65
|
+
*/
|
|
66
|
+
export declare const joinText: (parts: ReadonlyArray<{
|
|
67
|
+
readonly text: string;
|
|
68
|
+
}>) => string;
|
|
69
|
+
/**
|
|
70
|
+
* Stable fallback representation for chronological `Message.system(...)`
|
|
71
|
+
* updates on routes that do not support that privileged role natively. The
|
|
72
|
+
* wrapper remains visibly lower-authority user text, preserves the original
|
|
73
|
+
* temporal position, and XML-escapes content so it cannot close the wrapper.
|
|
74
|
+
*/
|
|
75
|
+
export declare const wrapSystemUpdate: (parts: ReadonlyArray<{
|
|
76
|
+
readonly text: string;
|
|
77
|
+
}>) => string;
|
|
78
|
+
/**
|
|
79
|
+
* Chronological system updates deliberately accept text only. Do not insert
|
|
80
|
+
* raw retrieved, tool, or web content into privileged updates: keep untrusted
|
|
81
|
+
* data in ordinary user/tool messages instead.
|
|
82
|
+
*/
|
|
83
|
+
export declare const systemUpdateText: (route: string, message: import("../schema/messages.js").Message) => Effect.Effect<{
|
|
84
|
+
readonly type: "text";
|
|
85
|
+
readonly text: string;
|
|
86
|
+
readonly metadata?: {
|
|
87
|
+
readonly [x: string]: unknown;
|
|
88
|
+
} | undefined;
|
|
89
|
+
readonly cache?: import("../schema/options.js").CacheHint | undefined;
|
|
90
|
+
readonly providerMetadata?: {
|
|
91
|
+
readonly [x: string]: {
|
|
92
|
+
readonly [x: string]: unknown;
|
|
93
|
+
};
|
|
94
|
+
} | undefined;
|
|
95
|
+
}[], AIError, never>;
|
|
96
|
+
/** Lower an unsupported privileged update into visible, in-order user text. */
|
|
97
|
+
export declare const wrappedSystemUpdate: (route: string, message: import("../schema/messages.js").Message) => Effect.Effect<{
|
|
98
|
+
type: "text";
|
|
99
|
+
text: string;
|
|
100
|
+
cache: import("../schema/options.js").CacheHint | undefined;
|
|
101
|
+
}, AIError, never>;
|
|
102
|
+
/**
|
|
103
|
+
* Parse the streamed JSON input of a tool call. Treats an empty string as
|
|
104
|
+
* `"{}"` — providers occasionally finish a tool call without ever emitting
|
|
105
|
+
* input deltas (e.g. zero-arg tools). The error message is uniform across
|
|
106
|
+
* routes: `Invalid JSON input for <route> tool call <name>`.
|
|
107
|
+
*/
|
|
108
|
+
export declare const parseToolInput: (route: string, name: string, raw: string) => Effect.Effect<unknown, AIError, never>;
|
|
109
|
+
export declare const IMAGE_MIMES: readonly ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
110
|
+
export declare const VIDEO_MIMES: readonly ["video/mp4", "video/webm", "video/quicktime"];
|
|
111
|
+
export declare const AUDIO_MIMES: readonly ["audio/wav", "audio/mp3", "audio/aiff", "audio/aac", "audio/ogg", "audio/flac"];
|
|
112
|
+
export declare const PDF_MIMES: readonly ["application/pdf"];
|
|
113
|
+
export declare const MEDIA_MIMES: readonly ["image/png", "image/jpeg", "image/gif", "image/webp", "video/mp4", "video/webm", "video/quicktime", "audio/wav", "audio/mp3", "audio/aiff", "audio/aac", "audio/ogg", "audio/flac", "application/pdf"];
|
|
114
|
+
export declare const MAX_MEDIA_ENCODED_BYTES: number;
|
|
115
|
+
export declare const MAX_MEDIA_DECODED_BYTES: number;
|
|
116
|
+
export interface ValidatedMedia {
|
|
117
|
+
readonly mime: string;
|
|
118
|
+
readonly base64: string;
|
|
119
|
+
readonly dataUrl: string;
|
|
120
|
+
readonly bytes: Uint8Array;
|
|
121
|
+
}
|
|
122
|
+
export declare const validateMedia: (route: string, part: {
|
|
123
|
+
readonly data: string | Uint8Array<ArrayBufferLike>;
|
|
124
|
+
readonly type: "media";
|
|
125
|
+
readonly mediaType: string;
|
|
126
|
+
readonly metadata?: {
|
|
127
|
+
readonly [x: string]: unknown;
|
|
128
|
+
} | undefined;
|
|
129
|
+
readonly filename?: string | undefined;
|
|
130
|
+
}, supportedMimes: ReadonlySet<string>) => Effect.Effect<{
|
|
131
|
+
mime: string;
|
|
132
|
+
base64: string;
|
|
133
|
+
dataUrl: string;
|
|
134
|
+
bytes: Buffer<ArrayBuffer>;
|
|
135
|
+
}, AIError, never>;
|
|
136
|
+
export declare const validateToolFile: (route: string, part: Tool.FileContent, supportedMimes: ReadonlySet<string>) => Effect.Effect<{
|
|
137
|
+
mime: string;
|
|
138
|
+
base64: string;
|
|
139
|
+
dataUrl: string;
|
|
140
|
+
bytes: Buffer<ArrayBuffer>;
|
|
141
|
+
}, AIError, never>;
|
|
142
|
+
export declare const trimBaseUrl: (value: string) => string;
|
|
143
|
+
export declare const toolResultText: (part: ToolResultPart) => string;
|
|
144
|
+
export declare const errorText: (error: unknown) => string;
|
|
145
|
+
/**
|
|
146
|
+
* `framing` step for Server-Sent Events. Decodes UTF-8, runs the SSE channel
|
|
147
|
+
* decoder, and drops empty / `[DONE]` keep-alive events so the downstream
|
|
148
|
+
* `decodeChunk` sees one JSON string per element. The SSE channel emits a
|
|
149
|
+
* `Retry` control event on its error channel; we drop it here (we don't
|
|
150
|
+
* implement client-driven retries) so the public error channel stays
|
|
151
|
+
* `AIError`.
|
|
152
|
+
*/
|
|
153
|
+
export declare const sseFraming: (bytes: Stream.Stream<Uint8Array, AIError>) => Stream.Stream<string, AIError>;
|
|
154
|
+
/**
|
|
155
|
+
* Canonical invalid-request constructor. Lift one-line `const invalid =
|
|
156
|
+
* (message) => invalidRequest(message)` aliases out of every
|
|
157
|
+
* route so the error constructor lives in one place. If we ever extend
|
|
158
|
+
* `InvalidRequestReason` with route context or trace metadata, the change
|
|
159
|
+
* lands here.
|
|
160
|
+
*/
|
|
161
|
+
export declare const invalidRequest: (message: string) => AIError;
|
|
162
|
+
export declare const matchToolChoice: <Auto, None, Required, Tool>(route: string, toolChoice: NonNullable<LLMRequest["toolChoice"]>, cases: {
|
|
163
|
+
readonly auto: () => Auto;
|
|
164
|
+
readonly none: () => None;
|
|
165
|
+
readonly required: () => Required;
|
|
166
|
+
readonly tool: (name: string) => Tool;
|
|
167
|
+
}) => Effect.Effect<Auto | None | Required | Tool, AIError, never>;
|
|
168
|
+
type ContentType = ContentPart["type"];
|
|
169
|
+
export declare const supportsContent: <const Type extends ContentType>(part: ContentPart, types: ReadonlyArray<Type>) => part is Extract<ContentPart, {
|
|
170
|
+
readonly type: Type;
|
|
171
|
+
}>;
|
|
172
|
+
export declare const unsupportedContent: (route: string, role: LLMRequest["messages"][number]["role"], types: ReadonlyArray<ContentType>) => AIError;
|
|
173
|
+
/**
|
|
174
|
+
* Build a `validate` step from a Schema decoder. Replaces the per-route
|
|
175
|
+
* lambda body `(payload) => decode(payload).pipe(Effect.mapError((e) =>
|
|
176
|
+
* invalid(e.message)))`. Any decode error is translated into
|
|
177
|
+
* `AIError` carrying the original parse-error message.
|
|
178
|
+
*/
|
|
179
|
+
export declare const validateWith: <A, I, E extends {
|
|
180
|
+
readonly message: string;
|
|
181
|
+
}>(decode: (input: I) => Effect.Effect<A, E>) => (payload: I) => Effect.Effect<A, AIError, never>;
|
|
182
|
+
/**
|
|
183
|
+
* Build an HTTP POST with a JSON body. Sets `content-type: application/json`
|
|
184
|
+
* automatically after caller-supplied headers so routes cannot accidentally
|
|
185
|
+
* send JSON with a stale content type. The body is passed pre-encoded so
|
|
186
|
+
* routes can choose between
|
|
187
|
+
* `Schema.encodeSync(payload)` and `ProviderShared.encodeJson(payload)`.
|
|
188
|
+
*/
|
|
189
|
+
export declare const jsonPost: (input: {
|
|
190
|
+
readonly url: string;
|
|
191
|
+
readonly body: string;
|
|
192
|
+
readonly headers?: Headers.Input;
|
|
193
|
+
}) => HttpClientRequest.HttpClientRequest;
|
|
194
|
+
export * as ProviderShared from "./shared.js";
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { Tool } from "@opencode-ai/schema/tool";
|
|
3
|
+
import { Effect, Schema, Stream } from "effect";
|
|
4
|
+
import * as Sse from "effect/unstable/encoding/Sse";
|
|
5
|
+
import { Headers, HttpClientRequest } from "effect/unstable/http";
|
|
6
|
+
import { InvalidProviderOutputReason, InvalidRequestReason, AIError, } from "../schema/index.js";
|
|
7
|
+
import { isRecord } from "../utils/record.js";
|
|
8
|
+
export { isRecord };
|
|
9
|
+
export const Json = Schema.fromJsonString(Schema.Unknown);
|
|
10
|
+
export const decodeJson = Schema.decodeUnknownSync(Json);
|
|
11
|
+
export const encodeJson = Schema.encodeSync(Json);
|
|
12
|
+
const isJson = Schema.is(Schema.Json);
|
|
13
|
+
export const JsonObject = Schema.Record(Schema.String, Schema.Unknown);
|
|
14
|
+
export const optionalArray = (schema) => Schema.optional(Schema.Array(schema));
|
|
15
|
+
export const optionalNull = (schema) => Schema.optional(Schema.NullOr(schema));
|
|
16
|
+
/**
|
|
17
|
+
* `Usage.totalTokens` policy shared by every route. Honors a provider-
|
|
18
|
+
* supplied total; otherwise falls back to `inputTokens + outputTokens` only
|
|
19
|
+
* when at least one is defined. Returns `undefined` when neither input nor
|
|
20
|
+
* output is known so routes don't publish a misleading `0`.
|
|
21
|
+
*
|
|
22
|
+
* Under the additive `AI.Usage` contract, `inputTokens` and `outputTokens`
|
|
23
|
+
* are the non-cached input and visible output only. The provider-supplied
|
|
24
|
+
* `total` is the source of truth when present; the computed fallback
|
|
25
|
+
* under-counts cache and reasoning by design and exists mainly so
|
|
26
|
+
* Anthropic-style providers (which don't surface a total) still get a
|
|
27
|
+
* sensible aggregate on the input + output axes.
|
|
28
|
+
*/
|
|
29
|
+
export const totalTokens = (inputTokens, outputTokens, total) => {
|
|
30
|
+
if (total !== undefined)
|
|
31
|
+
return total;
|
|
32
|
+
if (inputTokens === undefined && outputTokens === undefined)
|
|
33
|
+
return undefined;
|
|
34
|
+
return (inputTokens ?? 0) + (outputTokens ?? 0);
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Subtract `subtrahend` from `total`, clamping to zero if the provider
|
|
38
|
+
* reports a non-sensical breakdown (e.g. `cached_tokens > prompt_tokens`).
|
|
39
|
+
* Used by protocol mappers when deriving a non-overlapping breakdown field
|
|
40
|
+
* from a provider's inclusive total — `nonCachedInputTokens` from
|
|
41
|
+
* `inputTokens - cacheReadInputTokens - cacheWriteInputTokens`.
|
|
42
|
+
*
|
|
43
|
+
* If `total` is `undefined`, returns `undefined` (we don't fabricate
|
|
44
|
+
* counts). If `subtrahend` is `undefined`, returns `total` unchanged. The
|
|
45
|
+
* provider-native breakdown stays available on `Usage.native` for debugging.
|
|
46
|
+
*/
|
|
47
|
+
export const subtractTokens = (total, subtrahend) => {
|
|
48
|
+
if (total === undefined)
|
|
49
|
+
return undefined;
|
|
50
|
+
if (subtrahend === undefined)
|
|
51
|
+
return total;
|
|
52
|
+
return Math.max(0, total - subtrahend);
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Sum a list of optional token counts, returning `undefined` only when
|
|
56
|
+
* every value is `undefined` (so we don't fabricate a `0`). Used by
|
|
57
|
+
* protocol mappers to derive the inclusive `inputTokens` total from a
|
|
58
|
+
* provider that natively reports a non-overlapping breakdown
|
|
59
|
+
* (e.g. Anthropic, whose `input_tokens` is already non-cached only).
|
|
60
|
+
*/
|
|
61
|
+
export const sumTokens = (...values) => {
|
|
62
|
+
if (values.every((value) => value === undefined))
|
|
63
|
+
return undefined;
|
|
64
|
+
return values.reduce((acc, value) => acc + (value ?? 0), 0);
|
|
65
|
+
};
|
|
66
|
+
export const eventError = (route, message, raw) => new AIError({
|
|
67
|
+
module: "ProviderShared",
|
|
68
|
+
method: "stream",
|
|
69
|
+
reason: new InvalidProviderOutputReason({ route, message, raw }),
|
|
70
|
+
});
|
|
71
|
+
export const parseJson = (route, input, message) => Effect.try({
|
|
72
|
+
try: () => decodeJson(input),
|
|
73
|
+
catch: () => eventError(route, message, input),
|
|
74
|
+
});
|
|
75
|
+
/**
|
|
76
|
+
* Join the `text` field of a list of parts with newlines. Used by routes
|
|
77
|
+
* that flatten system / message content arrays into a single provider string
|
|
78
|
+
* (OpenAI Chat `system` content, OpenAI Responses `system` content, Gemini
|
|
79
|
+
* `systemInstruction.parts[].text`).
|
|
80
|
+
*/
|
|
81
|
+
export const joinText = (parts) => parts.map((part) => part.text).join("\n");
|
|
82
|
+
const escapeSystemUpdateText = (text) => text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
83
|
+
/**
|
|
84
|
+
* Stable fallback representation for chronological `Message.system(...)`
|
|
85
|
+
* updates on routes that do not support that privileged role natively. The
|
|
86
|
+
* wrapper remains visibly lower-authority user text, preserves the original
|
|
87
|
+
* temporal position, and XML-escapes content so it cannot close the wrapper.
|
|
88
|
+
*/
|
|
89
|
+
export const wrapSystemUpdate = (parts) => `<system-update>\n${escapeSystemUpdateText(joinText(parts))}\n</system-update>`;
|
|
90
|
+
/**
|
|
91
|
+
* Chronological system updates deliberately accept text only. Do not insert
|
|
92
|
+
* raw retrieved, tool, or web content into privileged updates: keep untrusted
|
|
93
|
+
* data in ordinary user/tool messages instead.
|
|
94
|
+
*/
|
|
95
|
+
export const systemUpdateText = Effect.fn("ProviderShared.systemUpdateText")(function* (route, message) {
|
|
96
|
+
const content = [];
|
|
97
|
+
for (const part of message.content) {
|
|
98
|
+
if (!supportsContent(part, ["text"]))
|
|
99
|
+
return yield* unsupportedContent(route, "system", ["text"]);
|
|
100
|
+
content.push(part);
|
|
101
|
+
}
|
|
102
|
+
return content;
|
|
103
|
+
});
|
|
104
|
+
/** Lower an unsupported privileged update into visible, in-order user text. */
|
|
105
|
+
export const wrappedSystemUpdate = Effect.fn("ProviderShared.wrappedSystemUpdate")(function* (route, message) {
|
|
106
|
+
const content = yield* systemUpdateText(route, message);
|
|
107
|
+
return { type: "text", text: wrapSystemUpdate(content), cache: content.at(-1)?.cache };
|
|
108
|
+
});
|
|
109
|
+
/**
|
|
110
|
+
* Parse the streamed JSON input of a tool call. Treats an empty string as
|
|
111
|
+
* `"{}"` — providers occasionally finish a tool call without ever emitting
|
|
112
|
+
* input deltas (e.g. zero-arg tools). The error message is uniform across
|
|
113
|
+
* routes: `Invalid JSON input for <route> tool call <name>`.
|
|
114
|
+
*/
|
|
115
|
+
export const parseToolInput = (route, name, raw) => parseJson(route, raw || "{}", `Invalid JSON input for ${route} tool call ${name}`);
|
|
116
|
+
export const IMAGE_MIMES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
117
|
+
export const VIDEO_MIMES = ["video/mp4", "video/webm", "video/quicktime"];
|
|
118
|
+
export const AUDIO_MIMES = ["audio/wav", "audio/mp3", "audio/aiff", "audio/aac", "audio/ogg", "audio/flac"];
|
|
119
|
+
export const PDF_MIMES = ["application/pdf"];
|
|
120
|
+
export const MEDIA_MIMES = [...IMAGE_MIMES, ...VIDEO_MIMES, ...AUDIO_MIMES, ...PDF_MIMES];
|
|
121
|
+
export const MAX_MEDIA_ENCODED_BYTES = 28 * 1024 * 1024;
|
|
122
|
+
export const MAX_MEDIA_DECODED_BYTES = 20 * 1024 * 1024;
|
|
123
|
+
const base64Pattern = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
124
|
+
export const validateMedia = Effect.fn("ProviderShared.validateMedia")(function* (route, part, supportedMimes) {
|
|
125
|
+
const mime = part.mediaType.toLowerCase();
|
|
126
|
+
if (!supportedMimes.has(mime))
|
|
127
|
+
return yield* invalidRequest(`${route} does not support media type ${part.mediaType}`);
|
|
128
|
+
let base64;
|
|
129
|
+
if (typeof part.data !== "string") {
|
|
130
|
+
if (part.data.byteLength > MAX_MEDIA_DECODED_BYTES)
|
|
131
|
+
return yield* invalidRequest(`${route} media exceeds the ${MAX_MEDIA_DECODED_BYTES} byte decoded limit`);
|
|
132
|
+
base64 = Buffer.from(part.data).toString("base64");
|
|
133
|
+
}
|
|
134
|
+
else if (part.data.startsWith("data:")) {
|
|
135
|
+
const match = /^data:([^;,]+);base64,([A-Za-z0-9+/]*={0,2})$/s.exec(part.data);
|
|
136
|
+
if (!match)
|
|
137
|
+
return yield* invalidRequest(`${route} media data URL must contain valid base64`);
|
|
138
|
+
if (match[1].toLowerCase() !== mime)
|
|
139
|
+
return yield* invalidRequest(`${route} media type ${part.mediaType} does not match data URL type ${match[1]}`);
|
|
140
|
+
base64 = match[2];
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
base64 = part.data;
|
|
144
|
+
}
|
|
145
|
+
if (Buffer.byteLength(base64, "utf8") > MAX_MEDIA_ENCODED_BYTES)
|
|
146
|
+
return yield* invalidRequest(`${route} media exceeds the ${MAX_MEDIA_ENCODED_BYTES} byte encoded limit`);
|
|
147
|
+
if (!base64 || base64.length % 4 !== 0 || !base64Pattern.test(base64))
|
|
148
|
+
return yield* invalidRequest(`${route} media must contain valid base64`);
|
|
149
|
+
const bytes = Buffer.from(base64, "base64");
|
|
150
|
+
if (bytes.byteLength > MAX_MEDIA_DECODED_BYTES)
|
|
151
|
+
return yield* invalidRequest(`${route} media exceeds the ${MAX_MEDIA_DECODED_BYTES} byte decoded limit`);
|
|
152
|
+
if (bytes.toString("base64") !== base64)
|
|
153
|
+
return yield* invalidRequest(`${route} media must contain canonical base64`);
|
|
154
|
+
return { mime, base64, dataUrl: `data:${mime};base64,${base64}`, bytes };
|
|
155
|
+
});
|
|
156
|
+
export const validateToolFile = (route, part, supportedMimes) => validateMedia(route, { type: "media", mediaType: part.mime, data: part.uri, filename: part.name }, supportedMimes);
|
|
157
|
+
export const trimBaseUrl = (value) => value.replace(/\/+$/, "");
|
|
158
|
+
export const toolResultText = (part) => {
|
|
159
|
+
if (part.result.type === "text")
|
|
160
|
+
return String(part.result.value);
|
|
161
|
+
if (part.result.type === "error") {
|
|
162
|
+
const value = part.result.value;
|
|
163
|
+
const prototype = typeof value === "object" && value !== null && !Array.isArray(value) && Object.getPrototypeOf(value);
|
|
164
|
+
const structured = Array.isArray(value) || prototype === Object.prototype || prototype === null;
|
|
165
|
+
return structured && isJson(value) ? encodeJson(value) : String(value);
|
|
166
|
+
}
|
|
167
|
+
return encodeJson(part.result.value);
|
|
168
|
+
};
|
|
169
|
+
export const errorText = (error) => {
|
|
170
|
+
if (error instanceof Error)
|
|
171
|
+
return error.message;
|
|
172
|
+
if (typeof error === "string")
|
|
173
|
+
return error;
|
|
174
|
+
if (typeof error === "number" || typeof error === "boolean" || typeof error === "bigint")
|
|
175
|
+
return String(error);
|
|
176
|
+
if (error === null)
|
|
177
|
+
return "null";
|
|
178
|
+
if (error === undefined)
|
|
179
|
+
return "undefined";
|
|
180
|
+
return "Unknown stream error";
|
|
181
|
+
};
|
|
182
|
+
/**
|
|
183
|
+
* `framing` step for Server-Sent Events. Decodes UTF-8, runs the SSE channel
|
|
184
|
+
* decoder, and drops empty / `[DONE]` keep-alive events so the downstream
|
|
185
|
+
* `decodeChunk` sees one JSON string per element. The SSE channel emits a
|
|
186
|
+
* `Retry` control event on its error channel; we drop it here (we don't
|
|
187
|
+
* implement client-driven retries) so the public error channel stays
|
|
188
|
+
* `AIError`.
|
|
189
|
+
*/
|
|
190
|
+
export const sseFraming = (bytes) => bytes.pipe(Stream.decodeText(), Stream.pipeThroughChannel(Sse.decode()), Stream.catchTag("Retry", () => Stream.empty), Stream.filter((event) => event.data.length > 0 && event.data !== "[DONE]"), Stream.map((event) => event.data));
|
|
191
|
+
/**
|
|
192
|
+
* Canonical invalid-request constructor. Lift one-line `const invalid =
|
|
193
|
+
* (message) => invalidRequest(message)` aliases out of every
|
|
194
|
+
* route so the error constructor lives in one place. If we ever extend
|
|
195
|
+
* `InvalidRequestReason` with route context or trace metadata, the change
|
|
196
|
+
* lands here.
|
|
197
|
+
*/
|
|
198
|
+
export const invalidRequest = (message) => new AIError({
|
|
199
|
+
module: "ProviderShared",
|
|
200
|
+
method: "request",
|
|
201
|
+
reason: new InvalidRequestReason({ message }),
|
|
202
|
+
});
|
|
203
|
+
export const matchToolChoice = (route, toolChoice, cases) => Effect.gen(function* () {
|
|
204
|
+
if (toolChoice.type === "auto")
|
|
205
|
+
return cases.auto();
|
|
206
|
+
if (toolChoice.type === "none")
|
|
207
|
+
return cases.none();
|
|
208
|
+
if (toolChoice.type === "required")
|
|
209
|
+
return cases.required();
|
|
210
|
+
if (!toolChoice.name)
|
|
211
|
+
return yield* invalidRequest(`${route} tool choice requires a tool name`);
|
|
212
|
+
return cases.tool(toolChoice.name);
|
|
213
|
+
});
|
|
214
|
+
const formatContentTypes = (types) => {
|
|
215
|
+
if (types.length <= 1)
|
|
216
|
+
return types[0] ?? "";
|
|
217
|
+
if (types.length === 2)
|
|
218
|
+
return `${types[0]} and ${types[1]}`;
|
|
219
|
+
return `${types.slice(0, -1).join(", ")}, and ${types.at(-1)}`;
|
|
220
|
+
};
|
|
221
|
+
export const supportsContent = (part, types) => types.includes(part.type);
|
|
222
|
+
export const unsupportedContent = (route, role, types) => invalidRequest(`${route} ${role} messages only support ${formatContentTypes(types)} content for now`);
|
|
223
|
+
/**
|
|
224
|
+
* Build a `validate` step from a Schema decoder. Replaces the per-route
|
|
225
|
+
* lambda body `(payload) => decode(payload).pipe(Effect.mapError((e) =>
|
|
226
|
+
* invalid(e.message)))`. Any decode error is translated into
|
|
227
|
+
* `AIError` carrying the original parse-error message.
|
|
228
|
+
*/
|
|
229
|
+
export const validateWith = (decode) => (payload) => decode(payload).pipe(Effect.mapError((error) => invalidRequest(error.message)));
|
|
230
|
+
/**
|
|
231
|
+
* Build an HTTP POST with a JSON body. Sets `content-type: application/json`
|
|
232
|
+
* automatically after caller-supplied headers so routes cannot accidentally
|
|
233
|
+
* send JSON with a stale content type. The body is passed pre-encoded so
|
|
234
|
+
* routes can choose between
|
|
235
|
+
* `Schema.encodeSync(payload)` and `ProviderShared.encodeJson(payload)`.
|
|
236
|
+
*/
|
|
237
|
+
export const jsonPost = (input) => HttpClientRequest.post(input.url).pipe(HttpClientRequest.setHeaders(Headers.set(Headers.fromInput(input.headers), "content-type", "application/json")), HttpClientRequest.bodyText(input.body, "application/json"));
|
|
238
|
+
export * as ProviderShared from "./shared.js";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Auth } from "../../route/auth.js";
|
|
2
|
+
/**
|
|
3
|
+
* AWS credentials for SigV4 signing. Bedrock also supports Bearer API key auth,
|
|
4
|
+
* which provider facades configure as route auth instead of SigV4. STS-vended
|
|
5
|
+
* credentials should be refreshed by the consumer (rebuild the model) before
|
|
6
|
+
* they expire; the route does not refresh.
|
|
7
|
+
*/
|
|
8
|
+
export interface Credentials {
|
|
9
|
+
readonly region: string;
|
|
10
|
+
readonly accessKeyId: string;
|
|
11
|
+
readonly secretAccessKey: string;
|
|
12
|
+
readonly sessionToken?: string;
|
|
13
|
+
}
|
|
14
|
+
/** Sign the exact JSON bytes with SigV4 using credentials configured on the route. */
|
|
15
|
+
export declare const sigV4: (credentials: Credentials | undefined, options?: {
|
|
16
|
+
readonly service?: string;
|
|
17
|
+
readonly name?: string;
|
|
18
|
+
}) => Auth.Definition;
|
|
19
|
+
/** Bedrock route auth defaults to SigV4 and expects credentials from route configuration. */
|
|
20
|
+
export declare const auth: Auth.Definition;
|
|
21
|
+
export * as BedrockAuth from "./bedrock-auth.js";
|