@opencode/ai 0.0.0-dev-20170 → 0.0.0-dev-20180
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 +18 -16
- package/dist/generation.d.ts +5 -3
- package/dist/generation.js +16 -9
- package/dist/protocols/assemblyai-transcription.js +4 -2
- package/dist/protocols/bfl-images.d.ts +7 -1
- package/dist/protocols/bfl-images.js +18 -4
- package/dist/protocols/deepgram-speech.js +4 -1
- package/dist/protocols/fal-images.js +15 -12
- package/dist/protocols/google-speech.js +4 -1
- package/dist/protocols/openai-responses.js +5 -2
- package/dist/protocols/openai-speech.js +4 -1
- package/dist/protocols/runway-video.js +1 -1
- package/dist/route/media-protocol.d.ts +5 -0
- package/dist/route/media.js +14 -6
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -475,18 +475,18 @@ const program = Effect.gen(function* () {
|
|
|
475
475
|
Common fields are portable in shape, not in support. Unsupported fields fail with a typed `AIError` before any network
|
|
476
476
|
call rather than being dropped, so check this table before swapping only the `model`:
|
|
477
477
|
|
|
478
|
-
| Provider | `n` | `size` | `aspectRatio` | `seed` | `format` | `images`
|
|
479
|
-
| --------------------- | --- | --------- | ------------- | ------ | -------- |
|
|
480
|
-
| OpenAI | ✓¹ | ✓ | ✗ | ✗ | ✓ | ✓
|
|
481
|
-
| Google (Gemini) | 1 | ✗ | ✓ | ✓ | ✗ | ✓ (no public URLs)
|
|
482
|
-
| xAI | ✓ | ✗ | ✓ | ✗ | ✗ | ✓
|
|
483
|
-
| Z.ai | ✗ | ✓ | ✗ | ✗ | ✗ | ✗
|
|
484
|
-
| Meta | ✓ | ✓ (hint) | ✗ | ✗ | ✓ | ✓
|
|
485
|
-
| Black Forest Labs | 1 | per model | per model | ✓ | ✓ | per model (1–8)
|
|
486
|
-
| fal | ✓ | per model | per model | ✓ | ✓ | 1 (several on `/edit`)
|
|
487
|
-
| Replicate | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ (use `providerOptions`)
|
|
488
|
-
| Stability `image` | 1 | ✗ | ✓ | ✓ | ✓ | 1 (not on `core`)
|
|
489
|
-
| Stability `upscale()` | ✗ | ✗ | ✗ | ✓ | ✓ | exactly 1 (required)
|
|
478
|
+
| Provider | `n` | `size` | `aspectRatio` | `seed` | `format` | `images` | `mask` |
|
|
479
|
+
| --------------------- | --- | --------- | ------------- | ------ | -------- | -------------------------------- | ------------------- |
|
|
480
|
+
| OpenAI | ✓¹ | ✓ | ✗ | ✗ | ✓ | ✓ | ✓ |
|
|
481
|
+
| Google (Gemini) | 1 | ✗ | ✓ | ✓ | ✗ | ✓ (no public URLs) | ✗ |
|
|
482
|
+
| xAI | ✓ | ✗ | ✓ | ✗ | ✗ | ✓ | ✗ |
|
|
483
|
+
| Z.ai | ✗ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ |
|
|
484
|
+
| Meta | ✓ | ✓ (hint) | ✗ | ✗ | ✓ | ✓ | ✗ |
|
|
485
|
+
| Black Forest Labs | 1 | per model | per model | ✓ | ✓ | per model (1–8) | `flux-pro-1.0-fill` |
|
|
486
|
+
| fal | ✓ | per model | per model | ✓ | ✓ | 1 (several on `/edit`, `/multi`) | ✓ |
|
|
487
|
+
| Replicate | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ (use `providerOptions`) | ✗ |
|
|
488
|
+
| Stability `image` | 1 | ✗ | ✓ | ✓ | ✓ | 1 (not on `core`) | ✗ |
|
|
489
|
+
| Stability `upscale()` | ✗ | ✗ | ✗ | ✓ | ✓ | exactly 1 (required) | ✗ |
|
|
490
490
|
|
|
491
491
|
✓ lowers natively; ✗ fails whenever the field is set (including `n: 1`); `1` means `n > 1` fails. ¹ `Image.stream` on OpenAI generates one image. fal
|
|
492
492
|
rejects `size` and `aspectRatio` together; which one a fal or BFL model takes depends on the model.
|
|
@@ -699,7 +699,7 @@ const program = Effect.gen(function* () {
|
|
|
699
699
|
})
|
|
700
700
|
```
|
|
701
701
|
|
|
702
|
-
The hosted result is represented as a provider-executed tool call and tool result
|
|
702
|
+
The hosted result is represented as a provider-executed tool call and a tool result whose content carries the generated image as a file. Gemini image-capable models instead emit a first-class `media` `LLMEvent` for inline image output (`response.message` then carries a `media` part). Retaining `response.message` preserves the generated image for continuation on both routes.
|
|
703
703
|
|
|
704
704
|
## Video generation
|
|
705
705
|
|
|
@@ -840,9 +840,10 @@ Provider notes:
|
|
|
840
840
|
- **OpenAI** streams over SSE (`stream_format: "sse"`), which is also the only place it reports token usage; `tts-1`
|
|
841
841
|
and `tts-1-hd` do not support SSE and stream the raw audio body instead. `pcm` is 24 kHz 16-bit mono. `language`
|
|
842
842
|
and `timestamps` are not supported.
|
|
843
|
-
- **Gemini TTS** returns
|
|
844
|
-
|
|
845
|
-
|
|
843
|
+
- **Gemini TTS** returns the provider's default output: WAV for Gemini 3.8 TTS `generate`, raw 16-bit PCM
|
|
844
|
+
(`audio/L16;codec=pcm;rate=24000`) otherwise. `pcm` is the only explicit `format` it accepts, and it fails typed on
|
|
845
|
+
Gemini 3.8 `generate`; the route never wraps PCM as WAV. Style is directed in the text, so `instructions` and
|
|
846
|
+
`speed` fail typed. Only `gemini-3.1-flash-tts-preview` and later support streaming. Two-speaker audio goes through
|
|
846
847
|
`providerOptions.speechConfig.multiSpeakerVoiceConfig`.
|
|
847
848
|
- **ElevenLabs** requires `voice` (the path voice id) and authenticates with `xi-api-key`. `format` maps to the
|
|
848
849
|
`output_format` query parameter (`mp3_44100_128`, `pcm_24000`, `wav_24000`, `opus_48000_64`);
|
|
@@ -944,6 +945,7 @@ const transcript = await generation.await({ poll: { interval: 3_000 } })
|
|
|
944
945
|
- **`ImageClient`** — Effect service and layer for image execution, parallel to `LLMClient`.
|
|
945
946
|
- **`Media`** — the shared asset type (`Media.Asset`, `Media.Source`) and constructors used by messages, tool results, and media requests.
|
|
946
947
|
- **`Generation`** — provider-neutral handle for an in-flight media generation (`await`, `refresh`, `cancel`, `events`) used by queued media routes.
|
|
948
|
+
- **`Video.request` / `generate` / `stream` / `start` / `resume`** — queued video generation through a provider-neutral request; `VideoClient` is its Effect service and layer.
|
|
947
949
|
- **`Speech.request` / `Speech.generate` / `Speech.stream`** — text-to-speech through a provider-neutral request; `SpeechClient` is its Effect service and layer.
|
|
948
950
|
- **`Transcription.request` / `generate` / `stream` / `start` / `resume`** — speech-to-text over inline, streaming, and queued routes; `TranscriptionClient` is its Effect service and layer.
|
|
949
951
|
- **`AIClient.layer` / `AIClient.layerWith(executor)`** — every modality client plus the request executor in one layer.
|
package/dist/generation.d.ts
CHANGED
|
@@ -44,6 +44,7 @@ export type Event = Observation | {
|
|
|
44
44
|
readonly id: string;
|
|
45
45
|
readonly status: Status;
|
|
46
46
|
};
|
|
47
|
+
export declare const isTerminal: (status: Status) => boolean;
|
|
47
48
|
export declare class Generation<Response> {
|
|
48
49
|
readonly route: Route<Response>;
|
|
49
50
|
/** Route-owned serializable JSON; pass it to the modality's `resume` from another process. */
|
|
@@ -64,9 +65,10 @@ export declare class Generation<Response> {
|
|
|
64
65
|
await(options?: AwaitOptions): Effect.Effect<Response, AIError>;
|
|
65
66
|
cancel(): Effect.Effect<void, AIError>;
|
|
66
67
|
/**
|
|
67
|
-
* Status observations as a stream, ending after the first terminal observation. Each poll
|
|
68
|
-
* remaining until `poll.timeout`, so a hung status request
|
|
69
|
-
*
|
|
68
|
+
* Status observations as a stream, ending after the first terminal observation. Each poll and each sleep between polls
|
|
69
|
+
* is bounded by the time remaining until `poll.timeout`, so a hung status request or a long interval fails the stream at
|
|
70
|
+
* the deadline instead of stalling it. (`Stream.interruptWhen` would express this directly but deadlocks under
|
|
71
|
+
* `TestClock` when the source completes while the timer sleeps.)
|
|
70
72
|
*/
|
|
71
73
|
events(options?: AwaitOptions): Stream.Stream<Event, AIError>;
|
|
72
74
|
private event;
|
package/dist/generation.js
CHANGED
|
@@ -14,6 +14,7 @@ export const ProgressEvent = Schema.Struct({
|
|
|
14
14
|
progress: Schema.optional(Schema.Number),
|
|
15
15
|
}).annotate({ identifier: "Generation.Event.Progress" });
|
|
16
16
|
const TERMINAL = new Set(["completed", "failed", "cancelled", "expired"]);
|
|
17
|
+
export const isTerminal = (status) => TERMINAL.has(status);
|
|
17
18
|
export class Generation {
|
|
18
19
|
route;
|
|
19
20
|
token;
|
|
@@ -40,7 +41,7 @@ export class Generation {
|
|
|
40
41
|
};
|
|
41
42
|
}
|
|
42
43
|
get terminal() {
|
|
43
|
-
return
|
|
44
|
+
return isTerminal(this.status);
|
|
44
45
|
}
|
|
45
46
|
refresh() {
|
|
46
47
|
return this.route.status.pipe(Effect.map((snapshot) => new Generation(this.route, this.token, snapshot)));
|
|
@@ -61,9 +62,10 @@ export class Generation {
|
|
|
61
62
|
return this.route.cancel ?? Effect.void;
|
|
62
63
|
}
|
|
63
64
|
/**
|
|
64
|
-
* Status observations as a stream, ending after the first terminal observation. Each poll
|
|
65
|
-
* remaining until `poll.timeout`, so a hung status request
|
|
66
|
-
*
|
|
65
|
+
* Status observations as a stream, ending after the first terminal observation. Each poll and each sleep between polls
|
|
66
|
+
* is bounded by the time remaining until `poll.timeout`, so a hung status request or a long interval fails the stream at
|
|
67
|
+
* the deadline instead of stalling it. (`Stream.interruptWhen` would express this directly but deadlocks under
|
|
68
|
+
* `TestClock` when the source completes while the timer sleeps.)
|
|
67
69
|
*/
|
|
68
70
|
events(options) {
|
|
69
71
|
if (this.terminal)
|
|
@@ -71,11 +73,16 @@ export class Generation {
|
|
|
71
73
|
const timeout = Duration.fromInputUnsafe(options?.poll?.timeout ?? DEFAULT_POLL_TIMEOUT);
|
|
72
74
|
return Stream.unwrap(Clock.currentTimeMillis.pipe(Effect.map((start) => {
|
|
73
75
|
const deadline = start + Duration.toMillis(timeout);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
// Fail before polling once the deadline has passed: a fast status request could otherwise win the zero-budget
|
|
77
|
+
// race and schedule another zero-delay poll.
|
|
78
|
+
const refresh = Clock.currentTimeMillis.pipe(Effect.flatMap((now) => now >= deadline
|
|
79
|
+
? this.timeoutError(timeout)
|
|
80
|
+
: this.refresh().pipe(Effect.timeoutOrElse({
|
|
81
|
+
duration: Duration.millis(deadline - now),
|
|
82
|
+
orElse: () => this.timeoutError(timeout),
|
|
83
|
+
}))));
|
|
84
|
+
const schedule = this.schedule(options?.poll).pipe(Schedule.modifyDelay((meta) => Effect.succeed(Duration.min(meta.duration, Duration.millis(Math.max(0, deadline - meta.now))))));
|
|
85
|
+
return Stream.fromEffectSchedule(refresh, schedule).pipe(Stream.takeUntil((generation) => generation.terminal), Stream.map((generation) => generation.event()));
|
|
79
86
|
})));
|
|
80
87
|
}
|
|
81
88
|
event() {
|
|
@@ -64,8 +64,10 @@ const fromRequest = Effect.fn("AssemblyAITranscription.fromRequest")(function* (
|
|
|
64
64
|
language_code: request.language,
|
|
65
65
|
language_detection: request.language === undefined ? true : undefined,
|
|
66
66
|
prompt: request.prompt,
|
|
67
|
-
// Turn-level `utterances`, the only segments AssemblyAI returns, require speaker labels.
|
|
68
|
-
speaker_labels: request.diarize === true || request.timestamps === "segment"
|
|
67
|
+
// Turn-level `utterances`, the only segments AssemblyAI returns, and `speakers_expected` require speaker labels.
|
|
68
|
+
speaker_labels: request.diarize === true || request.timestamps === "segment" || request.speakers !== undefined
|
|
69
|
+
? true
|
|
70
|
+
: undefined,
|
|
69
71
|
speakers_expected: request.speakers,
|
|
70
72
|
}, request.providerOptions, request.http?.body) ?? {});
|
|
71
73
|
});
|
|
@@ -12,21 +12,27 @@ export type BlackForestLabsImageOptions = {
|
|
|
12
12
|
readonly steps?: number;
|
|
13
13
|
} & Record<string, unknown>;
|
|
14
14
|
export type Request = ImageRequestFor<BlackForestLabsImageOptions>;
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Regional clusters answer on different hosts, so the returned `polling_url` is followed verbatim. BFL reports the
|
|
17
|
+
* credit cost on submit, so it rides on the token; it is optional so tokens persisted before it existed still decode.
|
|
18
|
+
*/
|
|
16
19
|
export declare const Token: Schema.Struct<{
|
|
17
20
|
readonly id: Schema.String;
|
|
18
21
|
readonly pollingURL: Schema.String;
|
|
22
|
+
readonly cost: Schema.optionalKey<Schema.Number>;
|
|
19
23
|
}>;
|
|
20
24
|
export type Token = Schema.Schema.Type<typeof Token>;
|
|
21
25
|
export declare const protocol: MediaProtocol.Queued<Request, ImageResponse, {
|
|
22
26
|
readonly id: string;
|
|
23
27
|
readonly pollingURL: string;
|
|
28
|
+
readonly cost?: number | undefined;
|
|
24
29
|
}>;
|
|
25
30
|
export declare const model: (input: MediaRoute.ModelInput) => ImageModel<BlackForestLabsImageOptions>;
|
|
26
31
|
export declare const BlackForestLabsImages: {
|
|
27
32
|
readonly protocol: MediaProtocol.Queued<Request, ImageResponse, {
|
|
28
33
|
readonly id: string;
|
|
29
34
|
readonly pollingURL: string;
|
|
35
|
+
readonly cost?: number | undefined;
|
|
30
36
|
}>;
|
|
31
37
|
readonly model: (input: MediaRoute.ModelInput) => ImageModel<BlackForestLabsImageOptions>;
|
|
32
38
|
};
|
|
@@ -11,11 +11,19 @@ export const DEFAULT_BASE_URL = "https://api.bfl.ai";
|
|
|
11
11
|
// ---------------------------------------------------------------------------
|
|
12
12
|
// 2. Token and response schemas
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
14
|
-
/**
|
|
15
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Regional clusters answer on different hosts, so the returned `polling_url` is followed verbatim. BFL reports the
|
|
16
|
+
* credit cost on submit, so it rides on the token; it is optional so tokens persisted before it existed still decode.
|
|
17
|
+
*/
|
|
18
|
+
export const Token = Schema.Struct({
|
|
19
|
+
id: Schema.String,
|
|
20
|
+
pollingURL: Schema.String,
|
|
21
|
+
cost: Schema.optionalKey(Schema.Number),
|
|
22
|
+
});
|
|
16
23
|
const StartResponse = Schema.Struct({
|
|
17
24
|
id: Schema.String,
|
|
18
25
|
polling_url: Schema.String,
|
|
26
|
+
cost: optionalNull(Schema.Number),
|
|
19
27
|
});
|
|
20
28
|
const Result = Schema.Struct({
|
|
21
29
|
id: Schema.String,
|
|
@@ -93,7 +101,11 @@ const fromRequest = Effect.fn("BlackForestLabsImages.fromRequest")(function* (re
|
|
|
93
101
|
// 6. Response decoding
|
|
94
102
|
// ---------------------------------------------------------------------------
|
|
95
103
|
const decodeStart = route.decodeStarted(StartResponse, (value) => ({
|
|
96
|
-
token: {
|
|
104
|
+
token: {
|
|
105
|
+
id: value.id,
|
|
106
|
+
pollingURL: value.polling_url,
|
|
107
|
+
...(value.cost === undefined || value.cost === null ? {} : { cost: value.cost }),
|
|
108
|
+
},
|
|
97
109
|
snapshot: { id: value.id, status: "queued" },
|
|
98
110
|
}));
|
|
99
111
|
const decodeDocument = route.decodeJson(Result);
|
|
@@ -112,10 +124,12 @@ const decodeResult = Effect.fn("BlackForestLabsImages.decodeResult")(function* (
|
|
|
112
124
|
if (status !== "completed" || document.result === undefined || document.result === null)
|
|
113
125
|
return yield* output.invalid(`${route.name} generation ${context.token.id} has no result`);
|
|
114
126
|
const { sample, seed, prompt, ...rest } = document.result;
|
|
127
|
+
// A settled `cost` on the result supersedes the submit-time cost carried on the token.
|
|
128
|
+
const cost = document.cost ?? context.token.cost;
|
|
115
129
|
return new ImageResponse({
|
|
116
130
|
// `sample` is a signed URL that expires 10 minutes after the result is ready, so it is downloaded now.
|
|
117
131
|
images: [yield* context.materialize(Media.url(sample))],
|
|
118
|
-
usage:
|
|
132
|
+
usage: cost === undefined ? undefined : { type: "credits", credits: cost },
|
|
119
133
|
providerMetadata: {
|
|
120
134
|
bfl: { id: context.token.id, seed: seed ?? undefined, prompt: prompt ?? undefined, ...rest },
|
|
121
135
|
},
|
|
@@ -38,6 +38,9 @@ const queryParameters = (request) => {
|
|
|
38
38
|
});
|
|
39
39
|
};
|
|
40
40
|
const fromRequest = Effect.fn("DeepgramSpeech.fromRequest")(function* (request) {
|
|
41
|
+
// Not in `unsupported`: that list would also reject `timestamps: false`, which asks for nothing.
|
|
42
|
+
if (request.timestamps === true)
|
|
43
|
+
return yield* route.unsupported("media.timestamps", `${route.name} does not return timestamps`);
|
|
41
44
|
if (request.format !== undefined &&
|
|
42
45
|
FORMATS[request.format] === undefined &&
|
|
43
46
|
request.providerOptions?.encoding === undefined)
|
|
@@ -74,7 +77,7 @@ const finish = (state, context) => {
|
|
|
74
77
|
// 7. Protocol and route
|
|
75
78
|
// ---------------------------------------------------------------------------
|
|
76
79
|
export const protocol = MediaProtocol.stream(route, {
|
|
77
|
-
unsupported: ["voice", "language", "instructions"
|
|
80
|
+
unsupported: ["voice", "language", "instructions"],
|
|
78
81
|
body: { from: fromRequest },
|
|
79
82
|
frames: (bytes) => bytes,
|
|
80
83
|
initial: () => ({ chunks: [] }),
|
|
@@ -25,7 +25,7 @@ const QueueResult = Schema.StructWithRest(Schema.Struct({
|
|
|
25
25
|
// 5. Request body construction
|
|
26
26
|
// ---------------------------------------------------------------------------
|
|
27
27
|
const sizing = (model) => {
|
|
28
|
-
if (/^fal-ai\/(nano-banana|flux-pro\/v1\.1-ultra)/.test(model))
|
|
28
|
+
if (/^fal-ai\/(nano-banana|flux-pro\/(v1\.1-ultra|kontext))/.test(model))
|
|
29
29
|
return "aspect_ratio";
|
|
30
30
|
if (model.startsWith("fal-ai/flux"))
|
|
31
31
|
return "image_size";
|
|
@@ -40,16 +40,17 @@ const validate = (request) => {
|
|
|
40
40
|
return Effect.fail(route.unsupported("media.size", `${id} sizes by aspectRatio`));
|
|
41
41
|
if (request.aspectRatio !== undefined && field === "image_size")
|
|
42
42
|
return Effect.fail(route.unsupported("media.aspectRatio", `${id} sizes by size (image_size)`));
|
|
43
|
-
if ((request.images?.length ?? 0) > 1 && !
|
|
44
|
-
return Effect.fail(route.unsupported("media.images", `${id} takes one image_url; use an /edit endpoint for several images`));
|
|
43
|
+
if ((request.images?.length ?? 0) > 1 && !takesImageList(id))
|
|
44
|
+
return Effect.fail(route.unsupported("media.images", `${id} takes one image_url; use an /edit or /multi endpoint for several images`));
|
|
45
45
|
return Effect.void;
|
|
46
46
|
};
|
|
47
|
-
// `/edit` endpoints take an `image_urls` list; image-to-image, fill, and Ultra take one
|
|
48
|
-
|
|
47
|
+
// `/edit` and `/multi` (Kontext) endpoints take an `image_urls` list; image-to-image, fill, and Ultra take one
|
|
48
|
+
// `image_url` (beside `mask_url`).
|
|
49
|
+
const takesImageList = (model) => model.endsWith("/edit") || model.endsWith("/multi");
|
|
49
50
|
const fromRequest = Effect.fn("FalImages.fromRequest")(function* (request) {
|
|
50
51
|
yield* validate(request);
|
|
51
52
|
const images = yield* Effect.forEach(request.images ?? [], (image) => FalQueue.mediaUrl(image, route.name));
|
|
52
|
-
const
|
|
53
|
+
const list = takesImageList(request.model.id);
|
|
53
54
|
return MediaProtocol.json(mergeJsonRecords({
|
|
54
55
|
prompt: request.prompt,
|
|
55
56
|
num_images: request.n,
|
|
@@ -57,8 +58,8 @@ const fromRequest = Effect.fn("FalImages.fromRequest")(function* (request) {
|
|
|
57
58
|
image_size: request.size === undefined ? undefined : MediaInput.dimensions(request.size),
|
|
58
59
|
aspect_ratio: request.aspectRatio,
|
|
59
60
|
output_format: request.format,
|
|
60
|
-
image_urls:
|
|
61
|
-
image_url:
|
|
61
|
+
image_urls: list && images.length > 0 ? images : undefined,
|
|
62
|
+
image_url: list ? undefined : images[0],
|
|
62
63
|
mask_url: request.mask === undefined ? undefined : yield* FalQueue.mediaUrl(request.mask, route.name),
|
|
63
64
|
}, request.providerOptions, request.http?.body) ?? {});
|
|
64
65
|
});
|
|
@@ -74,10 +75,12 @@ const decodeResult = Effect.fn("FalImages.decodeResult")(function* (response, co
|
|
|
74
75
|
// With the safety checker on, flagged images come back blacked out rather than omitted.
|
|
75
76
|
const flagged = (has_nsfw_concepts ?? []).flatMap((value, index) => (value ? [index] : []));
|
|
76
77
|
return new ImageResponse({
|
|
77
|
-
images: images.map((image) =>
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
images: images.map((image) => {
|
|
79
|
+
const info = { width: image.width ?? undefined, height: image.height ?? undefined };
|
|
80
|
+
// `sync_mode: true` returns data URIs instead of hosted URLs.
|
|
81
|
+
return (Media.parseDataUrl(image.url, { info }) ??
|
|
82
|
+
Media.url(image.url, { mediaType: image.content_type ?? undefined, info }));
|
|
83
|
+
}),
|
|
81
84
|
notices: flagged.length === 0
|
|
82
85
|
? undefined
|
|
83
86
|
: flagged.map((index) => ({
|
|
@@ -20,6 +20,9 @@ const decodeChunk = route.decodeFrame(GenerateContentChunk);
|
|
|
20
20
|
// 5. Request body construction
|
|
21
21
|
// ---------------------------------------------------------------------------
|
|
22
22
|
const fromRequest = Effect.fn("GoogleSpeech.fromRequest")(function* (request) {
|
|
23
|
+
// Not in `unsupported`: that list would also reject `timestamps: false`, which asks for nothing.
|
|
24
|
+
if (request.timestamps === true)
|
|
25
|
+
return yield* route.unsupported("media.timestamps", `${route.name} does not return timestamps`);
|
|
23
26
|
if (request.format === "pcm" && request.mode === "generate" && /^gemini-3\.8-.*-tts(?:-|$)/.test(request.model.id))
|
|
24
27
|
return yield* route.unsupported("media.format", `${route.name} returns WAV by default for Gemini 3.8 TTS unary requests; omit the format to accept it`);
|
|
25
28
|
if (request.format !== undefined && request.format !== "pcm")
|
|
@@ -66,7 +69,7 @@ const finish = (state, context) => {
|
|
|
66
69
|
// 7. Protocol and route
|
|
67
70
|
// ---------------------------------------------------------------------------
|
|
68
71
|
export const protocol = MediaProtocol.stream(route, {
|
|
69
|
-
unsupported: ["instructions", "speed"
|
|
72
|
+
unsupported: ["instructions", "speed"],
|
|
70
73
|
body: { from: fromRequest },
|
|
71
74
|
frames: (bytes, context) => GeminiGenerateContent.frames(bytes, context.request.mode),
|
|
72
75
|
initial: () => ({ chunks: [] }),
|
|
@@ -111,14 +111,17 @@ const adapter = {
|
|
|
111
111
|
name: NAME,
|
|
112
112
|
restoreHostedToolItem: (item) => (Schema.is(OpenAIResponsesHostedToolItem)(item) ? item : undefined),
|
|
113
113
|
};
|
|
114
|
-
//
|
|
114
|
+
// GPT-6 Astra, Sol, and Luna accept `configuration_update` only in standard mode (not `reasoning.mode: "pro"` or
|
|
115
|
+
// `-pro` slugs), and never alongside automatic `context_management` compaction.
|
|
115
116
|
const supportsEffortUpdates = (request) => {
|
|
116
117
|
if (request.providerOptions?.contextManagement !== undefined)
|
|
117
118
|
return false;
|
|
119
|
+
if (Schema.is(Schema.Struct({ mode: Schema.Literal("pro") }))(request.http?.body?.reasoning))
|
|
120
|
+
return false;
|
|
118
121
|
const override = request.model.compatibility?.supportsEffortUpdates;
|
|
119
122
|
if (override !== undefined)
|
|
120
123
|
return override;
|
|
121
|
-
return /(?:^|\/)gpt-6-astra$/i.test(request.model.id);
|
|
124
|
+
return /(?:^|\/)gpt-6-(?:astra|sol|luna)$/i.test(request.model.id);
|
|
122
125
|
};
|
|
123
126
|
const nativeImageToolInput = (tool) => {
|
|
124
127
|
const native = tool.native?.openai;
|
|
@@ -31,6 +31,9 @@ const decodeEvent = route.decodeFrame(SpeechStreamEvent);
|
|
|
31
31
|
// `sse` is not supported for `tts-1` or `tts-1-hd`; those models stream the raw audio body instead.
|
|
32
32
|
const supportsSse = (model) => !/^tts-1(-hd)?(-|$)/.test(model);
|
|
33
33
|
const fromRequest = Effect.fn("OpenAISpeech.fromRequest")(function* (request) {
|
|
34
|
+
// Not in `unsupported`: that list would also reject `timestamps: false`, which asks for nothing.
|
|
35
|
+
if (request.timestamps === true)
|
|
36
|
+
return yield* route.unsupported("media.timestamps", `${route.name} does not return timestamps`);
|
|
34
37
|
return MediaProtocol.json(mergeJsonRecords({
|
|
35
38
|
model: request.model.id,
|
|
36
39
|
input: request.text,
|
|
@@ -80,7 +83,7 @@ const finish = (state, context) => {
|
|
|
80
83
|
// 7. Protocol and route
|
|
81
84
|
// ---------------------------------------------------------------------------
|
|
82
85
|
export const protocol = MediaProtocol.stream(route, {
|
|
83
|
-
unsupported: ["language"
|
|
86
|
+
unsupported: ["language"],
|
|
84
87
|
body: { from: fromRequest },
|
|
85
88
|
frames: (bytes, context) => (isSse(context.body) ? Framing.sse.frame(bytes) : bytes),
|
|
86
89
|
initial: () => ({ chunks: [], done: false }),
|
|
@@ -119,7 +119,7 @@ export const protocol = MediaProtocol.queued(route, {
|
|
|
119
119
|
start: { body: { from: fromRequest }, decode: decodeStart },
|
|
120
120
|
status: { path: taskPath, decode: decodeStatus },
|
|
121
121
|
result: { path: taskPath, decode: decodeResult },
|
|
122
|
-
cancel: { method: "DELETE", path: taskPath },
|
|
122
|
+
cancel: { method: "DELETE", path: taskPath, activeOnly: true },
|
|
123
123
|
});
|
|
124
124
|
const startPath = (request) => {
|
|
125
125
|
if (request.video !== undefined)
|
|
@@ -93,6 +93,11 @@ export interface Queued<Request, Response, Token> {
|
|
|
93
93
|
readonly cancel?: {
|
|
94
94
|
readonly method: AuthInput["method"];
|
|
95
95
|
readonly path: (token: Token) => string;
|
|
96
|
+
/**
|
|
97
|
+
* Fetch a fresh status first and skip the call for terminal generations, for providers whose cancel endpoint
|
|
98
|
+
* destroys finished work (Runway's `DELETE /v1/tasks/{id}` deletes completed tasks and their outputs).
|
|
99
|
+
*/
|
|
100
|
+
readonly activeOnly?: boolean;
|
|
96
101
|
};
|
|
97
102
|
}
|
|
98
103
|
export declare const queued: <Request, Response, Token>(route: Identity, input: Omit<Queued<Request, Response, Token>, "kind" | "id" | "provider">) => Queued<Request, Response, Token>;
|
package/dist/route/media.js
CHANGED
|
@@ -5,7 +5,7 @@ import { Endpoint } from "./endpoint.js";
|
|
|
5
5
|
import { RequestExecutorService } from "./executor-service.js";
|
|
6
6
|
import { RequestExecutor } from "./executor.js";
|
|
7
7
|
import { MediaProtocol } from "./media-protocol.js";
|
|
8
|
-
import { Generation } from "../generation.js";
|
|
8
|
+
import { Generation, isTerminal } from "../generation.js";
|
|
9
9
|
import { AIError, AIErrorReason, HttpOptions, InvalidRequestError, ProviderID, UnsupportedOperationError, mergeHttpOptions, } from "../schema/index.js";
|
|
10
10
|
import { encodeJson } from "../utils/json.js";
|
|
11
11
|
import { sanitizeSurrogates } from "../utils/sanitize.js";
|
|
@@ -51,13 +51,17 @@ export const queued = (input) => {
|
|
|
51
51
|
const poll = (operation) => transport
|
|
52
52
|
.call("GET", operation.path(token), http, execute)
|
|
53
53
|
.pipe(Effect.flatMap((sent) => operation.decode(sent.response, { token, auth: sent.auth, materialize })));
|
|
54
|
+
const status = poll(protocol.status);
|
|
54
55
|
const cancel = protocol.cancel;
|
|
56
|
+
const send = cancel === undefined
|
|
57
|
+
? undefined
|
|
58
|
+
: transport.call(cancel.method, cancel.path(token), http, execute).pipe(Effect.asVoid);
|
|
55
59
|
return {
|
|
56
|
-
status
|
|
60
|
+
status,
|
|
57
61
|
result: poll(protocol.result),
|
|
58
|
-
cancel:
|
|
59
|
-
?
|
|
60
|
-
:
|
|
62
|
+
cancel: send !== undefined && cancel?.activeOnly
|
|
63
|
+
? status.pipe(Effect.flatMap((snapshot) => (isTerminal(snapshot.status) ? Effect.void : send)))
|
|
64
|
+
: send,
|
|
61
65
|
};
|
|
62
66
|
};
|
|
63
67
|
const start = Effect.fn("MediaRoute.start")(function* (request, execute) {
|
|
@@ -192,7 +196,11 @@ const encode = (body, headers) => {
|
|
|
192
196
|
apply: HttpClientRequest.bodyFormData(body.value),
|
|
193
197
|
};
|
|
194
198
|
};
|
|
195
|
-
/**
|
|
199
|
+
/**
|
|
200
|
+
* Common fields are never silently dropped: a present field the protocol declared unsupported fails typed. `false`
|
|
201
|
+
* counts as present because some booleans mean something when false (video `audio`); protocols reject opt-in
|
|
202
|
+
* booleans such as speech `timestamps` with `=== true` in `body.from` instead of listing them.
|
|
203
|
+
*/
|
|
196
204
|
const rejectUnsupported = (route, provider, request, unsupported) => {
|
|
197
205
|
const present = (unsupported ?? []).filter((field) => {
|
|
198
206
|
const value = request[field];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
|
-
"version": "0.0.0-dev-
|
|
3
|
+
"version": "0.0.0-dev-20180",
|
|
4
4
|
"name": "@opencode/ai",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@clack/prompts": "1.0.0-alpha.1",
|
|
36
36
|
"@effect/platform-node": "4.0.0-rc.112",
|
|
37
|
-
"@opencode/http-recorder": "0.0.0-dev-
|
|
37
|
+
"@opencode/http-recorder": "0.0.0-dev-20180",
|
|
38
38
|
"@tsconfig/bun": "1.0.9",
|
|
39
39
|
"@types/bun": "1.4.0",
|
|
40
40
|
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"@aws-sdk/credential-providers": "3.1057.0",
|
|
45
45
|
"@smithy/eventstream-codec": "4.2.14",
|
|
46
46
|
"@smithy/util-utf8": "4.2.2",
|
|
47
|
-
"@opencode/schema": "0.0.0-dev-
|
|
47
|
+
"@opencode/schema": "0.0.0-dev-20180",
|
|
48
48
|
"aws4fetch": "1.0.20",
|
|
49
49
|
"effect": "4.0.0-rc.112",
|
|
50
50
|
"google-auth-library": "10.5.0"
|