@opencode/ai 2.0.14 → 2.0.16
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 +399 -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 +83 -0
- package/dist/generation.js +113 -0
- package/dist/image-client.d.ts +16 -7
- package/dist/image-client.js +29 -13
- package/dist/image.d.ts +1410 -81
- package/dist/image.js +97 -67
- package/dist/index.d.ts +17 -2
- package/dist/index.js +12 -1
- package/dist/llm.d.ts +9 -1
- package/dist/media-model.d.ts +44 -0
- package/dist/media-model.js +49 -0
- package/dist/media.d.ts +213 -0
- package/dist/media.js +227 -0
- package/dist/promise.d.ts +974 -0
- package/dist/promise.js +81 -0
- package/dist/protocols/alibaba-chat.d.ts +12 -0
- package/dist/protocols/alibaba-responses.d.ts +2 -2
- package/dist/protocols/anthropic-messages.js +8 -19
- package/dist/protocols/assemblyai-transcription.d.ts +40 -0
- package/dist/protocols/assemblyai-transcription.js +138 -0
- package/dist/protocols/bedrock-converse.d.ts +4 -4
- package/dist/protocols/bedrock-converse.js +6 -17
- package/dist/protocols/bfl-images.d.ts +32 -0
- package/dist/protocols/bfl-images.js +153 -0
- package/dist/protocols/cartesia-speech.d.ts +127 -0
- package/dist/protocols/cartesia-speech.js +126 -0
- package/dist/protocols/deepgram-speech.d.ts +119 -0
- package/dist/protocols/deepgram-speech.js +92 -0
- package/dist/protocols/deepgram-transcription.d.ts +25 -0
- package/dist/protocols/deepgram-transcription.js +129 -0
- package/dist/protocols/elevenlabs-speech.d.ts +122 -0
- package/dist/protocols/elevenlabs-speech.js +115 -0
- package/dist/protocols/fal-images.d.ts +24 -0
- package/dist/protocols/fal-images.js +114 -0
- package/dist/protocols/fal-video.d.ts +29 -0
- package/dist/protocols/fal-video.js +88 -0
- package/dist/protocols/gemini.d.ts +30 -9
- package/dist/protocols/gemini.js +45 -35
- package/dist/protocols/google-images.d.ts +9 -21
- package/dist/protocols/google-images.js +158 -133
- package/dist/protocols/google-speech.d.ts +130 -0
- package/dist/protocols/google-speech.js +84 -0
- package/dist/protocols/google-transcription.d.ts +173 -0
- package/dist/protocols/google-transcription.js +138 -0
- package/dist/protocols/google-video.d.ts +26 -0
- package/dist/protocols/google-video.js +158 -0
- package/dist/protocols/meta-images.d.ts +7 -12
- package/dist/protocols/meta-images.js +85 -66
- package/dist/protocols/meta-responses.d.ts +4 -4
- package/dist/protocols/meta-responses.js +1 -1
- package/dist/protocols/mistral-chat.js +7 -6
- package/dist/protocols/open-responses.d.ts +17 -9
- package/dist/protocols/open-responses.js +24 -14
- package/dist/protocols/openai-chat.d.ts +118 -1
- package/dist/protocols/openai-chat.js +125 -44
- package/dist/protocols/openai-compatible-chat.d.ts +12 -0
- package/dist/protocols/openai-compatible-responses.d.ts +2 -2
- package/dist/protocols/openai-images.d.ts +128 -18
- package/dist/protocols/openai-images.js +177 -154
- package/dist/protocols/openai-responses.d.ts +15 -15
- package/dist/protocols/openai-responses.js +5 -6
- package/dist/protocols/openai-speech.d.ts +116 -0
- package/dist/protocols/openai-speech.js +98 -0
- package/dist/protocols/openai-transcription.d.ts +207 -0
- package/dist/protocols/openai-transcription.js +190 -0
- package/dist/protocols/replicate-images.d.ts +28 -0
- package/dist/protocols/replicate-images.js +133 -0
- package/dist/protocols/runway-video.d.ts +38 -0
- package/dist/protocols/runway-video.js +146 -0
- package/dist/protocols/shared.d.ts +27 -17
- package/dist/protocols/shared.js +52 -35
- package/dist/protocols/stability-images.d.ts +38 -0
- package/dist/protocols/stability-images.js +148 -0
- package/dist/protocols/utils/bedrock-media.d.ts +2 -3
- package/dist/protocols/utils/bedrock-media.js +4 -4
- package/dist/protocols/utils/fal-queue.d.ts +28 -0
- package/dist/protocols/utils/fal-queue.js +69 -0
- package/dist/protocols/utils/gemini-generate-content.d.ts +65 -0
- package/dist/protocols/utils/gemini-generate-content.js +65 -0
- package/dist/protocols/utils/gemini-json-schema.d.ts +3 -0
- package/dist/protocols/utils/gemini-json-schema.js +76 -0
- package/dist/protocols/utils/media-input.d.ts +18 -0
- package/dist/protocols/utils/media-input.js +35 -0
- package/dist/protocols/utils/responses-compaction.js +6 -5
- package/dist/protocols/utils/speech-stream.d.ts +49 -0
- package/dist/protocols/utils/speech-stream.js +67 -0
- package/dist/protocols/utils/tool-schema.d.ts +2 -2
- package/dist/protocols/utils/tool-schema.js +40 -17
- 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 +75 -84
- package/dist/protocols/xai-responses.d.ts +2 -2
- package/dist/protocols/xai-video.d.ts +34 -0
- package/dist/protocols/xai-video.js +147 -0
- package/dist/protocols/zai-chat.d.ts +13 -1
- package/dist/protocols/zai-images.d.ts +9 -13
- package/dist/protocols/zai-images.js +59 -57
- package/dist/provider-error.js +3 -0
- package/dist/providers/alibaba.d.ts +14 -2
- package/dist/providers/amazon-bedrock-mantle.d.ts +14 -2
- package/dist/providers/amazon-bedrock.d.ts +2 -2
- package/dist/providers/assemblyai.d.ts +25 -0
- package/dist/providers/assemblyai.js +29 -0
- package/dist/providers/azure.d.ts +18 -6
- package/dist/providers/baseten.d.ts +24 -0
- package/dist/providers/black-forest-labs.d.ts +25 -0
- package/dist/providers/black-forest-labs.js +28 -0
- package/dist/providers/cartesia.d.ts +24 -0
- package/dist/providers/cartesia.js +22 -0
- package/dist/providers/cerebras.d.ts +24 -0
- package/dist/providers/cerebras.js +6 -1
- package/dist/providers/cloudflare-ai-gateway.d.ts +30 -6
- package/dist/providers/cloudflare-workers-ai.d.ts +24 -0
- package/dist/providers/deepgram.d.ts +29 -0
- package/dist/providers/deepgram.js +31 -0
- package/dist/providers/deepinfra.d.ts +24 -0
- package/dist/providers/deepinfra.js +6 -1
- package/dist/providers/deepseek.d.ts +24 -0
- package/dist/providers/elevenlabs.d.ts +24 -0
- package/dist/providers/elevenlabs.js +28 -0
- package/dist/providers/fal.d.ts +29 -0
- package/dist/providers/fal.js +33 -0
- package/dist/providers/fireworks.d.ts +24 -0
- package/dist/providers/google-vertex-chat.d.ts +12 -0
- package/dist/providers/google-vertex-responses.d.ts +2 -2
- package/dist/providers/google-vertex.d.ts +10 -3
- package/dist/providers/google.d.ts +25 -3
- package/dist/providers/google.js +11 -2
- package/dist/providers/groq.d.ts +24 -0
- package/dist/providers/index.d.ts +10 -0
- package/dist/providers/index.js +10 -0
- package/dist/providers/meta.d.ts +14 -2
- package/dist/providers/minimax.d.ts +14 -2
- package/dist/providers/moonshot.d.ts +14 -2
- package/dist/providers/moonshot.js +3 -3
- package/dist/providers/openai-compatible-responses.d.ts +2 -2
- package/dist/providers/openai-compatible.d.ts +12 -0
- package/dist/providers/openai.d.ts +25 -3
- package/dist/providers/openai.js +10 -1
- package/dist/providers/openrouter.d.ts +67 -0
- package/dist/providers/openrouter.js +13 -1
- package/dist/providers/replicate.d.ts +25 -0
- package/dist/providers/replicate.js +22 -0
- package/dist/providers/runway.d.ts +24 -0
- package/dist/providers/runway.js +22 -0
- package/dist/providers/stability.d.ts +28 -0
- package/dist/providers/stability.js +23 -0
- package/dist/providers/togetherai.d.ts +24 -0
- package/dist/providers/vercel-ai-gateway.d.ts +41 -0
- package/dist/providers/vercel-ai-gateway.js +85 -0
- package/dist/providers/xai.d.ts +17 -0
- package/dist/providers/xai.js +5 -2
- package/dist/providers/zai-coding-plan.d.ts +15 -3
- package/dist/providers/zai.d.ts +13 -1
- package/dist/route/auth.d.ts +4 -1
- package/dist/route/auth.js +6 -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/framing.d.ts +5 -1
- package/dist/route/framing.js +9 -0
- package/dist/route/index.d.ts +2 -0
- package/dist/route/index.js +2 -0
- package/dist/route/media-protocol.d.ts +158 -0
- package/dist/route/media-protocol.js +96 -0
- package/dist/route/media.d.ts +97 -0
- package/dist/route/media.js +236 -0
- package/dist/schema/errors.d.ts +13 -3
- package/dist/schema/errors.js +7 -0
- package/dist/schema/events.d.ts +557 -40
- package/dist/schema/events.js +35 -2
- package/dist/schema/messages.d.ts +95 -8
- package/dist/schema/messages.js +8 -6
- package/dist/schema/options.d.ts +6 -3
- package/dist/schema/options.js +6 -2
- package/dist/speech-client.d.ts +21 -0
- package/dist/speech-client.js +25 -0
- package/dist/speech.d.ts +1150 -0
- package/dist/speech.js +119 -0
- package/dist/testing.d.ts +72 -8
- package/dist/transcription-client.d.ts +28 -0
- package/dist/transcription-client.js +44 -0
- package/dist/transcription.d.ts +1504 -0
- package/dist/transcription.js +133 -0
- package/dist/utils/bytes.d.ts +1 -0
- package/dist/utils/bytes.js +10 -0
- package/dist/utils/media-type.d.ts +7 -0
- package/dist/utils/media-type.js +70 -0
- package/dist/utils/sanitize.js +3 -1
- package/dist/video-client.d.ts +28 -0
- package/dist/video-client.js +40 -0
- package/dist/video.d.ts +1359 -0
- package/dist/video.js +119 -0
- package/package.json +7 -3
- package/dist/protocols/utils/gemini-tool-schema.d.ts +0 -2
- package/dist/protocols/utils/gemini-tool-schema.js +0 -103
- package/dist/protocols/utils/image-input.d.ts +0 -21
- package/dist/protocols/utils/image-input.js +0 -20
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,44 @@ 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
|
+
* The one string a provider can address an asset by: inline payloads as a data URL, `url` sources as their URL, and
|
|
139
|
+
* this provider's own `ref` as its id. Other providers' refs are never forwarded and fail typed; omit `provider` for
|
|
140
|
+
* APIs with no file handles at all.
|
|
141
|
+
*/
|
|
142
|
+
export const mediaReference = (asset, provider, label) => {
|
|
143
|
+
const inline = asset.inline();
|
|
144
|
+
if (inline)
|
|
145
|
+
return Effect.succeed({ type: "dataUrl", value: inline.dataUrl });
|
|
146
|
+
const url = mediaUrl(asset);
|
|
147
|
+
if (url)
|
|
148
|
+
return Effect.succeed({ type: "url", value: url });
|
|
149
|
+
if (provider !== undefined && asset.source.type === "ref" && asset.source.provider === provider)
|
|
150
|
+
return Effect.succeed({ type: "ref", value: asset.source.id });
|
|
151
|
+
const accepted = provider === undefined ? "" : `, and ${provider} references`;
|
|
152
|
+
return Effect.fail(invalidRequest(`${label} accepts inline bytes, data URLs, http(s) URLs${accepted}`));
|
|
153
|
+
};
|
|
154
|
+
/**
|
|
155
|
+
* Lift a tool-result file into a `MediaPart`. Tool files carry either a data URL, an `http(s)` URL, or raw base64 in
|
|
156
|
+
* `uri`; the declared `mime` wins over any data-URL prefix so tool authors control the type the model sees.
|
|
157
|
+
*/
|
|
158
|
+
export const toolFileMedia = (item) => {
|
|
159
|
+
const parsed = Media.parseDataUrl(item.uri);
|
|
160
|
+
const asset = parsed
|
|
161
|
+
? Media.from({ ...parsed.source, mediaType: item.mime })
|
|
162
|
+
: /^https?:\/\//.test(item.uri)
|
|
163
|
+
? Media.url(item.uri, { mediaType: item.mime })
|
|
164
|
+
: Media.base64(item.uri, item.mime);
|
|
165
|
+
return Message.media(asset, { filename: item.name });
|
|
138
166
|
};
|
|
139
|
-
export const normalizeToolFile = (part) => normalizeMedia({ type: "media", mediaType: part.mime, data: part.uri, filename: part.name });
|
|
140
167
|
export const trimBaseUrl = (value) => value.replace(/\/+$/, "");
|
|
141
168
|
export const toolResultText = (part) => {
|
|
142
169
|
if (part.result.type === "text")
|
|
@@ -164,11 +191,11 @@ export const errorText = (error) => {
|
|
|
164
191
|
};
|
|
165
192
|
/**
|
|
166
193
|
* `framing` step for Server-Sent Events. Decodes UTF-8, runs the SSE channel
|
|
167
|
-
* decoder, optionally filters named events, and drops empty events
|
|
168
|
-
*
|
|
169
|
-
* boundary. Retry control events are ignored without
|
|
170
|
-
* Decoder failures become provider output errors so
|
|
171
|
-
* stays `AIError`.
|
|
194
|
+
* decoder, optionally filters named events, and drops empty events and known
|
|
195
|
+
* keepalives that proxies send as data. `[DONE]` is dropped by default or
|
|
196
|
+
* retained for protocols that use it as their stream boundary. Retry control events are ignored without
|
|
197
|
+
* interrupting the stream. Decoder failures become provider output errors so
|
|
198
|
+
* the public error channel stays `AIError`.
|
|
172
199
|
*/
|
|
173
200
|
export const sseFraming = (bytes, events, includeDone = false) => bytes.pipe(Stream.decodeText(), Stream.mapAccumEffect(() => {
|
|
174
201
|
const output = [];
|
|
@@ -186,6 +213,13 @@ export const sseFraming = (bytes, events, includeDone = false) => bytes.pipe(Str
|
|
|
186
213
|
return [state, state.output.splice(0)];
|
|
187
214
|
})), Stream.filter((event) => (events === undefined || events.has(event.event)) &&
|
|
188
215
|
event.data.length > 0 &&
|
|
216
|
+
// Some OpenAI-compatible proxies serialize an empty flush as a bare
|
|
217
|
+
// `data: null`, between events or after `[DONE]`. No protocol has a
|
|
218
|
+
// null event, so it carries nothing and must not abort the stream.
|
|
219
|
+
event.data !== "null" &&
|
|
220
|
+
// Vertex AI partner models (e.g. `xai/grok-4.6`) send their SSE
|
|
221
|
+
// keepalive comment as `data: : keepalive` while reasoning.
|
|
222
|
+
event.data !== ": keepalive" &&
|
|
189
223
|
(event.data !== "[DONE]" || includeDone || (events !== undefined && event.event !== "message"))), Stream.map((event) => event.data));
|
|
190
224
|
/**
|
|
191
225
|
* Canonical invalid-request constructor shared by protocol lowering.
|
|
@@ -241,23 +275,6 @@ export const flattenToolRequest = (request) => {
|
|
|
241
275
|
: LLMRequest.update(request, { messages }),
|
|
242
276
|
};
|
|
243
277
|
};
|
|
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
278
|
export const matchToolChoice = (route, toolChoice, cases) => Effect.gen(function* () {
|
|
262
279
|
if (toolChoice.type === "auto")
|
|
263
280
|
return cases.auto();
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { ImageModel, ImageResponse, type ImageRequestFor } from "../image.js";
|
|
3
|
+
import { MediaProtocol } from "../route/media-protocol.js";
|
|
4
|
+
import { MediaRoute } from "../route/media.js";
|
|
5
|
+
export declare const DEFAULT_BASE_URL = "https://api.stability.ai";
|
|
6
|
+
export type StabilityStylePreset = "enhance" | "anime" | "photographic" | "digital-art" | "comic-book" | "fantasy-art" | "line-art" | "analog-film" | "neon-punk" | "isometric" | "low-poly" | "origami" | "modeling-compound" | "cinematic" | "3d-model" | "pixel-art" | "tile-texture" | (string & {});
|
|
7
|
+
export type StabilityImageOptions = {
|
|
8
|
+
readonly negative_prompt?: string;
|
|
9
|
+
readonly style_preset?: StabilityStylePreset;
|
|
10
|
+
readonly strength?: number;
|
|
11
|
+
readonly cfg_scale?: number;
|
|
12
|
+
} & Record<string, unknown>;
|
|
13
|
+
/** Creative upscale options; the one `images` source is the image to upscale. */
|
|
14
|
+
export type StabilityUpscaleOptions = {
|
|
15
|
+
readonly negative_prompt?: string;
|
|
16
|
+
readonly style_preset?: StabilityStylePreset;
|
|
17
|
+
readonly creativity?: number;
|
|
18
|
+
} & Record<string, unknown>;
|
|
19
|
+
export type Request = ImageRequestFor<StabilityImageOptions>;
|
|
20
|
+
export type UpscaleRequest = ImageRequestFor<StabilityUpscaleOptions>;
|
|
21
|
+
export declare const Token: Schema.Struct<{
|
|
22
|
+
readonly id: Schema.String;
|
|
23
|
+
}>;
|
|
24
|
+
export type Token = Schema.Schema.Type<typeof Token>;
|
|
25
|
+
export declare const protocol: MediaProtocol.Inline<Request, ImageResponse>;
|
|
26
|
+
export declare const upscaleProtocol: MediaProtocol.Queued<UpscaleRequest, ImageResponse, {
|
|
27
|
+
readonly id: string;
|
|
28
|
+
}>;
|
|
29
|
+
export declare const model: (input: MediaRoute.ModelInput) => ImageModel<StabilityImageOptions>;
|
|
30
|
+
export declare const upscaleModel: (input: Omit<MediaRoute.ModelInput, "id">) => ImageModel<StabilityUpscaleOptions>;
|
|
31
|
+
export declare const StabilityImages: {
|
|
32
|
+
readonly protocol: MediaProtocol.Inline<Request, ImageResponse>;
|
|
33
|
+
readonly upscaleProtocol: MediaProtocol.Queued<UpscaleRequest, ImageResponse, {
|
|
34
|
+
readonly id: string;
|
|
35
|
+
}>;
|
|
36
|
+
readonly model: (input: MediaRoute.ModelInput) => ImageModel<StabilityImageOptions>;
|
|
37
|
+
readonly upscaleModel: (input: Omit<MediaRoute.ModelInput, "id">) => ImageModel<StabilityUpscaleOptions>;
|
|
38
|
+
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { ImageModel, ImageResponse } from "../image.js";
|
|
3
|
+
import { MediaProtocol } from "../route/media-protocol.js";
|
|
4
|
+
import { MediaRoute } from "../route/media.js";
|
|
5
|
+
import { ProviderID, mergeJsonRecords } from "../schema/index.js";
|
|
6
|
+
import { ProviderShared } from "./shared.js";
|
|
7
|
+
import { MediaInput } from "./utils/media-input.js";
|
|
8
|
+
const ADAPTER = "stability-images";
|
|
9
|
+
const UPSCALE_ADAPTER = "stability-upscale";
|
|
10
|
+
const NAME = "Stability AI";
|
|
11
|
+
const PROVIDER = ProviderID.make("stability");
|
|
12
|
+
export const DEFAULT_BASE_URL = "https://api.stability.ai";
|
|
13
|
+
const RESULTS_PATH = "/v2beta/results";
|
|
14
|
+
const UPSCALE_MODEL = "creative";
|
|
15
|
+
/** Base64 JSON instead of raw bytes, so the seed and finish reason arrive with the image. */
|
|
16
|
+
const HEADERS = { accept: "application/json" };
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// 2. Token and response schemas
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
export const Token = Schema.Struct({ id: Schema.String });
|
|
21
|
+
// Generate names the base64 image `image`; `/v2beta/results/{id}` names it `result`.
|
|
22
|
+
const ImageDocument = Schema.Struct({
|
|
23
|
+
image: Schema.optional(Schema.String),
|
|
24
|
+
result: Schema.optional(Schema.String),
|
|
25
|
+
seed: Schema.optional(Schema.Number),
|
|
26
|
+
finish_reason: Schema.optional(Schema.String),
|
|
27
|
+
});
|
|
28
|
+
const Started = Schema.Struct({ id: Schema.String });
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// 5. Request body construction
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
/** `sd3.5-*` ids share the `sd3` endpoint and select the model with a form field. */
|
|
33
|
+
const endpoint = (model) => (model.startsWith("sd3") ? "sd3" : model);
|
|
34
|
+
const RESERVED_FORM_FIELDS = new Set(["image", "prompt", "mode", "model"]);
|
|
35
|
+
const unsupported = (route, operation, message) => ProviderShared.unsupportedOperation({ operation, provider: PROVIDER, route, message });
|
|
36
|
+
const form = Effect.fn("StabilityImages.form")(function* (route, fields, native, source) {
|
|
37
|
+
if ((source?.length ?? 0) > 1)
|
|
38
|
+
return yield* unsupported(route, "media.images", `${NAME} takes one source image`);
|
|
39
|
+
const body = new FormData();
|
|
40
|
+
const overlay = Object.entries(native ?? {}).filter(([key]) => !RESERVED_FORM_FIELDS.has(key));
|
|
41
|
+
Object.entries(mergeJsonRecords(fields, Object.fromEntries(overlay)) ?? {}).forEach(([key, value]) => body.append(key, typeof value === "string" ? value : ProviderShared.encodeJson(value)));
|
|
42
|
+
const image = source?.[0];
|
|
43
|
+
if (image !== undefined)
|
|
44
|
+
body.append("image", MediaInput.blob(yield* MediaInput.inlineBytes(route, image), image.mediaType), "image");
|
|
45
|
+
return MediaProtocol.multipart(body);
|
|
46
|
+
});
|
|
47
|
+
const fromRequest = Effect.fn("StabilityImages.fromRequest")(function* (request) {
|
|
48
|
+
if (request.n !== undefined && request.n > 1)
|
|
49
|
+
return yield* unsupported(ADAPTER, "media.n", `${NAME} generates one image per request; call it once per image`);
|
|
50
|
+
const target = endpoint(request.model.id);
|
|
51
|
+
const edit = (request.images?.length ?? 0) > 0;
|
|
52
|
+
if (edit && target === "core")
|
|
53
|
+
return yield* unsupported(ADAPTER, "media.images", `${NAME} core is text-to-image only; use ultra or sd3.5-*`);
|
|
54
|
+
return yield* form(ADAPTER, {
|
|
55
|
+
prompt: request.prompt,
|
|
56
|
+
aspect_ratio: request.aspectRatio,
|
|
57
|
+
seed: request.seed,
|
|
58
|
+
output_format: request.format,
|
|
59
|
+
model: target === "sd3" ? request.model.id : undefined,
|
|
60
|
+
mode: target === "sd3" && edit ? "image-to-image" : undefined,
|
|
61
|
+
}, mergeJsonRecords(request.providerOptions, request.http?.body), request.images);
|
|
62
|
+
});
|
|
63
|
+
const fromUpscaleRequest = Effect.fn("StabilityImages.fromUpscaleRequest")(function* (request) {
|
|
64
|
+
if ((request.images?.length ?? 0) === 0)
|
|
65
|
+
return yield* ProviderShared.invalidRequest(`${NAME} upscale requires the source image in images`);
|
|
66
|
+
return yield* form(UPSCALE_ADAPTER, { prompt: request.prompt, seed: request.seed, output_format: request.format }, mergeJsonRecords(request.providerOptions, request.http?.body), request.images);
|
|
67
|
+
});
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// 6. Response decoding
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
const decodeImageDocument = (route) => {
|
|
72
|
+
const decode = MediaProtocol.decodeJson(route, NAME, ImageDocument);
|
|
73
|
+
return Effect.fn("StabilityImages.decodeImage")(function* (response) {
|
|
74
|
+
const output = yield* decode(response);
|
|
75
|
+
const document = output.value;
|
|
76
|
+
const data = document.image ?? document.result;
|
|
77
|
+
if (data === undefined)
|
|
78
|
+
return yield* output.invalid(`${NAME} returned no image`);
|
|
79
|
+
const image = yield* MediaInput.decodedAsset(output.invalid, `${NAME} result`, data, undefined);
|
|
80
|
+
return new ImageResponse({
|
|
81
|
+
images: [image],
|
|
82
|
+
notices: document.finish_reason === "CONTENT_FILTERED"
|
|
83
|
+
? [{ type: "moderated", message: `${NAME} blurred the image for violating its content policy` }]
|
|
84
|
+
: undefined,
|
|
85
|
+
providerMetadata: { stability: { seed: document.seed, finishReason: document.finish_reason } },
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
const decodeResponse = decodeImageDocument(ADAPTER);
|
|
90
|
+
const decodeUpscaleImage = decodeImageDocument(UPSCALE_ADAPTER);
|
|
91
|
+
const decodeStart = MediaProtocol.decodeStarted(UPSCALE_ADAPTER, NAME, Started, (value) => ({
|
|
92
|
+
token: { id: value.id },
|
|
93
|
+
snapshot: { id: value.id, status: "queued" },
|
|
94
|
+
}));
|
|
95
|
+
// `/v2beta/results/{id}` answers 202 while in progress and 200 with the finished image document.
|
|
96
|
+
const decodeStatus = (response, context) => Effect.succeed({
|
|
97
|
+
id: context.token.id,
|
|
98
|
+
status: response.status === 202 ? "running" : "completed",
|
|
99
|
+
});
|
|
100
|
+
const decodeUpscaleResult = Effect.fn("StabilityImages.decodeUpscaleResult")(function* (response, context) {
|
|
101
|
+
if (response.status === 202) {
|
|
102
|
+
const output = yield* MediaProtocol.text(UPSCALE_ADAPTER, NAME, response);
|
|
103
|
+
return yield* output.invalid(`${NAME} upscale ${context.token.id} has not finished`);
|
|
104
|
+
}
|
|
105
|
+
return yield* decodeUpscaleImage(response);
|
|
106
|
+
});
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// 7. Protocol and route
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
export const protocol = MediaProtocol.inline({
|
|
111
|
+
id: ADAPTER,
|
|
112
|
+
name: NAME,
|
|
113
|
+
unsupported: ["size", "mask"],
|
|
114
|
+
body: { from: fromRequest },
|
|
115
|
+
response: { decode: decodeResponse },
|
|
116
|
+
});
|
|
117
|
+
export const upscaleProtocol = MediaProtocol.queued({
|
|
118
|
+
id: UPSCALE_ADAPTER,
|
|
119
|
+
name: NAME,
|
|
120
|
+
token: Token,
|
|
121
|
+
unsupported: ["n", "size", "aspectRatio", "mask"],
|
|
122
|
+
start: { body: { from: fromUpscaleRequest }, decode: decodeStart },
|
|
123
|
+
status: { path: (token) => `${RESULTS_PATH}/${token.id}`, decode: decodeStatus },
|
|
124
|
+
result: { path: (token) => `${RESULTS_PATH}/${token.id}`, decode: decodeUpscaleResult },
|
|
125
|
+
});
|
|
126
|
+
export const model = (input) => ImageModel.fromRoute({
|
|
127
|
+
id: ADAPTER,
|
|
128
|
+
provider: PROVIDER,
|
|
129
|
+
protocol,
|
|
130
|
+
baseURL: DEFAULT_BASE_URL,
|
|
131
|
+
headers: HEADERS,
|
|
132
|
+
path: ({ request }) => `/v2beta/stable-image/generate/${endpoint(request.model.id)}`,
|
|
133
|
+
}, input);
|
|
134
|
+
export const upscaleModel = (input) => ImageModel.fromRoute({
|
|
135
|
+
id: UPSCALE_ADAPTER,
|
|
136
|
+
provider: PROVIDER,
|
|
137
|
+
protocol: upscaleProtocol,
|
|
138
|
+
baseURL: DEFAULT_BASE_URL,
|
|
139
|
+
headers: HEADERS,
|
|
140
|
+
// Only the creative upscaler is asynchronous; fast and conservative answer inline.
|
|
141
|
+
path: `/v2beta/stable-image/upscale/${UPSCALE_MODEL}`,
|
|
142
|
+
}, { ...input, id: UPSCALE_MODEL });
|
|
143
|
+
export const StabilityImages = {
|
|
144
|
+
protocol,
|
|
145
|
+
upscaleProtocol,
|
|
146
|
+
model,
|
|
147
|
+
upscaleModel,
|
|
148
|
+
};
|
|
@@ -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,28 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import type { HttpClientResponse } from "effect/unstable/http";
|
|
3
|
+
import type { Media } from "../../media.js";
|
|
4
|
+
import { MediaProtocol } from "../../route/media-protocol.js";
|
|
5
|
+
import type { AIError } from "../../schema/index.js";
|
|
6
|
+
export declare const DEFAULT_BASE_URL = "https://queue.fal.run";
|
|
7
|
+
/** fal hands back absolute follow-up URLs on submit; they are authoritative for status, result, and cancel. */
|
|
8
|
+
export declare const Token: Schema.Struct<{
|
|
9
|
+
readonly requestID: Schema.String;
|
|
10
|
+
readonly statusURL: Schema.String;
|
|
11
|
+
readonly responseURL: Schema.String;
|
|
12
|
+
readonly cancelURL: Schema.String;
|
|
13
|
+
}>;
|
|
14
|
+
export type Token = Schema.Schema.Type<typeof Token>;
|
|
15
|
+
export declare const mediaUrl: (asset: Media.Asset, name: string) => Effect.Effect<string, AIError, never>;
|
|
16
|
+
export declare const protocol: <Request, Response>(input: {
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly name: string;
|
|
19
|
+
readonly unsupported?: ReadonlyArray<keyof Request & string>;
|
|
20
|
+
readonly from: (request: Request) => Effect.Effect<MediaProtocol.Body, AIError>;
|
|
21
|
+
readonly decodeResult: (response: HttpClientResponse.HttpClientResponse, context: MediaProtocol.PollContext<Token>) => Effect.Effect<Response, AIError>;
|
|
22
|
+
}) => MediaProtocol.Queued<Request, Response, {
|
|
23
|
+
readonly requestID: string;
|
|
24
|
+
readonly statusURL: string;
|
|
25
|
+
readonly responseURL: string;
|
|
26
|
+
readonly cancelURL: string;
|
|
27
|
+
}>;
|
|
28
|
+
export * as FalQueue from "./fal-queue.js";
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { MediaProtocol } from "../../route/media-protocol.js";
|
|
3
|
+
import { ProviderShared, optionalNull } from "../shared.js";
|
|
4
|
+
export const DEFAULT_BASE_URL = "https://queue.fal.run";
|
|
5
|
+
/** fal hands back absolute follow-up URLs on submit; they are authoritative for status, result, and cancel. */
|
|
6
|
+
export const Token = Schema.Struct({
|
|
7
|
+
requestID: Schema.String,
|
|
8
|
+
statusURL: Schema.String,
|
|
9
|
+
responseURL: Schema.String,
|
|
10
|
+
cancelURL: Schema.String,
|
|
11
|
+
});
|
|
12
|
+
const StartResponse = Schema.Struct({
|
|
13
|
+
request_id: Schema.String,
|
|
14
|
+
status_url: Schema.String,
|
|
15
|
+
response_url: Schema.String,
|
|
16
|
+
cancel_url: Schema.String,
|
|
17
|
+
queue_position: optionalNull(Schema.Number),
|
|
18
|
+
});
|
|
19
|
+
const QueueStatus = Schema.Struct({
|
|
20
|
+
status: Schema.String,
|
|
21
|
+
queue_position: optionalNull(Schema.Number),
|
|
22
|
+
error: optionalNull(Schema.Unknown),
|
|
23
|
+
});
|
|
24
|
+
const STATUS = {
|
|
25
|
+
IN_QUEUE: "queued",
|
|
26
|
+
IN_PROGRESS: "running",
|
|
27
|
+
COMPLETED: "completed",
|
|
28
|
+
};
|
|
29
|
+
// fal accepts public URLs and data URIs; there is no provider file handle to forward.
|
|
30
|
+
export const mediaUrl = (asset, name) => ProviderShared.mediaReference(asset, undefined, name).pipe(Effect.map((reference) => reference.value));
|
|
31
|
+
export const protocol = (input) => {
|
|
32
|
+
const decodeQueueStatus = MediaProtocol.decodeJson(input.id, input.name, QueueStatus);
|
|
33
|
+
return MediaProtocol.queued({
|
|
34
|
+
id: input.id,
|
|
35
|
+
name: input.name,
|
|
36
|
+
token: Token,
|
|
37
|
+
unsupported: input.unsupported,
|
|
38
|
+
start: {
|
|
39
|
+
body: { from: input.from },
|
|
40
|
+
decode: MediaProtocol.decodeStarted(input.id, input.name, StartResponse, (value) => ({
|
|
41
|
+
token: {
|
|
42
|
+
requestID: value.request_id,
|
|
43
|
+
statusURL: value.status_url,
|
|
44
|
+
responseURL: value.response_url,
|
|
45
|
+
cancelURL: value.cancel_url,
|
|
46
|
+
},
|
|
47
|
+
snapshot: { id: value.request_id, status: "queued", position: value.queue_position ?? undefined },
|
|
48
|
+
})),
|
|
49
|
+
},
|
|
50
|
+
status: {
|
|
51
|
+
path: (token) => token.statusURL,
|
|
52
|
+
decode: Effect.fn("FalQueue.decodeStatus")(function* (response, context) {
|
|
53
|
+
const output = yield* decodeQueueStatus(response);
|
|
54
|
+
const decoded = output.value;
|
|
55
|
+
const status = yield* MediaProtocol.status(STATUS, decoded.status, output);
|
|
56
|
+
// fal reports request failures as COMPLETED with an `error`; the response endpoint carries the details.
|
|
57
|
+
const failed = status === "completed" && decoded.error !== undefined && decoded.error !== null;
|
|
58
|
+
return {
|
|
59
|
+
id: context.token.requestID,
|
|
60
|
+
status: failed ? "failed" : status,
|
|
61
|
+
position: status === "queued" ? (decoded.queue_position ?? undefined) : undefined,
|
|
62
|
+
};
|
|
63
|
+
}),
|
|
64
|
+
},
|
|
65
|
+
result: { path: (token) => token.responseURL, decode: input.decodeResult },
|
|
66
|
+
cancel: { method: "PUT", path: (token) => token.cancelURL },
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
export * as FalQueue from "./fal-queue.js";
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Effect, Schema, type Stream } from "effect";
|
|
2
|
+
import type { Media } from "../../media.js";
|
|
3
|
+
import type { MediaProtocol } from "../../route/media-protocol.js";
|
|
4
|
+
import { AIError, type MediaUsage, type ProviderMetadata } from "../../schema/index.js";
|
|
5
|
+
declare const UsageMetadata: Schema.Struct<{
|
|
6
|
+
readonly promptTokenCount: Schema.optional<Schema.Number>;
|
|
7
|
+
readonly candidatesTokenCount: Schema.optional<Schema.Number>;
|
|
8
|
+
readonly totalTokenCount: Schema.optional<Schema.Number>;
|
|
9
|
+
}>;
|
|
10
|
+
type UsageMetadata = Schema.Schema.Type<typeof UsageMetadata>;
|
|
11
|
+
export declare const chunk: <const Part extends Schema.Top>(part: Part) => Schema.Struct<{
|
|
12
|
+
readonly candidates: Schema.optional<Schema.$Array<Schema.Struct<{
|
|
13
|
+
readonly content: Schema.optional<Schema.Struct<{
|
|
14
|
+
readonly parts: Schema.optional<Schema.$Array<Part>>;
|
|
15
|
+
}>>;
|
|
16
|
+
readonly finishReason: Schema.optional<Schema.String>;
|
|
17
|
+
}>>>;
|
|
18
|
+
readonly promptFeedback: Schema.optional<Schema.Struct<{
|
|
19
|
+
readonly blockReason: Schema.optional<Schema.String>;
|
|
20
|
+
readonly blockReasonMessage: Schema.optional<Schema.String>;
|
|
21
|
+
}>>;
|
|
22
|
+
readonly usageMetadata: Schema.optional<Schema.Struct<{
|
|
23
|
+
readonly promptTokenCount: Schema.optional<Schema.Number>;
|
|
24
|
+
readonly candidatesTokenCount: Schema.optional<Schema.Number>;
|
|
25
|
+
readonly totalTokenCount: Schema.optional<Schema.Number>;
|
|
26
|
+
}>>;
|
|
27
|
+
readonly modelVersion: Schema.optional<Schema.String>;
|
|
28
|
+
readonly responseId: Schema.optional<Schema.String>;
|
|
29
|
+
}>;
|
|
30
|
+
interface Chunk {
|
|
31
|
+
readonly candidates?: ReadonlyArray<{
|
|
32
|
+
readonly finishReason?: string;
|
|
33
|
+
}>;
|
|
34
|
+
readonly promptFeedback?: {
|
|
35
|
+
readonly blockReason?: string;
|
|
36
|
+
readonly blockReasonMessage?: string;
|
|
37
|
+
};
|
|
38
|
+
readonly usageMetadata?: UsageMetadata;
|
|
39
|
+
readonly modelVersion?: string;
|
|
40
|
+
readonly responseId?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface Metadata {
|
|
43
|
+
readonly usage?: UsageMetadata;
|
|
44
|
+
readonly finishReason?: string;
|
|
45
|
+
readonly modelVersion?: string;
|
|
46
|
+
readonly responseId?: string;
|
|
47
|
+
}
|
|
48
|
+
export declare const track: <State extends Metadata>(state: State, chunk: Chunk) => State;
|
|
49
|
+
export declare const blocked: (name: string, chunk: Chunk, frame: string) => AIError | undefined;
|
|
50
|
+
export declare const usage: (usage: UsageMetadata | undefined) => MediaUsage | undefined;
|
|
51
|
+
export declare const providerMetadata: (state: Metadata) => ProviderMetadata;
|
|
52
|
+
export declare const path: (model: string, mode: MediaProtocol.Mode) => string;
|
|
53
|
+
export declare const frames: (bytes: Stream.Stream<Uint8Array, AIError>, mode: MediaProtocol.Mode) => Stream.Stream<string, AIError, never>;
|
|
54
|
+
export declare const mediaPart: (route: string, asset: Media.Asset) => Effect.Effect<{
|
|
55
|
+
readonly fileData: {
|
|
56
|
+
readonly mimeType: string;
|
|
57
|
+
readonly fileUri: string;
|
|
58
|
+
};
|
|
59
|
+
} | {
|
|
60
|
+
readonly inlineData: {
|
|
61
|
+
readonly mimeType: string;
|
|
62
|
+
readonly data: string;
|
|
63
|
+
};
|
|
64
|
+
}, AIError>;
|
|
65
|
+
export * as GeminiGenerateContent from "./gemini-generate-content.js";
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { Framing } from "../../route/framing.js";
|
|
3
|
+
import { AIError, ContentPolicyError, ProviderID } from "../../schema/index.js";
|
|
4
|
+
import { ProviderShared } from "../shared.js";
|
|
5
|
+
import { MediaInput } from "./media-input.js";
|
|
6
|
+
const PROVIDER = ProviderID.make("google");
|
|
7
|
+
const UsageMetadata = Schema.Struct({
|
|
8
|
+
promptTokenCount: Schema.optional(Schema.Number),
|
|
9
|
+
candidatesTokenCount: Schema.optional(Schema.Number),
|
|
10
|
+
totalTokenCount: Schema.optional(Schema.Number),
|
|
11
|
+
});
|
|
12
|
+
export const chunk = (part) => Schema.Struct({
|
|
13
|
+
candidates: Schema.optional(Schema.Array(Schema.Struct({
|
|
14
|
+
content: Schema.optional(Schema.Struct({ parts: Schema.optional(Schema.Array(part)) })),
|
|
15
|
+
finishReason: Schema.optional(Schema.String),
|
|
16
|
+
}))),
|
|
17
|
+
promptFeedback: Schema.optional(Schema.Struct({
|
|
18
|
+
blockReason: Schema.optional(Schema.String),
|
|
19
|
+
blockReasonMessage: Schema.optional(Schema.String),
|
|
20
|
+
})),
|
|
21
|
+
usageMetadata: Schema.optional(UsageMetadata),
|
|
22
|
+
modelVersion: Schema.optional(Schema.String),
|
|
23
|
+
responseId: Schema.optional(Schema.String),
|
|
24
|
+
});
|
|
25
|
+
export const track = (state, chunk) => ({
|
|
26
|
+
...state,
|
|
27
|
+
usage: chunk.usageMetadata ?? state.usage,
|
|
28
|
+
finishReason: chunk.candidates?.[0]?.finishReason ?? state.finishReason,
|
|
29
|
+
modelVersion: chunk.modelVersion ?? state.modelVersion,
|
|
30
|
+
responseId: chunk.responseId ?? state.responseId,
|
|
31
|
+
});
|
|
32
|
+
export const blocked = (name, chunk, frame) => {
|
|
33
|
+
const feedback = chunk.promptFeedback;
|
|
34
|
+
if (feedback?.blockReason === undefined)
|
|
35
|
+
return undefined;
|
|
36
|
+
return new AIError({
|
|
37
|
+
reason: new ContentPolicyError({
|
|
38
|
+
message: `${name} blocked the request (${feedback.blockReason})${feedback.blockReasonMessage === undefined ? "" : `: ${feedback.blockReasonMessage}`}`,
|
|
39
|
+
body: frame,
|
|
40
|
+
}),
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
export const usage = (usage) => usage === undefined
|
|
44
|
+
? undefined
|
|
45
|
+
: {
|
|
46
|
+
type: "tokens",
|
|
47
|
+
input: usage.promptTokenCount,
|
|
48
|
+
output: usage.candidatesTokenCount,
|
|
49
|
+
total: ProviderShared.totalTokens(usage.promptTokenCount, usage.candidatesTokenCount, usage.totalTokenCount),
|
|
50
|
+
details: { google: usage },
|
|
51
|
+
};
|
|
52
|
+
export const providerMetadata = (state) => ({
|
|
53
|
+
google: { finishReason: state.finishReason, modelVersion: state.modelVersion, responseId: state.responseId },
|
|
54
|
+
});
|
|
55
|
+
export const path = (model, mode) => mode === "stream" ? `/models/${model}:streamGenerateContent?alt=sse` : `/models/${model}:generateContent`;
|
|
56
|
+
// `generateContent` answers with one document shaped exactly like a streamed chunk, so it is a single frame.
|
|
57
|
+
export const frames = (bytes, mode) => mode === "stream" ? Framing.sse.frame(bytes) : Framing.document.frame(bytes);
|
|
58
|
+
// Gemini does not fetch public URLs; inline payloads and Gemini Files references are the accepted inputs.
|
|
59
|
+
export const mediaPart = (route, asset) => {
|
|
60
|
+
const fileUri = MediaInput.refID(asset, PROVIDER);
|
|
61
|
+
if (fileUri !== undefined)
|
|
62
|
+
return Effect.succeed({ fileData: { mimeType: asset.mediaType, fileUri } });
|
|
63
|
+
return ProviderShared.requireInlineMedia(route, asset).pipe(Effect.map((media) => ({ inlineData: { mimeType: media.mime, data: media.base64 } })));
|
|
64
|
+
};
|
|
65
|
+
export * as GeminiGenerateContent from "./gemini-generate-content.js";
|