@opencode/ai 2.0.14 → 2.0.15
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 +115 -56
- package/dist/experimental/evaluation-client.d.ts +1 -1
- package/dist/experimental/evaluation-client.js +39 -3
- package/dist/experimental/evaluation.d.ts +4 -4
- package/dist/experimental/evaluation.js +2 -2
- package/dist/experimental/system-one.d.ts +3 -3
- package/dist/experimental/system-one.js +40 -51
- package/dist/generation.d.ts +69 -0
- package/dist/generation.js +84 -0
- package/dist/image-client.d.ts +6 -4
- package/dist/image-client.js +11 -11
- package/dist/image.d.ts +1040 -62
- package/dist/image.js +80 -52
- package/dist/index.d.ts +5 -2
- package/dist/index.js +3 -1
- package/dist/llm.d.ts +9 -1
- package/dist/media.d.ts +212 -0
- package/dist/media.js +228 -0
- package/dist/promise.d.ts +554 -0
- package/dist/promise.js +44 -0
- package/dist/protocols/anthropic-messages.js +7 -17
- package/dist/protocols/bedrock-converse.d.ts +4 -4
- package/dist/protocols/bedrock-converse.js +1 -6
- package/dist/protocols/gemini.d.ts +21 -0
- package/dist/protocols/gemini.js +42 -6
- package/dist/protocols/google-images.d.ts +9 -21
- package/dist/protocols/google-images.js +169 -132
- package/dist/protocols/meta-images.d.ts +7 -12
- package/dist/protocols/meta-images.js +92 -66
- package/dist/protocols/mistral-chat.js +7 -6
- package/dist/protocols/open-responses.d.ts +11 -3
- package/dist/protocols/open-responses.js +23 -12
- package/dist/protocols/openai-chat.d.ts +34 -1
- package/dist/protocols/openai-chat.js +100 -31
- package/dist/protocols/openai-images.d.ts +9 -20
- package/dist/protocols/openai-images.js +112 -142
- package/dist/protocols/shared.d.ts +17 -17
- package/dist/protocols/shared.js +32 -35
- package/dist/protocols/utils/bedrock-media.d.ts +2 -3
- package/dist/protocols/utils/bedrock-media.js +4 -4
- package/dist/protocols/utils/media-input.d.ts +10 -0
- package/dist/protocols/utils/media-input.js +17 -0
- package/dist/protocols/utils/responses-compaction.js +6 -5
- package/dist/protocols/utils/tool-stream.d.ts +27 -3
- package/dist/protocols/xai-images.d.ts +9 -15
- package/dist/protocols/xai-images.js +85 -83
- package/dist/protocols/zai-images.d.ts +9 -13
- package/dist/protocols/zai-images.js +59 -57
- package/dist/providers/amazon-bedrock.d.ts +2 -2
- package/dist/providers/cerebras.js +6 -1
- package/dist/providers/deepinfra.js +6 -1
- package/dist/providers/google-vertex.d.ts +7 -0
- package/dist/providers/google.d.ts +7 -0
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.js +1 -0
- package/dist/providers/openrouter.d.ts +19 -0
- package/dist/providers/openrouter.js +13 -1
- package/dist/providers/vercel-ai-gateway.d.ts +41 -0
- package/dist/providers/vercel-ai-gateway.js +85 -0
- package/dist/route/client.d.ts +9 -1
- package/dist/route/endpoint.d.ts +10 -10
- package/dist/route/executor-service.d.ts +12 -0
- package/dist/route/executor-service.js +3 -0
- package/dist/route/executor.d.ts +4 -9
- package/dist/route/executor.js +3 -3
- package/dist/route/index.d.ts +2 -0
- package/dist/route/index.js +2 -0
- package/dist/route/media-protocol.d.ts +45 -0
- package/dist/route/media-protocol.js +40 -0
- package/dist/route/media.d.ts +46 -0
- package/dist/route/media.js +64 -0
- package/dist/schema/errors.d.ts +13 -3
- package/dist/schema/errors.js +7 -0
- package/dist/schema/events.d.ts +563 -40
- package/dist/schema/events.js +35 -2
- package/dist/schema/messages.d.ts +98 -8
- package/dist/schema/messages.js +8 -6
- package/dist/schema/options.d.ts +2 -0
- package/dist/schema/options.js +3 -0
- package/dist/testing.d.ts +72 -8
- package/dist/utils/media-type.d.ts +6 -0
- package/dist/utils/media-type.js +49 -0
- package/dist/utils/sanitize.js +3 -1
- package/package.json +7 -3
- package/dist/protocols/utils/image-input.d.ts +0 -21
- package/dist/protocols/utils/image-input.js +0 -20
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
import { Effect,
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { ImageModel, ImageResponse } from "../image.js";
|
|
3
|
+
import { Media } from "../media.js";
|
|
4
|
+
import { MediaProtocol } from "../route/media-protocol.js";
|
|
5
|
+
import { MediaRoute } from "../route/media.js";
|
|
6
|
+
import { ProviderID, mergeJsonRecords } from "../schema/index.js";
|
|
6
7
|
import { ProviderShared } from "./shared.js";
|
|
7
|
-
import {
|
|
8
|
-
import { OpenAIImage } from "./utils/openai-image.js";
|
|
8
|
+
import { MediaInput } from "./utils/media-input.js";
|
|
9
9
|
const ADAPTER = "openai-images";
|
|
10
|
+
const NAME = "OpenAI Images";
|
|
11
|
+
const PROVIDER = ProviderID.make("openai");
|
|
10
12
|
export const DEFAULT_BASE_URL = "https://api.openai.com/v1";
|
|
11
13
|
export const PATH = "/images/generations";
|
|
12
14
|
export const EDIT_PATH = "/images/edits";
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// 2. Response schema
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
13
18
|
const OpenAIImageResponse = Schema.Struct({
|
|
14
19
|
data: Schema.Array(Schema.Struct({
|
|
15
20
|
b64_json: Schema.optional(Schema.String),
|
|
@@ -25,159 +30,124 @@ const OpenAIImageResponse = Schema.Struct({
|
|
|
25
30
|
output_tokens_details: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
26
31
|
})),
|
|
27
32
|
});
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// 5. Request body construction
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
/** Multipart field names the route owns; `http.body` overlays cannot smuggle replacements for them. */
|
|
37
|
+
const RESERVED_FORM_FIELDS = new Set(["model", "prompt", "image", "image[]", "images", "mask"]);
|
|
28
38
|
const nativeOptions = (options) => {
|
|
29
39
|
if (!options)
|
|
30
40
|
return undefined;
|
|
31
|
-
const {
|
|
32
|
-
return {
|
|
33
|
-
output_format: outputFormat,
|
|
34
|
-
output_compression: outputCompression,
|
|
35
|
-
...native,
|
|
36
|
-
};
|
|
41
|
+
const { outputCompression, ...native } = options;
|
|
42
|
+
return { output_compression: outputCompression, ...native };
|
|
37
43
|
};
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
return
|
|
44
|
+
const isEdit = (request) => (request.images?.length ?? 0) > 0;
|
|
45
|
+
const isInline = (asset) => asset.inline() !== undefined;
|
|
46
|
+
const blob = (data, mediaType) => {
|
|
47
|
+
const buffer = new ArrayBuffer(data.byteLength);
|
|
48
|
+
new Uint8Array(buffer).set(data);
|
|
49
|
+
return new Blob([buffer], { type: mediaType });
|
|
44
50
|
};
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
headers: Headers.remove(Headers.fromInput({ ...input.headers, ...http?.headers }), "content-type"),
|
|
95
|
-
});
|
|
96
|
-
const response = yield* execute(HttpClientRequest.post(url).pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.bodyFormData(form)));
|
|
97
|
-
return yield* parseResponse(response, request.options, http?.body);
|
|
98
|
-
}
|
|
99
|
-
const references = sourceImages.map((image) => {
|
|
100
|
-
if (image.type === "bytes")
|
|
101
|
-
return { image_url: ImageInputs.dataUrl(image) };
|
|
102
|
-
if (image.type === "url")
|
|
103
|
-
return { image_url: image.url };
|
|
104
|
-
if (image.type === "file-id")
|
|
105
|
-
return { file_id: image.id };
|
|
106
|
-
return undefined;
|
|
107
|
-
});
|
|
108
|
-
if (references.some((image) => image === undefined))
|
|
109
|
-
return yield* ImageInputs.invalid("OpenAI Images accepts image URLs, data URLs, bytes, and file IDs");
|
|
110
|
-
const maskReference = mask === undefined
|
|
111
|
-
? undefined
|
|
112
|
-
: mask.type === "bytes"
|
|
113
|
-
? { image_url: ImageInputs.dataUrl(mask) }
|
|
114
|
-
: mask.type === "url"
|
|
115
|
-
? { image_url: mask.url }
|
|
116
|
-
: mask.type === "file-id"
|
|
117
|
-
? { file_id: mask.id }
|
|
118
|
-
: undefined;
|
|
119
|
-
if (mask !== undefined && maskReference === undefined)
|
|
120
|
-
return yield* ImageInputs.invalid("OpenAI Images accepts masks as URLs, data URLs, bytes, or file IDs");
|
|
121
|
-
const requestBody = mergeJsonRecords({
|
|
122
|
-
model: request.model.id,
|
|
123
|
-
prompt: request.prompt,
|
|
124
|
-
images: references.length === 0 ? undefined : references,
|
|
125
|
-
mask: maskReference,
|
|
126
|
-
}, nativeOptions(request.options), http?.body);
|
|
127
|
-
const text = ProviderShared.encodeJson(requestBody);
|
|
128
|
-
const headers = yield* Auth.toEffect(input.auth)({
|
|
129
|
-
request,
|
|
130
|
-
method: "POST",
|
|
131
|
-
url,
|
|
132
|
-
body: text,
|
|
133
|
-
headers: Headers.fromInput({ ...input.headers, ...http?.headers }),
|
|
134
|
-
});
|
|
135
|
-
const response = yield* execute(HttpClientRequest.post(url).pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.bodyText(text, "application/json")));
|
|
136
|
-
return yield* parseResponse(response, request.options, http?.body);
|
|
137
|
-
}),
|
|
138
|
-
};
|
|
139
|
-
return ImageModel.make({ id: input.id, provider: "openai", route, http: input.http });
|
|
51
|
+
const reference = (asset) => {
|
|
52
|
+
const inline = asset.inline();
|
|
53
|
+
if (inline)
|
|
54
|
+
return Effect.succeed({ image_url: inline.dataUrl });
|
|
55
|
+
const url = ProviderShared.mediaUrl(asset);
|
|
56
|
+
if (url)
|
|
57
|
+
return Effect.succeed({ image_url: url });
|
|
58
|
+
const id = MediaInput.refID(asset, PROVIDER);
|
|
59
|
+
if (id)
|
|
60
|
+
return Effect.succeed({ file_id: id });
|
|
61
|
+
return Effect.fail(ProviderShared.invalidRequest("OpenAI Images accepts image URLs, data URLs, bytes, and OpenAI file IDs"));
|
|
62
|
+
};
|
|
63
|
+
const fromRequest = Effect.fn("OpenAIImages.fromRequest")(function* (request) {
|
|
64
|
+
const images = request.images ?? [];
|
|
65
|
+
const mask = request.mask;
|
|
66
|
+
if (mask !== undefined && images.length === 0)
|
|
67
|
+
return yield* ProviderShared.invalidRequest("An OpenAI image mask requires at least one input image");
|
|
68
|
+
const fields = mergeJsonRecords({ n: request.n, size: request.size, output_format: request.format }, nativeOptions(request.providerOptions), request.http?.body);
|
|
69
|
+
// Owned bytes go through multipart edits; remote URLs and file IDs use the JSON edits body instead.
|
|
70
|
+
if (images.length > 0 && images.every(isInline) && (mask === undefined || isInline(mask))) {
|
|
71
|
+
const form = new FormData();
|
|
72
|
+
form.append("model", request.model.id);
|
|
73
|
+
form.append("prompt", request.prompt);
|
|
74
|
+
Object.entries(fields ?? {}).forEach(([key, value]) => {
|
|
75
|
+
if (RESERVED_FORM_FIELDS.has(key))
|
|
76
|
+
return;
|
|
77
|
+
form.append(key, typeof value === "string" ? value : ProviderShared.encodeJson(value));
|
|
78
|
+
});
|
|
79
|
+
const uploads = yield* Effect.forEach(images, (image) => MediaInput.inlineBytes(ADAPTER, image));
|
|
80
|
+
uploads.forEach((data, index) => form.append("image[]", blob(data, images[index].mediaType), `image-${index}`));
|
|
81
|
+
if (mask !== undefined)
|
|
82
|
+
form.append("mask", blob(yield* MediaInput.inlineBytes(ADAPTER, mask), mask.mediaType), "mask");
|
|
83
|
+
return MediaProtocol.multipart(form);
|
|
84
|
+
}
|
|
85
|
+
const references = yield* Effect.forEach(images, reference);
|
|
86
|
+
const maskReference = mask === undefined ? undefined : yield* reference(mask);
|
|
87
|
+
return MediaProtocol.json(mergeJsonRecords({
|
|
88
|
+
model: request.model.id,
|
|
89
|
+
prompt: request.prompt,
|
|
90
|
+
images: references.length === 0 ? undefined : references,
|
|
91
|
+
mask: maskReference,
|
|
92
|
+
}, fields) ?? {});
|
|
93
|
+
});
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// 6. Response decoding
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
const requestedFormat = (body) => {
|
|
98
|
+
const value = body.type === "json" ? body.value.output_format : body.value.get("output_format");
|
|
99
|
+
return typeof value === "string" ? value : undefined;
|
|
140
100
|
};
|
|
141
|
-
const
|
|
142
|
-
const output = yield*
|
|
143
|
-
const decoded =
|
|
144
|
-
const
|
|
145
|
-
const
|
|
101
|
+
const decodeResponse = Effect.fn("OpenAIImages.decodeResponse")(function* (response, context) {
|
|
102
|
+
const output = yield* MediaProtocol.decodeJson(ADAPTER, NAME, OpenAIImageResponse)(response);
|
|
103
|
+
const decoded = output.value;
|
|
104
|
+
const format = decoded.output_format ?? requestedFormat(context.body) ?? "png";
|
|
105
|
+
const mediaType = `image/${format}`;
|
|
146
106
|
const images = yield* Effect.forEach(decoded.data, (item, index) => {
|
|
107
|
+
const providerMetadata = item.revised_prompt === undefined ? undefined : { openai: { revisedPrompt: item.revised_prompt } };
|
|
147
108
|
if (item.b64_json)
|
|
148
|
-
return
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
})));
|
|
109
|
+
return MediaInput.decodedAsset(output.invalid, `${NAME} result ${index}`, item.b64_json, mediaType, {
|
|
110
|
+
info: { format },
|
|
111
|
+
providerMetadata,
|
|
112
|
+
});
|
|
153
113
|
if (item.url)
|
|
154
|
-
return Effect.succeed(
|
|
155
|
-
|
|
156
|
-
data: item.url,
|
|
157
|
-
providerMetadata: item.revised_prompt === undefined ? undefined : { openai: { revisedPrompt: item.revised_prompt } },
|
|
158
|
-
}));
|
|
159
|
-
return Effect.fail(output.invalid(`OpenAI Images result ${index} has neither image data nor a URL`));
|
|
114
|
+
return Effect.succeed(Media.url(item.url, { mediaType, info: { format }, providerMetadata }));
|
|
115
|
+
return Effect.fail(output.invalid(`${NAME} result ${index} has neither image data nor a URL`));
|
|
160
116
|
});
|
|
161
117
|
if (images.length === 0)
|
|
162
|
-
return yield* output.invalid(
|
|
118
|
+
return yield* output.invalid(`${NAME} returned no images`);
|
|
163
119
|
return new ImageResponse({
|
|
164
120
|
images,
|
|
165
121
|
usage: decoded.usage === undefined
|
|
166
122
|
? undefined
|
|
167
|
-
:
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
123
|
+
: {
|
|
124
|
+
type: "tokens",
|
|
125
|
+
input: decoded.usage.input_tokens,
|
|
126
|
+
output: decoded.usage.output_tokens,
|
|
127
|
+
total: decoded.usage.total_tokens,
|
|
128
|
+
details: { openai: decoded.usage },
|
|
129
|
+
},
|
|
173
130
|
providerMetadata: { openai: { outputFormat: format } },
|
|
174
131
|
});
|
|
175
132
|
});
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// 7. Protocol and route
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
export const protocol = MediaProtocol.inline({
|
|
137
|
+
id: ADAPTER,
|
|
138
|
+
name: NAME,
|
|
139
|
+
unsupported: ["aspectRatio", "seed"],
|
|
140
|
+
body: { from: fromRequest },
|
|
141
|
+
response: { decode: decodeResponse },
|
|
142
|
+
});
|
|
143
|
+
export const model = (input) => ImageModel.fromRoute({
|
|
144
|
+
id: ADAPTER,
|
|
145
|
+
provider: PROVIDER,
|
|
146
|
+
protocol,
|
|
147
|
+
baseURL: DEFAULT_BASE_URL,
|
|
148
|
+
path: ({ request }) => (isEdit(request) ? EDIT_PATH : PATH),
|
|
149
|
+
}, input);
|
|
181
150
|
export const OpenAIImages = {
|
|
151
|
+
protocol,
|
|
182
152
|
model,
|
|
183
153
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Tool } from "@opencode/schema/tool";
|
|
2
2
|
import { Effect, Schema, Stream } from "effect";
|
|
3
|
-
import { Headers, HttpClientRequest
|
|
3
|
+
import { Headers, HttpClientRequest } from "effect/unstable/http";
|
|
4
|
+
import { Media } from "../media.js";
|
|
4
5
|
import { AIError, LLMRequest, Message, ToolDefinition, type ContentPart, type MediaPart, type ProviderID, type ToolEntry, type ToolResultPart } from "../schema/index.js";
|
|
5
6
|
import { isRecord } from "../utils/record.js";
|
|
6
7
|
export { isRecord };
|
|
@@ -110,23 +111,26 @@ export declare const wrappedSystemUpdate: (route: string, message: Message) => E
|
|
|
110
111
|
* routes: `Invalid JSON input for <route> tool call <name>`.
|
|
111
112
|
*/
|
|
112
113
|
export declare const parseToolInput: (route: string, name: string, raw: string) => Effect.Effect<unknown, AIError, never>;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
114
|
+
/** Inline view or a typed `InvalidRequest` for routes that cannot fetch URLs or dereference provider refs. */
|
|
115
|
+
export declare const requireInlineMedia: (route: string, asset: Media.Asset) => Effect.Effect<Media.Inline, AIError>;
|
|
116
|
+
export declare const inlineRequired: (route: string, asset: Media.Asset) => AIError;
|
|
117
|
+
/** The remote URL of a `url` asset, for protocols that accept `http(s)` references natively. */
|
|
118
|
+
export declare const mediaUrl: (asset: Media.Asset) => string | undefined;
|
|
119
|
+
/**
|
|
120
|
+
* Lift a tool-result file into a `MediaPart`. Tool files carry either a data URL, an `http(s)` URL, or raw base64 in
|
|
121
|
+
* `uri`; the declared `mime` wins over any data-URL prefix so tool authors control the type the model sees.
|
|
122
|
+
*/
|
|
123
|
+
export declare const toolFileMedia: (item: Tool.FileContent) => MediaPart;
|
|
120
124
|
export declare const trimBaseUrl: (value: string) => string;
|
|
121
125
|
export declare const toolResultText: (part: ToolResultPart) => string;
|
|
122
126
|
export declare const errorText: (error: unknown) => string;
|
|
123
127
|
/**
|
|
124
128
|
* `framing` step for Server-Sent Events. Decodes UTF-8, runs the SSE channel
|
|
125
|
-
* decoder, optionally filters named events, and drops empty
|
|
126
|
-
* is dropped by default or retained for protocols that use it
|
|
127
|
-
* boundary. Retry control events are ignored without
|
|
128
|
-
* Decoder failures become provider output errors so
|
|
129
|
-
* stays `AIError`.
|
|
129
|
+
* decoder, optionally filters named events, and drops empty and bare `null`
|
|
130
|
+
* events. `[DONE]` is dropped by default or retained for protocols that use it
|
|
131
|
+
* as their stream boundary. Retry control events are ignored without
|
|
132
|
+
* interrupting the stream. Decoder failures become provider output errors so
|
|
133
|
+
* the public error channel stays `AIError`.
|
|
130
134
|
*/
|
|
131
135
|
export declare const sseFraming: (bytes: Stream.Stream<Uint8Array, AIError>, events?: ReadonlySet<string>, includeDone?: boolean) => Stream.Stream<string, AIError>;
|
|
132
136
|
/**
|
|
@@ -158,10 +162,6 @@ export declare const flattenToolRequest: (request: LLMRequest) => {
|
|
|
158
162
|
readonly [x: string]: unknown;
|
|
159
163
|
}, import("../route.js").CompactionOperations | undefined>>;
|
|
160
164
|
};
|
|
161
|
-
export declare const imageResponse: (route: string, name: string, response: HttpClientResponse.HttpClientResponse) => Effect.Effect<{
|
|
162
|
-
body: string;
|
|
163
|
-
invalid: (message: string, cause?: unknown) => AIError;
|
|
164
|
-
}, AIError, never>;
|
|
165
165
|
export declare const matchToolChoice: <Auto, None, Required, Tool>(route: string, toolChoice: NonNullable<LLMRequest["toolChoice"]>, cases: {
|
|
166
166
|
readonly auto: () => Auto;
|
|
167
167
|
readonly none: () => None;
|
package/dist/protocols/shared.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { Buffer } from "node:buffer";
|
|
2
1
|
import { Tool } from "@opencode/schema/tool";
|
|
3
2
|
import { Effect, Option, Schema, Stream } from "effect";
|
|
4
3
|
import * as Sse from "effect/unstable/encoding/Sse";
|
|
5
|
-
import { Headers, HttpClientRequest
|
|
6
|
-
import {
|
|
4
|
+
import { Headers, HttpClientRequest } from "effect/unstable/http";
|
|
5
|
+
import { Media } from "../media.js";
|
|
6
|
+
import { InvalidProviderOutputError, InvalidRequestError, UnsupportedOperationError, AIError, LLMRequest, Message, ToolDefinition, } from "../schema/index.js";
|
|
7
7
|
import { isRecord } from "../utils/record.js";
|
|
8
8
|
export { isRecord };
|
|
9
9
|
export const Json = Schema.fromJsonString(Schema.Unknown);
|
|
@@ -126,17 +126,27 @@ export const wrappedSystemUpdate = Effect.fn("ProviderShared.wrappedSystemUpdate
|
|
|
126
126
|
* routes: `Invalid JSON input for <route> tool call <name>`.
|
|
127
127
|
*/
|
|
128
128
|
export const parseToolInput = (route, name, raw) => parseJson(route, raw || "{}", `Invalid JSON input for ${route} tool call ${name}`);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
129
|
+
/** Inline view or a typed `InvalidRequest` for routes that cannot fetch URLs or dereference provider refs. */
|
|
130
|
+
export const requireInlineMedia = (route, asset) => {
|
|
131
|
+
const inline = asset.inline();
|
|
132
|
+
return inline ? Effect.succeed(inline) : Effect.fail(inlineRequired(route, asset));
|
|
133
|
+
};
|
|
134
|
+
export const inlineRequired = (route, asset) => invalidRequest(`${route} requires inline media (bytes or base64); ${asset.source.type} sources must be materialized first`);
|
|
135
|
+
/** The remote URL of a `url` asset, for protocols that accept `http(s)` references natively. */
|
|
136
|
+
export const mediaUrl = (asset) => (asset.source.type === "url" ? asset.source.url : undefined);
|
|
137
|
+
/**
|
|
138
|
+
* Lift a tool-result file into a `MediaPart`. Tool files carry either a data URL, an `http(s)` URL, or raw base64 in
|
|
139
|
+
* `uri`; the declared `mime` wins over any data-URL prefix so tool authors control the type the model sees.
|
|
140
|
+
*/
|
|
141
|
+
export const toolFileMedia = (item) => {
|
|
142
|
+
const parsed = Media.parseDataUrl(item.uri);
|
|
143
|
+
const asset = parsed
|
|
144
|
+
? Media.from({ ...parsed.source, mediaType: item.mime })
|
|
145
|
+
: /^https?:\/\//.test(item.uri)
|
|
146
|
+
? Media.url(item.uri, { mediaType: item.mime })
|
|
147
|
+
: Media.base64(item.uri, item.mime);
|
|
148
|
+
return Message.media(asset, { filename: item.name });
|
|
138
149
|
};
|
|
139
|
-
export const normalizeToolFile = (part) => normalizeMedia({ type: "media", mediaType: part.mime, data: part.uri, filename: part.name });
|
|
140
150
|
export const trimBaseUrl = (value) => value.replace(/\/+$/, "");
|
|
141
151
|
export const toolResultText = (part) => {
|
|
142
152
|
if (part.result.type === "text")
|
|
@@ -164,11 +174,11 @@ export const errorText = (error) => {
|
|
|
164
174
|
};
|
|
165
175
|
/**
|
|
166
176
|
* `framing` step for Server-Sent Events. Decodes UTF-8, runs the SSE channel
|
|
167
|
-
* decoder, optionally filters named events, and drops empty
|
|
168
|
-
* is dropped by default or retained for protocols that use it
|
|
169
|
-
* boundary. Retry control events are ignored without
|
|
170
|
-
* Decoder failures become provider output errors so
|
|
171
|
-
* stays `AIError`.
|
|
177
|
+
* decoder, optionally filters named events, and drops empty and bare `null`
|
|
178
|
+
* events. `[DONE]` is dropped by default or retained for protocols that use it
|
|
179
|
+
* as their stream boundary. Retry control events are ignored without
|
|
180
|
+
* interrupting the stream. Decoder failures become provider output errors so
|
|
181
|
+
* the public error channel stays `AIError`.
|
|
172
182
|
*/
|
|
173
183
|
export const sseFraming = (bytes, events, includeDone = false) => bytes.pipe(Stream.decodeText(), Stream.mapAccumEffect(() => {
|
|
174
184
|
const output = [];
|
|
@@ -186,6 +196,10 @@ export const sseFraming = (bytes, events, includeDone = false) => bytes.pipe(Str
|
|
|
186
196
|
return [state, state.output.splice(0)];
|
|
187
197
|
})), Stream.filter((event) => (events === undefined || events.has(event.event)) &&
|
|
188
198
|
event.data.length > 0 &&
|
|
199
|
+
// Some OpenAI-compatible proxies serialize an empty flush as a bare
|
|
200
|
+
// `data: null`, between events or after `[DONE]`. No protocol has a
|
|
201
|
+
// null event, so it carries nothing and must not abort the stream.
|
|
202
|
+
event.data !== "null" &&
|
|
189
203
|
(event.data !== "[DONE]" || includeDone || (events !== undefined && event.event !== "message"))), Stream.map((event) => event.data));
|
|
190
204
|
/**
|
|
191
205
|
* Canonical invalid-request constructor shared by protocol lowering.
|
|
@@ -241,23 +255,6 @@ export const flattenToolRequest = (request) => {
|
|
|
241
255
|
: LLMRequest.update(request, { messages }),
|
|
242
256
|
};
|
|
243
257
|
};
|
|
244
|
-
export const imageResponse = Effect.fn("ProviderShared.imageResponse")(function* (route, name, response) {
|
|
245
|
-
const http = new HttpContext({ url: response.request.url, status: response.status, headers: response.headers });
|
|
246
|
-
const body = yield* response.text.pipe(Effect.mapError((cause) => new AIError({
|
|
247
|
-
reason: new InvalidProviderOutputError({
|
|
248
|
-
route,
|
|
249
|
-
message: `Failed to read the ${name} response`,
|
|
250
|
-
http,
|
|
251
|
-
cause,
|
|
252
|
-
}),
|
|
253
|
-
})));
|
|
254
|
-
return {
|
|
255
|
-
body,
|
|
256
|
-
invalid: (message, cause) => new AIError({
|
|
257
|
-
reason: new InvalidProviderOutputError({ route, message, body, http, cause }),
|
|
258
|
-
}),
|
|
259
|
-
};
|
|
260
|
-
});
|
|
261
258
|
export const matchToolChoice = (route, toolChoice, cases) => Effect.gen(function* () {
|
|
262
259
|
if (toolChoice.type === "auto")
|
|
263
260
|
return cases.auto();
|
|
@@ -23,9 +23,8 @@ export declare const DocumentBlock: Schema.Struct<{
|
|
|
23
23
|
}>;
|
|
24
24
|
export type DocumentBlock = Schema.Schema.Type<typeof DocumentBlock>;
|
|
25
25
|
export declare const lower: (part: {
|
|
26
|
-
readonly data: string | Uint8Array<ArrayBufferLike>;
|
|
27
26
|
readonly type: "media";
|
|
28
|
-
readonly
|
|
27
|
+
readonly media: import("../../media.js").Asset;
|
|
29
28
|
readonly metadata?: {
|
|
30
29
|
readonly [x: string]: unknown;
|
|
31
30
|
} | undefined;
|
|
@@ -45,7 +44,7 @@ export declare const lower: (part: {
|
|
|
45
44
|
};
|
|
46
45
|
}[] | ({
|
|
47
46
|
readonly document: {
|
|
48
|
-
readonly format: "pdf" | "
|
|
47
|
+
readonly format: "pdf" | "txt" | "md" | "csv" | "doc" | "docx" | "xls" | "xlsx" | "html";
|
|
49
48
|
readonly name: string;
|
|
50
49
|
readonly source: {
|
|
51
50
|
readonly bytes: string;
|
|
@@ -62,7 +62,7 @@ function documentName(filename, names) {
|
|
|
62
62
|
return name;
|
|
63
63
|
}
|
|
64
64
|
const mediaBase64 = Effect.fn("BedrockMedia.mediaBase64")(function* (part) {
|
|
65
|
-
const media = ProviderShared.
|
|
65
|
+
const media = yield* ProviderShared.requireInlineMedia("Bedrock Converse", part.media);
|
|
66
66
|
const bytes = yield* Effect.fromResult(Encoding.decodeBase64(media.base64)).pipe(Effect.mapError((cause) => ProviderShared.invalidRequest("Bedrock Converse media data must be valid base64", cause)));
|
|
67
67
|
return Encoding.encodeBase64(bytes);
|
|
68
68
|
});
|
|
@@ -72,13 +72,13 @@ const mediaBase64 = Effect.fn("BedrockMedia.mediaBase64")(function* (part) {
|
|
|
72
72
|
// get an image-specific error so the caller knows it's a format-support issue,
|
|
73
73
|
// not a kind-detection issue.
|
|
74
74
|
export const lower = Effect.fn("BedrockMedia.lower")(function* (part, documentNames) {
|
|
75
|
-
const mime = part.mediaType.toLowerCase();
|
|
75
|
+
const mime = part.media.mediaType.toLowerCase();
|
|
76
76
|
const imageFormat = IMAGE_FORMATS[mime];
|
|
77
77
|
if (imageFormat) {
|
|
78
78
|
return [{ image: { format: imageFormat, source: { bytes: yield* mediaBase64(part) } } }];
|
|
79
79
|
}
|
|
80
80
|
if (mime.startsWith("image/"))
|
|
81
|
-
return yield* ProviderShared.invalidRequest(`Bedrock Converse does not support image media type ${part.mediaType}`);
|
|
81
|
+
return yield* ProviderShared.invalidRequest(`Bedrock Converse does not support image media type ${part.media.mediaType}`);
|
|
82
82
|
const documentFormat = DOCUMENT_FORMATS[mime];
|
|
83
83
|
if (documentFormat) {
|
|
84
84
|
const name = documentName(part.filename, documentNames);
|
|
@@ -92,6 +92,6 @@ export const lower = Effect.fn("BedrockMedia.lower")(function* (part, documentNa
|
|
|
92
92
|
]
|
|
93
93
|
: [block];
|
|
94
94
|
}
|
|
95
|
-
return yield* ProviderShared.invalidRequest(`Bedrock Converse does not support media type ${part.mediaType}`);
|
|
95
|
+
return yield* ProviderShared.invalidRequest(`Bedrock Converse does not support media type ${part.media.mediaType}`);
|
|
96
96
|
});
|
|
97
97
|
export * as BedrockMedia from "./bedrock-media.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { Media } from "../../media.js";
|
|
3
|
+
import type { AIError, ProviderID } from "../../schema/index.js";
|
|
4
|
+
/** Owned bytes for multipart uploads; decodes `base64` sources and rejects remote sources. */
|
|
5
|
+
export declare const inlineBytes: (route: string, asset: Media.Asset) => Effect.Effect<Uint8Array, AIError>;
|
|
6
|
+
/** Provider file handle when the ref belongs to this provider; refs from other providers are never forwarded. */
|
|
7
|
+
export declare const refID: (asset: Media.Asset, provider: ProviderID) => string | undefined;
|
|
8
|
+
/** Decode a provider's base64 output once into an owned `bytes` asset, sniffing the type when it is not declared. */
|
|
9
|
+
export declare const decodedAsset: (invalid: (message: string, cause?: unknown) => AIError, label: string, data: string, mediaType: string | undefined, options?: Media.AssetOptions) => Effect.Effect<Media.Asset, AIError, never>;
|
|
10
|
+
export * as MediaInput from "./media-input.js";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Effect, Encoding } from "effect";
|
|
2
|
+
import { Media } from "../../media.js";
|
|
3
|
+
import { ProviderShared } from "../shared.js";
|
|
4
|
+
/** Owned bytes for multipart uploads; decodes `base64` sources and rejects remote sources. */
|
|
5
|
+
export const inlineBytes = (route, asset) => {
|
|
6
|
+
if (asset.source.type === "bytes")
|
|
7
|
+
return Effect.succeed(asset.source.data);
|
|
8
|
+
const inline = asset.inline();
|
|
9
|
+
if (!inline)
|
|
10
|
+
return Effect.fail(ProviderShared.inlineRequired(route, asset));
|
|
11
|
+
return Effect.fromResult(Encoding.decodeBase64(inline.base64)).pipe(Effect.mapError((cause) => ProviderShared.invalidRequest(`${route} media contains invalid base64 data`, cause)));
|
|
12
|
+
};
|
|
13
|
+
/** Provider file handle when the ref belongs to this provider; refs from other providers are never forwarded. */
|
|
14
|
+
export const refID = (asset, provider) => asset.source.type === "ref" && asset.source.provider === provider ? asset.source.id : undefined;
|
|
15
|
+
/** Decode a provider's base64 output once into an owned `bytes` asset, sniffing the type when it is not declared. */
|
|
16
|
+
export const decodedAsset = (invalid, label, data, mediaType, options) => Effect.fromResult(Encoding.decodeBase64(data)).pipe(Effect.mapError((cause) => invalid(`${label} contains invalid base64 data`, cause)), Effect.map((bytes) => Media.bytes(bytes, mediaType, options)));
|
|
17
|
+
export * as MediaInput from "./media-input.js";
|
|
@@ -6,6 +6,7 @@ import { RequestExecutor } from "../../route/executor.js";
|
|
|
6
6
|
import { HttpTransport } from "../../route/transport/index.js";
|
|
7
7
|
import { OpenResponses } from "../open-responses.js";
|
|
8
8
|
import { JsonObject, optionalNull, ProviderShared } from "../shared.js";
|
|
9
|
+
import { Media } from "../../media.js";
|
|
9
10
|
const Body = Schema.Struct({
|
|
10
11
|
model: Schema.String,
|
|
11
12
|
input: Schema.Array(Schema.Unknown),
|
|
@@ -113,19 +114,19 @@ function toMessage(item, model) {
|
|
|
113
114
|
if (part.type === "input_image")
|
|
114
115
|
return {
|
|
115
116
|
type: "media",
|
|
116
|
-
|
|
117
|
-
mediaType: /^data:([^;,]+)/.exec(part.image_url)?.[1] ?? "image/*",
|
|
117
|
+
media: replayMedia(part.image_url, "image/*"),
|
|
118
118
|
providerMetadata: part.detail === undefined ? undefined : { [key]: { detail: part.detail } },
|
|
119
119
|
};
|
|
120
|
-
const data = part.file_url === undefined ? part.file_data : part.file_url;
|
|
121
120
|
return {
|
|
122
121
|
type: "media",
|
|
123
|
-
|
|
122
|
+
media: replayMedia(part.file_url === undefined ? part.file_data : part.file_url, "application/octet-stream"),
|
|
124
123
|
filename: part.filename,
|
|
125
|
-
mediaType: /^data:([^;,]+)/.exec(data)?.[1] ?? "application/octet-stream",
|
|
126
124
|
providerMetadata: part.detail === undefined ? undefined : { [key]: { detail: part.detail } },
|
|
127
125
|
};
|
|
128
126
|
}),
|
|
129
127
|
});
|
|
130
128
|
}
|
|
129
|
+
/** Replayed compaction items carry either a data URL or a remote URL; the data URL's own type wins when present. */
|
|
130
|
+
const replayMedia = (value, fallbackType) => Media.parseDataUrl(value) ??
|
|
131
|
+
(/^https?:\/\//.test(value) ? Media.url(value, { mediaType: fallbackType }) : Media.base64(value, fallbackType));
|
|
131
132
|
export * as ResponsesCompaction from "./responses-compaction.js";
|