@opencode/ai 0.0.0-dev-20170 → 0.0.0-dev-20175
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 +12 -12
- package/dist/generation.d.ts +1 -0
- package/dist/generation.js +2 -1
- 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/fal-images.js +15 -12
- package/dist/protocols/openai-responses.js +5 -2
- package/dist/protocols/runway-video.js +1 -1
- package/dist/route/media-protocol.d.ts +5 -0
- package/dist/route/media.js +9 -5
- 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.
|
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. */
|
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)));
|
|
@@ -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
|
},
|
|
@@ -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) => ({
|
|
@@ -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;
|
|
@@ -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) {
|
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-20175",
|
|
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-20175",
|
|
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-20175",
|
|
48
48
|
"aws4fetch": "1.0.20",
|
|
49
49
|
"effect": "4.0.0-rc.112",
|
|
50
50
|
"google-auth-library": "10.5.0"
|