@opencode/ai 2.0.15 → 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 +286 -2
- package/dist/generation.d.ts +36 -22
- package/dist/generation.js +53 -24
- package/dist/image-client.d.ts +14 -7
- package/dist/image-client.js +24 -8
- package/dist/image.d.ts +398 -47
- package/dist/image.js +47 -45
- package/dist/index.d.ts +13 -1
- package/dist/index.js +9 -0
- package/dist/media-model.d.ts +44 -0
- package/dist/media-model.js +49 -0
- package/dist/media.d.ts +10 -9
- package/dist/media.js +9 -10
- package/dist/promise.d.ts +428 -8
- package/dist/promise.js +40 -3
- package/dist/protocols/alibaba-chat.d.ts +12 -0
- package/dist/protocols/alibaba-responses.d.ts +2 -2
- package/dist/protocols/anthropic-messages.js +1 -2
- package/dist/protocols/assemblyai-transcription.d.ts +40 -0
- package/dist/protocols/assemblyai-transcription.js +138 -0
- package/dist/protocols/bedrock-converse.js +5 -11
- 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 +9 -9
- package/dist/protocols/gemini.js +8 -34
- package/dist/protocols/google-images.js +2 -14
- 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.js +2 -9
- package/dist/protocols/meta-responses.d.ts +4 -4
- package/dist/protocols/meta-responses.js +1 -1
- package/dist/protocols/open-responses.d.ts +6 -6
- package/dist/protocols/open-responses.js +1 -2
- package/dist/protocols/openai-chat.d.ts +84 -0
- package/dist/protocols/openai-chat.js +26 -14
- 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 +124 -3
- package/dist/protocols/openai-images.js +107 -54
- 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 +13 -3
- package/dist/protocols/shared.js +23 -3
- package/dist/protocols/stability-images.d.ts +38 -0
- package/dist/protocols/stability-images.js +148 -0
- 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 +8 -0
- package/dist/protocols/utils/media-input.js +18 -0
- 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/xai-images.js +1 -12
- 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/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/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/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/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 +3 -3
- package/dist/providers/google.d.ts +18 -3
- package/dist/providers/google.js +11 -2
- package/dist/providers/groq.d.ts +24 -0
- package/dist/providers/index.d.ts +9 -0
- package/dist/providers/index.js +9 -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 +48 -0
- 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/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/framing.d.ts +5 -1
- package/dist/route/framing.js +9 -0
- package/dist/route/media-protocol.d.ts +116 -3
- package/dist/route/media-protocol.js +60 -4
- package/dist/route/media.d.ts +58 -7
- package/dist/route/media.js +201 -29
- package/dist/schema/events.d.ts +0 -6
- package/dist/schema/messages.d.ts +0 -3
- package/dist/schema/options.d.ts +4 -3
- package/dist/schema/options.js +3 -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/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 +1 -0
- package/dist/utils/media-type.js +22 -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 +3 -3
- package/dist/protocols/utils/gemini-tool-schema.d.ts +0 -2
- package/dist/protocols/utils/gemini-tool-schema.js +0 -103
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { Duration, Effect, Schema } from "effect";
|
|
2
|
+
import { Media } from "../media.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 { VideoModel, VideoResponse } from "../video.js";
|
|
7
|
+
import { ProviderShared, optionalArray, optionalNull } from "./shared.js";
|
|
8
|
+
const ADAPTER = "runway-video";
|
|
9
|
+
const NAME = "Runway";
|
|
10
|
+
const PROVIDER = ProviderID.make("runway");
|
|
11
|
+
export const DEFAULT_BASE_URL = "https://api.dev.runwayml.com/v1";
|
|
12
|
+
/** Every Runway request must pin the API version. */
|
|
13
|
+
export const API_VERSION = "2024-11-06";
|
|
14
|
+
export const TEXT_TO_VIDEO_PATH = "/text_to_video";
|
|
15
|
+
export const IMAGE_TO_VIDEO_PATH = "/image_to_video";
|
|
16
|
+
export const VIDEO_TO_VIDEO_PATH = "/video_to_video";
|
|
17
|
+
export const TASKS_PATH = "/tasks";
|
|
18
|
+
/** Output URLs are valid for 24–48 hours; the asset carries the conservative bound. */
|
|
19
|
+
const OUTPUT_RETENTION = Duration.hours(24);
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// 2. Token and response schemas
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
export const Token = Schema.Struct({ taskID: Schema.String });
|
|
24
|
+
const Cost = Schema.Struct({ credits: Schema.Number });
|
|
25
|
+
const StartResponse = Schema.Struct({ id: Schema.String });
|
|
26
|
+
const Task = Schema.Struct({
|
|
27
|
+
status: Schema.String,
|
|
28
|
+
progress: optionalNull(Schema.Number),
|
|
29
|
+
output: optionalArray(Schema.String),
|
|
30
|
+
failure: optionalNull(Schema.String),
|
|
31
|
+
failureCode: optionalNull(Schema.String),
|
|
32
|
+
cost: Schema.optional(Cost),
|
|
33
|
+
estimatedCost: Schema.optional(Cost),
|
|
34
|
+
});
|
|
35
|
+
const STATUS = {
|
|
36
|
+
PENDING: "queued",
|
|
37
|
+
THROTTLED: "queued",
|
|
38
|
+
RUNNING: "running",
|
|
39
|
+
SUCCEEDED: "completed",
|
|
40
|
+
FAILED: "failed",
|
|
41
|
+
CANCELLED: "cancelled",
|
|
42
|
+
};
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// 5. Request body construction
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Runway accepts HTTPS URLs, `runway://` upload URIs, and data URIs, all as one string.
|
|
47
|
+
const mediaUri = (asset) => ProviderShared.mediaReference(asset, PROVIDER, NAME).pipe(Effect.map((reference) => reference.value));
|
|
48
|
+
const fromRequest = Effect.fn("RunwayVideo.fromRequest")(function* (request) {
|
|
49
|
+
const first = request.frames?.first === undefined ? undefined : yield* mediaUri(request.frames.first);
|
|
50
|
+
const last = request.frames?.last === undefined ? undefined : yield* mediaUri(request.frames.last);
|
|
51
|
+
const promptImage = [
|
|
52
|
+
...(first === undefined ? [] : [{ uri: first, position: "first" }]),
|
|
53
|
+
...(last === undefined ? [] : [{ uri: last, position: "last" }]),
|
|
54
|
+
];
|
|
55
|
+
const videoUri = request.video === undefined ? undefined : yield* mediaUri(request.video);
|
|
56
|
+
const references = yield* Effect.forEach(request.references ?? [], (asset) => mediaUri(asset).pipe(Effect.map((uri) => ({ uri }))));
|
|
57
|
+
return MediaProtocol.json(mergeJsonRecords({
|
|
58
|
+
model: request.model.id,
|
|
59
|
+
promptText: request.prompt,
|
|
60
|
+
promptImage: promptImage.length === 0 ? undefined : promptImage,
|
|
61
|
+
videoUri,
|
|
62
|
+
references: references.length === 0 ? undefined : references,
|
|
63
|
+
ratio: request.aspectRatio,
|
|
64
|
+
duration: request.durationSeconds,
|
|
65
|
+
resolution: request.resolution,
|
|
66
|
+
audio: request.audio,
|
|
67
|
+
negativePrompt: request.negativePrompt,
|
|
68
|
+
seed: request.seed,
|
|
69
|
+
}, request.providerOptions, request.http?.body) ?? {});
|
|
70
|
+
});
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// 6. Response decoding
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
const decodeStart = MediaProtocol.decodeStarted(ADAPTER, NAME, StartResponse, (value) => ({
|
|
75
|
+
token: { taskID: value.id },
|
|
76
|
+
snapshot: { id: value.id, status: "queued" },
|
|
77
|
+
}));
|
|
78
|
+
const decodeTask = MediaProtocol.decodeJson(ADAPTER, NAME, Task);
|
|
79
|
+
const decodeStatus = Effect.fn("RunwayVideo.decodeStatus")(function* (response, context) {
|
|
80
|
+
const output = yield* decodeTask(response);
|
|
81
|
+
const status = yield* MediaProtocol.status(STATUS, output.value.status, output);
|
|
82
|
+
return { id: context.token.taskID, status, progress: output.value.progress ?? undefined };
|
|
83
|
+
});
|
|
84
|
+
const decodeResult = Effect.fn("RunwayVideo.decodeResult")(function* (response, context) {
|
|
85
|
+
const output = yield* decodeTask(response);
|
|
86
|
+
const task = output.value;
|
|
87
|
+
const status = yield* MediaProtocol.status(STATUS, task.status, output);
|
|
88
|
+
if (status === "failed") {
|
|
89
|
+
const code = task.failureCode ?? undefined;
|
|
90
|
+
const message = `${NAME} task failed${code === undefined ? "" : ` (${code})`}${task.failure ? `: ${task.failure}` : ""}`;
|
|
91
|
+
// Runway failure codes are dotted paths; every moderation outcome carries a SAFETY segment.
|
|
92
|
+
if (code !== undefined && /(^|\.)SAFETY(\.|$)/.test(code))
|
|
93
|
+
return yield* output.contentPolicy(message);
|
|
94
|
+
return yield* output.ended("failed", message);
|
|
95
|
+
}
|
|
96
|
+
if (status === "cancelled")
|
|
97
|
+
return yield* output.ended("cancelled", `${NAME} task ${context.token.taskID} was cancelled`);
|
|
98
|
+
if (status !== "completed")
|
|
99
|
+
return yield* output.invalid(`${NAME} task ${context.token.taskID} has not finished`);
|
|
100
|
+
const urls = task.output ?? [];
|
|
101
|
+
if (urls.length === 0)
|
|
102
|
+
return yield* output.invalid(`${NAME} task succeeded without any output`);
|
|
103
|
+
return new VideoResponse({
|
|
104
|
+
videos: yield* Effect.forEach(urls, (url) => MediaProtocol.expiringUrl(url, OUTPUT_RETENTION, { mediaType: "video/mp4" })),
|
|
105
|
+
usage: task.cost === undefined ? undefined : { type: "credits", credits: task.cost.credits },
|
|
106
|
+
providerMetadata: {
|
|
107
|
+
runway: {
|
|
108
|
+
taskId: context.token.taskID,
|
|
109
|
+
estimatedCredits: task.estimatedCost?.credits,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// 7. Protocol and route
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
const taskPath = (token) => `${TASKS_PATH}/${token.taskID}`;
|
|
118
|
+
export const protocol = MediaProtocol.queued({
|
|
119
|
+
id: ADAPTER,
|
|
120
|
+
name: NAME,
|
|
121
|
+
token: Token,
|
|
122
|
+
unsupported: ["n"],
|
|
123
|
+
start: { body: { from: fromRequest }, decode: decodeStart },
|
|
124
|
+
status: { path: taskPath, decode: decodeStatus },
|
|
125
|
+
result: { path: taskPath, decode: decodeResult },
|
|
126
|
+
cancel: { method: "DELETE", path: taskPath },
|
|
127
|
+
});
|
|
128
|
+
const startPath = (request) => {
|
|
129
|
+
if (request.video !== undefined)
|
|
130
|
+
return VIDEO_TO_VIDEO_PATH;
|
|
131
|
+
if (request.frames?.first !== undefined || request.frames?.last !== undefined)
|
|
132
|
+
return IMAGE_TO_VIDEO_PATH;
|
|
133
|
+
return TEXT_TO_VIDEO_PATH;
|
|
134
|
+
};
|
|
135
|
+
export const model = (input) => VideoModel.fromRoute({
|
|
136
|
+
id: ADAPTER,
|
|
137
|
+
provider: PROVIDER,
|
|
138
|
+
protocol,
|
|
139
|
+
baseURL: DEFAULT_BASE_URL,
|
|
140
|
+
headers: { "X-Runway-Version": API_VERSION },
|
|
141
|
+
path: ({ request }) => startPath(request),
|
|
142
|
+
}, input);
|
|
143
|
+
export const RunwayVideo = {
|
|
144
|
+
protocol,
|
|
145
|
+
model,
|
|
146
|
+
};
|
|
@@ -116,6 +116,16 @@ export declare const requireInlineMedia: (route: string, asset: Media.Asset) =>
|
|
|
116
116
|
export declare const inlineRequired: (route: string, asset: Media.Asset) => AIError;
|
|
117
117
|
/** The remote URL of a `url` asset, for protocols that accept `http(s)` references natively. */
|
|
118
118
|
export declare const mediaUrl: (asset: Media.Asset) => string | undefined;
|
|
119
|
+
export type MediaReference = {
|
|
120
|
+
readonly type: "dataUrl" | "url" | "ref";
|
|
121
|
+
readonly value: string;
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* The one string a provider can address an asset by: inline payloads as a data URL, `url` sources as their URL, and
|
|
125
|
+
* this provider's own `ref` as its id. Other providers' refs are never forwarded and fail typed; omit `provider` for
|
|
126
|
+
* APIs with no file handles at all.
|
|
127
|
+
*/
|
|
128
|
+
export declare const mediaReference: (asset: Media.Asset, provider: ProviderID | undefined, label: string) => Effect.Effect<MediaReference, AIError>;
|
|
119
129
|
/**
|
|
120
130
|
* Lift a tool-result file into a `MediaPart`. Tool files carry either a data URL, an `http(s)` URL, or raw base64 in
|
|
121
131
|
* `uri`; the declared `mime` wins over any data-URL prefix so tool authors control the type the model sees.
|
|
@@ -126,9 +136,9 @@ export declare const toolResultText: (part: ToolResultPart) => string;
|
|
|
126
136
|
export declare const errorText: (error: unknown) => string;
|
|
127
137
|
/**
|
|
128
138
|
* `framing` step for Server-Sent Events. Decodes UTF-8, runs the SSE channel
|
|
129
|
-
* decoder, optionally filters named events, and drops empty and
|
|
130
|
-
*
|
|
131
|
-
* as their stream boundary. Retry control events are ignored without
|
|
139
|
+
* decoder, optionally filters named events, and drops empty events and known
|
|
140
|
+
* keepalives that proxies send as data. `[DONE]` is dropped by default or
|
|
141
|
+
* retained for protocols that use it as their stream boundary. Retry control events are ignored without
|
|
132
142
|
* interrupting the stream. Decoder failures become provider output errors so
|
|
133
143
|
* the public error channel stays `AIError`.
|
|
134
144
|
*/
|
package/dist/protocols/shared.js
CHANGED
|
@@ -134,6 +134,23 @@ export const requireInlineMedia = (route, asset) => {
|
|
|
134
134
|
export const inlineRequired = (route, asset) => invalidRequest(`${route} requires inline media (bytes or base64); ${asset.source.type} sources must be materialized first`);
|
|
135
135
|
/** The remote URL of a `url` asset, for protocols that accept `http(s)` references natively. */
|
|
136
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
|
+
};
|
|
137
154
|
/**
|
|
138
155
|
* Lift a tool-result file into a `MediaPart`. Tool files carry either a data URL, an `http(s)` URL, or raw base64 in
|
|
139
156
|
* `uri`; the declared `mime` wins over any data-URL prefix so tool authors control the type the model sees.
|
|
@@ -174,9 +191,9 @@ export const errorText = (error) => {
|
|
|
174
191
|
};
|
|
175
192
|
/**
|
|
176
193
|
* `framing` step for Server-Sent Events. Decodes UTF-8, runs the SSE channel
|
|
177
|
-
* decoder, optionally filters named events, and drops empty and
|
|
178
|
-
*
|
|
179
|
-
* as their stream boundary. Retry control events are ignored without
|
|
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
|
|
180
197
|
* interrupting the stream. Decoder failures become provider output errors so
|
|
181
198
|
* the public error channel stays `AIError`.
|
|
182
199
|
*/
|
|
@@ -200,6 +217,9 @@ export const sseFraming = (bytes, events, includeDone = false) => bytes.pipe(Str
|
|
|
200
217
|
// `data: null`, between events or after `[DONE]`. No protocol has a
|
|
201
218
|
// null event, so it carries nothing and must not abort the stream.
|
|
202
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" &&
|
|
203
223
|
(event.data !== "[DONE]" || includeDone || (events !== undefined && event.event !== "message"))), Stream.map((event) => event.data));
|
|
204
224
|
/**
|
|
205
225
|
* Canonical invalid-request constructor shared by protocol lowering.
|
|
@@ -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
|
+
};
|
|
@@ -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";
|