@opencode/ai 2.0.14 → 2.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -56
- package/dist/experimental/evaluation-client.d.ts +1 -1
- package/dist/experimental/evaluation-client.js +39 -3
- package/dist/experimental/evaluation.d.ts +4 -4
- package/dist/experimental/evaluation.js +2 -2
- package/dist/experimental/system-one.d.ts +3 -3
- package/dist/experimental/system-one.js +40 -51
- package/dist/generation.d.ts +69 -0
- package/dist/generation.js +84 -0
- package/dist/image-client.d.ts +6 -4
- package/dist/image-client.js +11 -11
- package/dist/image.d.ts +1040 -62
- package/dist/image.js +80 -52
- package/dist/index.d.ts +5 -2
- package/dist/index.js +3 -1
- package/dist/llm.d.ts +9 -1
- package/dist/media.d.ts +212 -0
- package/dist/media.js +228 -0
- package/dist/promise.d.ts +554 -0
- package/dist/promise.js +44 -0
- package/dist/protocols/anthropic-messages.js +7 -17
- package/dist/protocols/bedrock-converse.d.ts +4 -4
- package/dist/protocols/bedrock-converse.js +1 -6
- package/dist/protocols/gemini.d.ts +21 -0
- package/dist/protocols/gemini.js +42 -6
- package/dist/protocols/google-images.d.ts +9 -21
- package/dist/protocols/google-images.js +169 -132
- package/dist/protocols/meta-images.d.ts +7 -12
- package/dist/protocols/meta-images.js +92 -66
- package/dist/protocols/mistral-chat.js +7 -6
- package/dist/protocols/open-responses.d.ts +11 -3
- package/dist/protocols/open-responses.js +23 -12
- package/dist/protocols/openai-chat.d.ts +34 -1
- package/dist/protocols/openai-chat.js +100 -31
- package/dist/protocols/openai-images.d.ts +9 -20
- package/dist/protocols/openai-images.js +112 -142
- package/dist/protocols/shared.d.ts +17 -17
- package/dist/protocols/shared.js +32 -35
- package/dist/protocols/utils/bedrock-media.d.ts +2 -3
- package/dist/protocols/utils/bedrock-media.js +4 -4
- package/dist/protocols/utils/media-input.d.ts +10 -0
- package/dist/protocols/utils/media-input.js +17 -0
- package/dist/protocols/utils/responses-compaction.js +6 -5
- package/dist/protocols/utils/tool-stream.d.ts +27 -3
- package/dist/protocols/xai-images.d.ts +9 -15
- package/dist/protocols/xai-images.js +85 -83
- package/dist/protocols/zai-images.d.ts +9 -13
- package/dist/protocols/zai-images.js +59 -57
- package/dist/providers/amazon-bedrock.d.ts +2 -2
- package/dist/providers/cerebras.js +6 -1
- package/dist/providers/deepinfra.js +6 -1
- package/dist/providers/google-vertex.d.ts +7 -0
- package/dist/providers/google.d.ts +7 -0
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.js +1 -0
- package/dist/providers/openrouter.d.ts +19 -0
- package/dist/providers/openrouter.js +13 -1
- package/dist/providers/vercel-ai-gateway.d.ts +41 -0
- package/dist/providers/vercel-ai-gateway.js +85 -0
- package/dist/route/client.d.ts +9 -1
- package/dist/route/endpoint.d.ts +10 -10
- package/dist/route/executor-service.d.ts +12 -0
- package/dist/route/executor-service.js +3 -0
- package/dist/route/executor.d.ts +4 -9
- package/dist/route/executor.js +3 -3
- package/dist/route/index.d.ts +2 -0
- package/dist/route/index.js +2 -0
- package/dist/route/media-protocol.d.ts +45 -0
- package/dist/route/media-protocol.js +40 -0
- package/dist/route/media.d.ts +46 -0
- package/dist/route/media.js +64 -0
- package/dist/schema/errors.d.ts +13 -3
- package/dist/schema/errors.js +7 -0
- package/dist/schema/events.d.ts +563 -40
- package/dist/schema/events.js +35 -2
- package/dist/schema/messages.d.ts +98 -8
- package/dist/schema/messages.js +8 -6
- package/dist/schema/options.d.ts +2 -0
- package/dist/schema/options.js +3 -0
- package/dist/testing.d.ts +72 -8
- package/dist/utils/media-type.d.ts +6 -0
- package/dist/utils/media-type.js +49 -0
- package/dist/utils/sanitize.js +3 -1
- package/package.json +7 -3
- package/dist/protocols/utils/image-input.d.ts +0 -21
- package/dist/protocols/utils/image-input.js +0 -20
|
@@ -1,15 +1,23 @@
|
|
|
1
|
-
import { Effect,
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { ImageModel, ImageResponse } from "../image.js";
|
|
3
|
+
import { Media } from "../media.js";
|
|
4
|
+
import { MediaProtocol } from "../route/media-protocol.js";
|
|
5
|
+
import { MediaRoute } from "../route/media.js";
|
|
6
|
+
import { ProviderID, mergeJsonRecords } from "../schema/index.js";
|
|
6
7
|
import { JsonObject, ProviderShared, optionalNull } from "./shared.js";
|
|
7
|
-
import {
|
|
8
|
+
import { MediaInput } from "./utils/media-input.js";
|
|
9
|
+
const ADAPTER = "meta-images";
|
|
10
|
+
const NAME = "Meta Images";
|
|
11
|
+
const PROVIDER = ProviderID.make("meta");
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// 2. Request body and response schemas
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
8
15
|
const Body = Schema.StructWithRest(Schema.Struct({
|
|
9
16
|
model: Schema.String,
|
|
10
17
|
prompt: Schema.String,
|
|
11
18
|
images: Schema.optional(Schema.Array(JsonObject)),
|
|
12
19
|
n: Schema.optional(Schema.Number),
|
|
20
|
+
/** Aspect ratio hint, not an exact output resolution. */
|
|
13
21
|
size: Schema.optional(Schema.String),
|
|
14
22
|
output_format: Schema.optional(Schema.String),
|
|
15
23
|
response_format: Schema.optional(Schema.String),
|
|
@@ -25,65 +33,83 @@ const Response = Schema.Struct({
|
|
|
25
33
|
total_tokens: Schema.optional(Schema.Number),
|
|
26
34
|
})),
|
|
27
35
|
});
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const { outputFormat, responseFormat, reasoningStrength, toolEnablement, ...native } = request.options ?? {};
|
|
41
|
-
const payload = yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(Body))(mergeJsonRecords({
|
|
42
|
-
model: request.model.id,
|
|
43
|
-
prompt: request.prompt,
|
|
44
|
-
images: images.length === 0 ? undefined : images,
|
|
45
|
-
output_format: outputFormat,
|
|
46
|
-
response_format: responseFormat,
|
|
47
|
-
reasoning_strength: reasoningStrength,
|
|
48
|
-
tool_enablement: toolEnablement,
|
|
49
|
-
}, native, http?.body));
|
|
50
|
-
const body = ProviderShared.encodeJson(payload);
|
|
51
|
-
const url = new URL(`${input.baseURL.replace(/\/$/, "")}/images/${images.length === 0 ? "generations" : "edits"}`);
|
|
52
|
-
Object.entries(http?.query ?? {}).forEach(([key, value]) => url.searchParams.set(key, value));
|
|
53
|
-
const headers = yield* Auth.toEffect(input.auth)({
|
|
54
|
-
request,
|
|
55
|
-
method: "POST",
|
|
56
|
-
url: url.toString(),
|
|
57
|
-
body,
|
|
58
|
-
headers: Headers.fromInput({ ...input.headers, ...http?.headers }),
|
|
59
|
-
});
|
|
60
|
-
const response = yield* execute(HttpClientRequest.post(url.toString()).pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.bodyText(body, "application/json")));
|
|
61
|
-
const output = yield* ProviderShared.imageResponse("meta-images", "Meta Images", response);
|
|
62
|
-
const decoded = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Response))(output.body).pipe(Effect.mapError((cause) => output.invalid("Meta Images returned an invalid response", cause)));
|
|
63
|
-
const format = decoded.output_format ?? payload.output_format ?? "webp";
|
|
64
|
-
const generated = yield* Effect.forEach(decoded.data, (item, index) => {
|
|
65
|
-
if (item.b64_json)
|
|
66
|
-
return Effect.fromResult(Encoding.decodeBase64(item.b64_json)).pipe(Effect.mapError((cause) => output.invalid(`Meta Images result ${index} contains invalid base64`, cause)), Effect.map((data) => new GeneratedImage({ mediaType: `image/${format}`, data })));
|
|
67
|
-
if (item.url)
|
|
68
|
-
return Effect.succeed(new GeneratedImage({ mediaType: `image/${format}`, data: item.url }));
|
|
69
|
-
return output.invalid(`Meta Images result ${index} has neither image data nor a URL`);
|
|
70
|
-
});
|
|
71
|
-
if (generated.length === 0)
|
|
72
|
-
return yield* output.invalid("Meta Images returned no images");
|
|
73
|
-
return new ImageResponse({
|
|
74
|
-
images: generated,
|
|
75
|
-
usage: decoded.usage === undefined
|
|
76
|
-
? undefined
|
|
77
|
-
: new Usage({
|
|
78
|
-
inputTokens: decoded.usage.input_tokens,
|
|
79
|
-
outputTokens: decoded.usage.output_tokens,
|
|
80
|
-
totalTokens: decoded.usage.total_tokens,
|
|
81
|
-
providerMetadata: { meta: decoded.usage },
|
|
82
|
-
}),
|
|
83
|
-
providerMetadata: { meta: { outputFormat: format } },
|
|
84
|
-
});
|
|
85
|
-
}),
|
|
86
|
-
};
|
|
87
|
-
return ImageModel.make({ id: input.id, provider: "meta", route, http: input.http });
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// 5. Request body construction
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
const isEdit = (request) => (request.images?.length ?? 0) > 0;
|
|
40
|
+
const reference = (asset) => {
|
|
41
|
+
const inline = asset.inline();
|
|
42
|
+
if (inline)
|
|
43
|
+
return Effect.succeed({ image_url: inline.dataUrl });
|
|
44
|
+
const url = ProviderShared.mediaUrl(asset);
|
|
45
|
+
if (url)
|
|
46
|
+
return Effect.succeed({ image_url: url });
|
|
47
|
+
return Effect.fail(ProviderShared.invalidRequest(`${NAME} accepts image bytes and URLs`));
|
|
88
48
|
};
|
|
49
|
+
const fromRequest = Effect.fn("MetaImages.fromRequest")(function* (request) {
|
|
50
|
+
const images = yield* Effect.forEach(request.images ?? [], reference);
|
|
51
|
+
const { responseFormat, reasoningStrength, toolEnablement, ...native } = request.providerOptions ?? {};
|
|
52
|
+
const payload = yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(Body))(mergeJsonRecords({
|
|
53
|
+
model: request.model.id,
|
|
54
|
+
prompt: request.prompt,
|
|
55
|
+
images: images.length === 0 ? undefined : images,
|
|
56
|
+
n: request.n,
|
|
57
|
+
size: request.size,
|
|
58
|
+
output_format: request.format,
|
|
59
|
+
response_format: responseFormat,
|
|
60
|
+
reasoning_strength: reasoningStrength,
|
|
61
|
+
tool_enablement: toolEnablement,
|
|
62
|
+
}, native, request.http?.body));
|
|
63
|
+
return MediaProtocol.json(payload);
|
|
64
|
+
});
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// 6. Response decoding
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
const decodeResponse = Effect.fn("MetaImages.decodeResponse")(function* (response, context) {
|
|
69
|
+
const output = yield* MediaProtocol.decodeJson(ADAPTER, NAME, Response)(response);
|
|
70
|
+
const decoded = output.value;
|
|
71
|
+
const requested = context.body.type === "json" ? context.body.value.output_format : undefined;
|
|
72
|
+
const format = decoded.output_format ?? (typeof requested === "string" ? requested : "webp");
|
|
73
|
+
const mediaType = `image/${format}`;
|
|
74
|
+
const images = yield* Effect.forEach(decoded.data, (item, index) => {
|
|
75
|
+
if (item.b64_json)
|
|
76
|
+
return MediaInput.decodedAsset(output.invalid, `${NAME} result ${index}`, item.b64_json, mediaType, {
|
|
77
|
+
info: { format },
|
|
78
|
+
});
|
|
79
|
+
if (item.url)
|
|
80
|
+
return Effect.succeed(Media.url(item.url, { mediaType, info: { format } }));
|
|
81
|
+
return Effect.fail(output.invalid(`${NAME} result ${index} has neither image data nor a URL`));
|
|
82
|
+
});
|
|
83
|
+
if (images.length === 0)
|
|
84
|
+
return yield* output.invalid(`${NAME} returned no images`);
|
|
85
|
+
return new ImageResponse({
|
|
86
|
+
images,
|
|
87
|
+
usage: decoded.usage === undefined
|
|
88
|
+
? undefined
|
|
89
|
+
: {
|
|
90
|
+
type: "tokens",
|
|
91
|
+
input: decoded.usage.input_tokens,
|
|
92
|
+
output: decoded.usage.output_tokens,
|
|
93
|
+
total: decoded.usage.total_tokens,
|
|
94
|
+
details: { meta: decoded.usage },
|
|
95
|
+
},
|
|
96
|
+
providerMetadata: { meta: { outputFormat: format } },
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// 7. Protocol and route
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
export const protocol = MediaProtocol.inline({
|
|
103
|
+
id: ADAPTER,
|
|
104
|
+
name: NAME,
|
|
105
|
+
unsupported: ["mask", "aspectRatio", "seed"],
|
|
106
|
+
body: { from: fromRequest },
|
|
107
|
+
response: { decode: decodeResponse },
|
|
108
|
+
});
|
|
109
|
+
export const model = (input) => ImageModel.fromRoute({
|
|
110
|
+
id: ADAPTER,
|
|
111
|
+
provider: PROVIDER,
|
|
112
|
+
protocol,
|
|
113
|
+
path: ({ request }) => `/images/${isEdit(request) ? "edits" : "generations"}`,
|
|
114
|
+
}, input);
|
|
89
115
|
export * as MetaImages from "./meta-images.js";
|
|
@@ -141,13 +141,14 @@ const MistralEvent = Schema.StructWithRest(Schema.Struct({
|
|
|
141
141
|
}), [Schema.Record(Schema.String, Schema.Unknown)]);
|
|
142
142
|
const MistralStreamEvent = Schema.Union([Schema.Literal(DONE), Protocol.jsonEvent(MistralEvent)]);
|
|
143
143
|
const lowerMedia = Effect.fn("MistralChat.lowerMedia")(function* (part) {
|
|
144
|
-
const
|
|
145
|
-
const url =
|
|
146
|
-
|
|
144
|
+
const mime = part.media.mediaType.toLowerCase();
|
|
145
|
+
const url = ProviderShared.mediaUrl(part.media) ??
|
|
146
|
+
(yield* ProviderShared.requireInlineMedia("Mistral Chat", part.media)).dataUrl;
|
|
147
|
+
if (mime.startsWith("image/"))
|
|
147
148
|
return { type: "image_url", image_url: url };
|
|
148
|
-
if (
|
|
149
|
+
if (mime === "application/pdf")
|
|
149
150
|
return { type: "document_url", document_url: url };
|
|
150
|
-
return yield* ProviderShared.invalidRequest(`Mistral Chat does not support media type ${part.mediaType}`);
|
|
151
|
+
return yield* ProviderShared.invalidRequest(`Mistral Chat does not support media type ${part.media.mediaType}`);
|
|
151
152
|
});
|
|
152
153
|
const lowerUser = Effect.fn("MistralChat.lowerUser")(function* (message) {
|
|
153
154
|
const content = [];
|
|
@@ -227,7 +228,7 @@ const lowerToolResults = Effect.fn("MistralChat.lowerToolResults")(function* (me
|
|
|
227
228
|
content.push({ type: "text", text: item.text });
|
|
228
229
|
continue;
|
|
229
230
|
}
|
|
230
|
-
content.push(yield* lowerMedia(
|
|
231
|
+
content.push(yield* lowerMedia(ProviderShared.toolFileMedia(item)));
|
|
231
232
|
}
|
|
232
233
|
output.push({
|
|
233
234
|
role: "tool",
|
|
@@ -2,7 +2,7 @@ import { Effect, Schema } from "effect";
|
|
|
2
2
|
import { HttpTransport } from "../route/transport/index.js";
|
|
3
3
|
import { Protocol } from "../route/protocol.js";
|
|
4
4
|
import { AIError, LLMEvent, Usage, type LLMRequest, type MediaPart, type ProviderMetadata, type ToolDefinition } from "../schema/index.js";
|
|
5
|
-
import {
|
|
5
|
+
import type { Media } from "../media.js";
|
|
6
6
|
import { OpenResponsesOptions } from "./utils/open-responses-options.js";
|
|
7
7
|
import { Lifecycle } from "./utils/lifecycle.js";
|
|
8
8
|
import { ToolStream } from "./utils/tool-stream.js";
|
|
@@ -689,7 +689,7 @@ export interface ProviderAdapter {
|
|
|
689
689
|
}, AIError>;
|
|
690
690
|
readonly lowerMedia?: (input: {
|
|
691
691
|
readonly part: MediaPart;
|
|
692
|
-
readonly media:
|
|
692
|
+
readonly media: Media.Inline | undefined;
|
|
693
693
|
readonly request: LLMRequest;
|
|
694
694
|
}) => MediaInput | undefined;
|
|
695
695
|
readonly restoreHostedToolItem?: (item: unknown) => HostedToolReplayItem | undefined;
|
|
@@ -1177,6 +1177,14 @@ export declare const step: (state: ParserState, event: NormalizedEvent) => AIErr
|
|
|
1177
1177
|
};
|
|
1178
1178
|
} | undefined;
|
|
1179
1179
|
readonly namespace?: string | undefined;
|
|
1180
|
+
} | {
|
|
1181
|
+
readonly type: "media";
|
|
1182
|
+
readonly media: Media.Asset;
|
|
1183
|
+
readonly providerMetadata?: {
|
|
1184
|
+
readonly [x: string]: {
|
|
1185
|
+
readonly [x: string]: unknown;
|
|
1186
|
+
};
|
|
1187
|
+
} | undefined;
|
|
1180
1188
|
} | {
|
|
1181
1189
|
readonly type: "step-finish";
|
|
1182
1190
|
readonly reason: {
|
|
@@ -1205,12 +1213,12 @@ export declare const step: (state: ParserState, event: NormalizedEvent) => AIErr
|
|
|
1205
1213
|
} | {
|
|
1206
1214
|
readonly type: "provider-error";
|
|
1207
1215
|
readonly message: string;
|
|
1216
|
+
readonly classification?: "context-overflow" | "payload-too-large" | undefined;
|
|
1208
1217
|
readonly providerMetadata?: {
|
|
1209
1218
|
readonly [x: string]: {
|
|
1210
1219
|
readonly [x: string]: unknown;
|
|
1211
1220
|
};
|
|
1212
1221
|
} | undefined;
|
|
1213
|
-
readonly classification?: "context-overflow" | "payload-too-large" | undefined;
|
|
1214
1222
|
})[]], AIError, never> | Effect.Effect<StepResult, never, never>;
|
|
1215
1223
|
/**
|
|
1216
1224
|
* The provider-neutral Open Responses protocol. Provider-specific Responses
|
|
@@ -335,27 +335,27 @@ const lowerReasoning = (part, providerMetadataKey) => {
|
|
|
335
335
|
};
|
|
336
336
|
};
|
|
337
337
|
const lowerMedia = Effect.fn("OpenResponses.lowerMedia")(function* (part, request, adapter, target) {
|
|
338
|
-
const media =
|
|
338
|
+
const media = part.media.inline();
|
|
339
339
|
const providerMedia = adapter.lowerMedia?.({ part, media, request });
|
|
340
340
|
if (providerMedia)
|
|
341
341
|
return providerMedia;
|
|
342
342
|
const detail = yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(OpenResponsesInputImage.fields.detail))(part.providerMetadata?.[metadataKey(request.model)]?.detail);
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
if (
|
|
347
|
-
if (target === "tool-result" && media.
|
|
348
|
-
return { type: "input_video", video_url:
|
|
343
|
+
const mime = part.media.mediaType.toLowerCase();
|
|
344
|
+
const url = ProviderShared.mediaUrl(part.media);
|
|
345
|
+
const location = url ?? (yield* ProviderShared.requireInlineMedia(adapter.name, part.media)).dataUrl;
|
|
346
|
+
if (part.media.kind !== "image") {
|
|
347
|
+
if (target === "tool-result" && part.media.kind === "video")
|
|
348
|
+
return { type: "input_video", video_url: location };
|
|
349
349
|
return {
|
|
350
350
|
type: "input_file",
|
|
351
|
-
filename: part.filename ?? (
|
|
351
|
+
filename: part.filename ?? (mime === "application/pdf" ? "document.pdf" : "file"),
|
|
352
352
|
detail,
|
|
353
|
-
...(url ? { file_url: url } : { file_data:
|
|
353
|
+
...(url ? { file_url: url } : { file_data: location }),
|
|
354
354
|
};
|
|
355
355
|
}
|
|
356
356
|
return {
|
|
357
357
|
type: "input_image",
|
|
358
|
-
image_url:
|
|
358
|
+
image_url: location,
|
|
359
359
|
detail,
|
|
360
360
|
};
|
|
361
361
|
});
|
|
@@ -377,12 +377,12 @@ const lowerMessageMedia = Effect.fnUntraced(function* (part, request, adapter) {
|
|
|
377
377
|
const lowerToolResultContentItem = Effect.fnUntraced(function* (item, request, adapter) {
|
|
378
378
|
if (item.type === "text")
|
|
379
379
|
return { type: "input_text", text: item.text };
|
|
380
|
-
return yield* lowerMedia(
|
|
380
|
+
return yield* lowerMedia(ProviderShared.toolFileMedia(item), request, adapter, "tool-result");
|
|
381
381
|
});
|
|
382
382
|
const lowerHostedToolResultContentItem = Effect.fnUntraced(function* (item, request, adapter) {
|
|
383
383
|
if (item.type === "text")
|
|
384
384
|
return { type: "input_text", text: item.text };
|
|
385
|
-
return yield* lowerMessageMedia(
|
|
385
|
+
return yield* lowerMessageMedia(ProviderShared.toolFileMedia(item), request, adapter);
|
|
386
386
|
});
|
|
387
387
|
const lowerToolResultOutput = Effect.fnUntraced(function* (part, request, adapter) {
|
|
388
388
|
// Text/json/error results are encoded as a plain string for backward
|
|
@@ -513,11 +513,22 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (reques
|
|
|
513
513
|
});
|
|
514
514
|
continue;
|
|
515
515
|
}
|
|
516
|
+
if (part.type === "media") {
|
|
517
|
+
flushText();
|
|
518
|
+
// Responses has no assistant-authored image item; replay generated media (e.g. from Gemini) as user input.
|
|
519
|
+
input.push({
|
|
520
|
+
type: "message",
|
|
521
|
+
role: "user",
|
|
522
|
+
content: [yield* lowerMessageMedia(part, request, adapter)],
|
|
523
|
+
});
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
516
526
|
return yield* ProviderShared.unsupportedContent(adapter.name, "assistant", [
|
|
517
527
|
"text",
|
|
518
528
|
"reasoning",
|
|
519
529
|
"tool-call",
|
|
520
530
|
"tool-result",
|
|
531
|
+
"media",
|
|
521
532
|
]);
|
|
522
533
|
}
|
|
523
534
|
flushText();
|
|
@@ -12,6 +12,37 @@ declare const OpenAIChatCacheControl: Schema.Struct<{
|
|
|
12
12
|
readonly type: Schema.Literal<"ephemeral">;
|
|
13
13
|
readonly ttl: Schema.optional<Schema.String>;
|
|
14
14
|
}>;
|
|
15
|
+
declare const ReasoningDetail: Schema.Union<readonly [Schema.StructWithRest<Schema.Struct<{
|
|
16
|
+
readonly id: Schema.optional<Schema.NullOr<Schema.String>>;
|
|
17
|
+
readonly format: Schema.optional<Schema.String>;
|
|
18
|
+
readonly index: Schema.optional<Schema.Number>;
|
|
19
|
+
readonly signature: Schema.optional<Schema.NullOr<Schema.String>>;
|
|
20
|
+
readonly type: Schema.Literal<"reasoning.text">;
|
|
21
|
+
readonly text: Schema.optional<Schema.String>;
|
|
22
|
+
}>, readonly [Schema.$Record<Schema.String, Schema.Unknown>]>, Schema.StructWithRest<Schema.Struct<{
|
|
23
|
+
readonly id: Schema.optional<Schema.NullOr<Schema.String>>;
|
|
24
|
+
readonly format: Schema.optional<Schema.String>;
|
|
25
|
+
readonly index: Schema.optional<Schema.Number>;
|
|
26
|
+
readonly signature: Schema.optional<Schema.NullOr<Schema.String>>;
|
|
27
|
+
readonly type: Schema.Literal<"reasoning.summary">;
|
|
28
|
+
readonly summary: Schema.optional<Schema.String>;
|
|
29
|
+
}>, readonly [Schema.$Record<Schema.String, Schema.Unknown>]>, Schema.StructWithRest<Schema.Struct<{
|
|
30
|
+
readonly id: Schema.optional<Schema.NullOr<Schema.String>>;
|
|
31
|
+
readonly format: Schema.optional<Schema.String>;
|
|
32
|
+
readonly index: Schema.optional<Schema.Number>;
|
|
33
|
+
readonly signature: Schema.optional<Schema.NullOr<Schema.String>>;
|
|
34
|
+
readonly type: Schema.Literal<"reasoning.encrypted">;
|
|
35
|
+
readonly data: Schema.String;
|
|
36
|
+
}>, readonly [Schema.$Record<Schema.String, Schema.Unknown>]>, Schema.Struct<{
|
|
37
|
+
readonly type: Schema.Literal<"summary">;
|
|
38
|
+
readonly summary: Schema.String;
|
|
39
|
+
readonly index: Schema.optional<Schema.Number>;
|
|
40
|
+
}>, Schema.Struct<{
|
|
41
|
+
readonly type: Schema.Literal<"encrypted">;
|
|
42
|
+
readonly encrypted: Schema.String;
|
|
43
|
+
readonly index: Schema.optional<Schema.Number>;
|
|
44
|
+
}>]>;
|
|
45
|
+
type ReasoningDetail = Schema.Schema.Type<typeof ReasoningDetail>;
|
|
15
46
|
export declare const bodyFields: {
|
|
16
47
|
model: Schema.String;
|
|
17
48
|
messages: Schema.$Array<Schema.toTaggedUnion<"role", readonly [Schema.Struct<{
|
|
@@ -278,7 +309,9 @@ export interface ParserState {
|
|
|
278
309
|
readonly finishReason?: FinishReasonDetails;
|
|
279
310
|
readonly lifecycle: Lifecycle.State;
|
|
280
311
|
readonly reasoningField?: string;
|
|
281
|
-
|
|
312
|
+
/** A scalar reasoning field (`reasoning_content`, ...) has carried text in this stream. */
|
|
313
|
+
readonly reasoningTextObserved: boolean;
|
|
314
|
+
readonly reasoningDetails: Array<ReasoningDetail>;
|
|
282
315
|
readonly reasoningDetailsObserved: boolean;
|
|
283
316
|
readonly reasoningEmitted: boolean;
|
|
284
317
|
readonly latestToolIndex?: number;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Effect, Schema } from "effect";
|
|
1
|
+
import { Effect, Option, Schema } from "effect";
|
|
2
2
|
import { Tool } from "@opencode/schema/tool";
|
|
3
3
|
import { Route } from "../route/client.js";
|
|
4
4
|
import { Auth } from "../route/auth.js";
|
|
@@ -50,6 +50,32 @@ const OpenAIChatAssistantToolCall = Schema.Struct({
|
|
|
50
50
|
arguments: Schema.String,
|
|
51
51
|
}),
|
|
52
52
|
});
|
|
53
|
+
// `reasoning_details` carries two dialects. OpenRouter's `reasoning.*` entries
|
|
54
|
+
// must be replayed unmodified (`index` included), so they keep every field they
|
|
55
|
+
// arrived with. Kimi's OpenAI-compatible surface streams preserved thinking as
|
|
56
|
+
// bare `summary` / `encrypted` entries keyed by a stream-only `index`; Kimi does
|
|
57
|
+
// not document this publicly, so the handling follows Kimi Code (Kimi's own
|
|
58
|
+
// client): merge summary deltas by `index`, replay without `index`, and always
|
|
59
|
+
// send `reasoning_content` alongside. Anything else is dropped at the boundary.
|
|
60
|
+
const OpenRouterDetailFields = {
|
|
61
|
+
id: Schema.optional(Schema.NullOr(Schema.String)),
|
|
62
|
+
format: Schema.optional(Schema.String),
|
|
63
|
+
index: Schema.optional(Schema.Number),
|
|
64
|
+
signature: Schema.optional(Schema.NullOr(Schema.String)),
|
|
65
|
+
};
|
|
66
|
+
const ReasoningDetail = Schema.Union([
|
|
67
|
+
Schema.StructWithRest(Schema.Struct({ type: Schema.Literal("reasoning.text"), text: Schema.optional(Schema.String), ...OpenRouterDetailFields }), [Schema.Record(Schema.String, Schema.Unknown)]),
|
|
68
|
+
Schema.StructWithRest(Schema.Struct({
|
|
69
|
+
type: Schema.Literal("reasoning.summary"),
|
|
70
|
+
summary: Schema.optional(Schema.String),
|
|
71
|
+
...OpenRouterDetailFields,
|
|
72
|
+
}), [Schema.Record(Schema.String, Schema.Unknown)]),
|
|
73
|
+
Schema.StructWithRest(Schema.Struct({ type: Schema.Literal("reasoning.encrypted"), data: Schema.String, ...OpenRouterDetailFields }), [Schema.Record(Schema.String, Schema.Unknown)]),
|
|
74
|
+
Schema.Struct({ type: Schema.Literal("summary"), summary: Schema.String, index: Schema.optional(Schema.Number) }),
|
|
75
|
+
Schema.Struct({ type: Schema.Literal("encrypted"), encrypted: Schema.String, index: Schema.optional(Schema.Number) }),
|
|
76
|
+
]);
|
|
77
|
+
const decodeReasoningDetail = Schema.decodeUnknownOption(ReasoningDetail);
|
|
78
|
+
const knownReasoningDetails = (details) => details.flatMap((detail) => Option.toArray(decodeReasoningDetail(detail)));
|
|
53
79
|
// Intentionally omit Gemini's provider-specific `extra_content.google.thought_signature`
|
|
54
80
|
// extension until direct Google OpenAI-compatible routing is supported here:
|
|
55
81
|
// https://github.com/vercel/ai/issues/11590
|
|
@@ -204,12 +230,9 @@ const lowerToolCall = (part, options) => ({
|
|
|
204
230
|
},
|
|
205
231
|
});
|
|
206
232
|
const lowerMedia = Effect.fn("OpenAIChat.lowerMedia")(function* (part) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const url = typeof part.data === "string" && (part.data.startsWith("https://") || part.data.startsWith("http://"))
|
|
211
|
-
? part.data
|
|
212
|
-
: media.dataUrl;
|
|
233
|
+
if (part.media.kind !== "image")
|
|
234
|
+
return yield* ProviderShared.invalidRequest(`OpenAI Chat does not support media type ${part.media.mediaType}`);
|
|
235
|
+
const url = ProviderShared.mediaUrl(part.media) ?? (yield* ProviderShared.requireInlineMedia("OpenAI Chat", part.media)).dataUrl;
|
|
213
236
|
return { type: "image_url", image_url: { url } };
|
|
214
237
|
});
|
|
215
238
|
const openAICompatibleReasoningContent = (native) => isRecord(native) && typeof native.reasoning_content === "string" ? native.reasoning_content : undefined;
|
|
@@ -223,10 +246,20 @@ const reasoningDetails = (parts, native, providerMetadataKey) => {
|
|
|
223
246
|
return Array.isArray(details) ? details : [];
|
|
224
247
|
});
|
|
225
248
|
if (parts.some((part) => Array.isArray(part.providerMetadata?.[providerMetadataKey]?.reasoningDetails)))
|
|
226
|
-
return observed;
|
|
249
|
+
return knownReasoningDetails(observed).map(lowerReasoningDetail);
|
|
227
250
|
if (isRecord(native) && Array.isArray(native.reasoning_details))
|
|
228
|
-
return native.reasoning_details;
|
|
251
|
+
return knownReasoningDetails(native.reasoning_details).map(lowerReasoningDetail);
|
|
252
|
+
};
|
|
253
|
+
// Kimi rejects its stream-only `index` on requests
|
|
254
|
+
// ("the reasoning_details ... must not contain streaming index").
|
|
255
|
+
const lowerReasoningDetail = (detail) => {
|
|
256
|
+
if (detail.type === "summary")
|
|
257
|
+
return { type: detail.type, summary: detail.summary };
|
|
258
|
+
if (detail.type === "encrypted")
|
|
259
|
+
return { type: detail.type, encrypted: detail.encrypted };
|
|
260
|
+
return detail;
|
|
229
261
|
};
|
|
262
|
+
const isKimiDetail = (detail) => detail.type === "summary" || detail.type === "encrypted";
|
|
230
263
|
const lowerUserMessage = Effect.fn("OpenAIChat.lowerUserMessage")(function* (message, options) {
|
|
231
264
|
const content = [];
|
|
232
265
|
for (const part of message.content) {
|
|
@@ -285,6 +318,10 @@ const lowerAssistantMessage = Effect.fn("OpenAIChat.lowerAssistantMessage")(func
|
|
|
285
318
|
return "reasoning_content";
|
|
286
319
|
if (!fullyStructured || requireReasoning)
|
|
287
320
|
return "reasoning_content";
|
|
321
|
+
// Kimi always expects `reasoning_content` on replayed assistant messages,
|
|
322
|
+
// even when thinking arrived only through structured details.
|
|
323
|
+
if (details?.some(isKimiDetail))
|
|
324
|
+
return "reasoning_content";
|
|
288
325
|
})();
|
|
289
326
|
const reasoningText = (() => {
|
|
290
327
|
if (configuredField !== undefined)
|
|
@@ -330,7 +367,7 @@ const lowerToolMessages = Effect.fn("OpenAIChat.lowerToolMessages")(function* (m
|
|
|
330
367
|
cache_control: options.cacheControl?.(part.cache),
|
|
331
368
|
});
|
|
332
369
|
const files = content.filter((item) => item.type === "file");
|
|
333
|
-
images.push(...(yield* Effect.forEach(files, (item) => lowerMedia(
|
|
370
|
+
images.push(...(yield* Effect.forEach(files, (item) => lowerMedia(ProviderShared.toolFileMedia(item)))));
|
|
334
371
|
}
|
|
335
372
|
return { messages, images };
|
|
336
373
|
});
|
|
@@ -708,14 +745,16 @@ const reasoningDelta = (delta, configuredField) => {
|
|
|
708
745
|
}
|
|
709
746
|
return undefined;
|
|
710
747
|
};
|
|
711
|
-
const detailText = (details) => {
|
|
748
|
+
const detailText = (details, hideKimiSummary) => {
|
|
712
749
|
const text = details.flatMap((detail) => {
|
|
713
|
-
if (
|
|
714
|
-
return [];
|
|
715
|
-
if (detail.type === "reasoning.
|
|
716
|
-
return [detail.
|
|
717
|
-
|
|
718
|
-
|
|
750
|
+
if (detail.type === "reasoning.text")
|
|
751
|
+
return detail.text ? [detail.text] : [];
|
|
752
|
+
if (detail.type === "reasoning.summary")
|
|
753
|
+
return detail.summary ? [detail.summary] : [];
|
|
754
|
+
// Kimi streams the full thinking through `reasoning_content` and a separate
|
|
755
|
+
// summary through details; show the summary only when nothing else does.
|
|
756
|
+
if (detail.type === "summary")
|
|
757
|
+
return detail.summary && !hideKimiSummary ? [detail.summary] : [];
|
|
719
758
|
return [];
|
|
720
759
|
});
|
|
721
760
|
if (text.length > 0)
|
|
@@ -724,25 +763,48 @@ const detailText = (details) => {
|
|
|
724
763
|
const appendReasoningDetails = (result, details) => {
|
|
725
764
|
for (const detail of details) {
|
|
726
765
|
const previous = result.at(-1);
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
!isRecord(detail) ||
|
|
730
|
-
detail.type !== "reasoning.text" ||
|
|
731
|
-
conflictingReasoningTextDetails(previous, detail)) {
|
|
766
|
+
const merged = previous === undefined ? undefined : mergeReasoningDetails(previous, detail);
|
|
767
|
+
if (merged === undefined) {
|
|
732
768
|
result.push(detail);
|
|
733
769
|
continue;
|
|
734
770
|
}
|
|
735
|
-
result[result.length - 1] =
|
|
771
|
+
result[result.length - 1] = merged;
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
// Consecutive text or summary deltas of the same kind accumulate into one
|
|
775
|
+
// entry; encrypted entries are opaque and never merge.
|
|
776
|
+
const mergeReasoningDetails = (previous, detail) => {
|
|
777
|
+
if (conflictingReasoningDetails(previous, detail))
|
|
778
|
+
return undefined;
|
|
779
|
+
if (previous.type === "reasoning.text" && detail.type === "reasoning.text")
|
|
780
|
+
return {
|
|
736
781
|
...previous,
|
|
737
|
-
...
|
|
738
|
-
text: `${
|
|
739
|
-
|
|
740
|
-
format: mergeDetailValue(previous.format, detail.format),
|
|
782
|
+
...detail,
|
|
783
|
+
text: `${previous.text ?? ""}${detail.text ?? ""}`,
|
|
784
|
+
...mergeDetailIdentity(previous, detail),
|
|
741
785
|
};
|
|
742
|
-
|
|
786
|
+
if (previous.type === "reasoning.summary" && detail.type === "reasoning.summary")
|
|
787
|
+
return {
|
|
788
|
+
...previous,
|
|
789
|
+
...detail,
|
|
790
|
+
summary: `${previous.summary ?? ""}${detail.summary ?? ""}`,
|
|
791
|
+
...mergeDetailIdentity(previous, detail),
|
|
792
|
+
};
|
|
793
|
+
if (previous.type === "summary" && detail.type === "summary")
|
|
794
|
+
return { ...previous, ...detail, summary: previous.summary + detail.summary };
|
|
795
|
+
};
|
|
796
|
+
// The first non-empty signature and format win; a later delta may carry the
|
|
797
|
+
// signature for text that streamed earlier.
|
|
798
|
+
const mergeDetailIdentity = (previous, current) => {
|
|
799
|
+
const signature = mergeDetailValue(previous.signature, current.signature);
|
|
800
|
+
const format = mergeDetailValue(previous.format, current.format);
|
|
801
|
+
return {
|
|
802
|
+
...(signature === undefined ? {} : { signature }),
|
|
803
|
+
...(format === undefined ? {} : { format }),
|
|
804
|
+
};
|
|
743
805
|
};
|
|
744
806
|
const mergeDetailValue = (previous, current) => previous || current || (previous !== undefined ? previous : current);
|
|
745
|
-
const
|
|
807
|
+
const conflictingReasoningDetails = (previous, current) => conflictingDetailValue(previous.id, current.id) ||
|
|
746
808
|
conflictingDetailValue(previous.index, current.index) ||
|
|
747
809
|
conflictingDetailValue(previous.format, current.format) ||
|
|
748
810
|
(Boolean(previous.signature) && Boolean(current.signature) && previous.signature !== current.signature);
|
|
@@ -798,12 +860,17 @@ const step = (state, event) => Effect.gen(function* () {
|
|
|
798
860
|
return [{ ...state, usage }, events];
|
|
799
861
|
}
|
|
800
862
|
const reasoningField = state.reasoningField ?? reasoning?.field;
|
|
801
|
-
const
|
|
863
|
+
const reasoningTextObserved = state.reasoningTextObserved || reasoning !== undefined;
|
|
864
|
+
const detailDelta = Array.isArray(delta?.reasoning_details)
|
|
865
|
+
? knownReasoningDetails(delta.reasoning_details)
|
|
866
|
+
: undefined;
|
|
802
867
|
if (detailDelta !== undefined)
|
|
803
868
|
appendReasoningDetails(state.reasoningDetails, detailDelta);
|
|
804
869
|
const reasoningDetailsObserved = state.reasoningDetailsObserved || detailDelta !== undefined;
|
|
805
870
|
const deltaMetadata = reasoningMetadata(state.providerMetadataKey, reasoningField);
|
|
806
|
-
const text = detailDelta?.length
|
|
871
|
+
const text = detailDelta?.length
|
|
872
|
+
? (detailText(detailDelta, reasoningTextObserved) ?? reasoning?.text)
|
|
873
|
+
: reasoning?.text;
|
|
807
874
|
if (text !== undefined)
|
|
808
875
|
lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", text, deltaMetadata);
|
|
809
876
|
else if (reasoningDetailsObserved &&
|
|
@@ -881,6 +948,7 @@ const step = (state, event) => Effect.gen(function* () {
|
|
|
881
948
|
finishReason,
|
|
882
949
|
lifecycle,
|
|
883
950
|
reasoningField,
|
|
951
|
+
reasoningTextObserved,
|
|
884
952
|
reasoningDetails: state.reasoningDetails,
|
|
885
953
|
reasoningDetailsObserved,
|
|
886
954
|
reasoningEmitted,
|
|
@@ -947,6 +1015,7 @@ export const protocol = Protocol.make({
|
|
|
947
1015
|
toolCallEvents: [],
|
|
948
1016
|
lifecycle: Lifecycle.initial(),
|
|
949
1017
|
reasoningField: request.model.compatibility?.reasoningField,
|
|
1018
|
+
reasoningTextObserved: false,
|
|
950
1019
|
reasoningDetails: [],
|
|
951
1020
|
reasoningDetailsObserved: false,
|
|
952
1021
|
reasoningEmitted: false,
|
|
@@ -1,32 +1,21 @@
|
|
|
1
|
-
import { ImageModel, type
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { ImageModel, ImageResponse, type ImageRequestFor } from "../image.js";
|
|
2
|
+
import { MediaProtocol } from "../route/media-protocol.js";
|
|
3
|
+
import { MediaRoute } from "../route/media.js";
|
|
4
4
|
export declare const DEFAULT_BASE_URL = "https://api.openai.com/v1";
|
|
5
5
|
export declare const PATH = "/images/generations";
|
|
6
6
|
export declare const EDIT_PATH = "/images/edits";
|
|
7
7
|
export type OpenAIImageString<Known extends string> = Known | (string & {});
|
|
8
|
+
/** Provider-native options. Common fields (`n`, `size`, `format`, `images`, `mask`) live on the request. */
|
|
8
9
|
export type OpenAIImageOptions = {
|
|
9
|
-
readonly mask?: ImageInput;
|
|
10
|
-
readonly n?: number;
|
|
11
|
-
readonly size?: OpenAIImageString<"auto" | "256x256" | "512x512" | "1024x1024" | "1536x1024" | "1024x1536" | "1792x1024" | "1024x1792">;
|
|
12
10
|
readonly quality?: OpenAIImageString<"auto" | "low" | "medium" | "high" | "standard" | "hd">;
|
|
13
11
|
readonly background?: OpenAIImageString<"auto" | "opaque" | "transparent">;
|
|
14
12
|
readonly moderation?: OpenAIImageString<"auto" | "low">;
|
|
15
|
-
readonly outputFormat?: OpenAIImageString<"png" | "jpeg" | "webp">;
|
|
16
13
|
readonly outputCompression?: number;
|
|
17
14
|
} & Record<string, unknown>;
|
|
18
|
-
export type
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
};
|
|
22
|
-
export interface ModelInput {
|
|
23
|
-
readonly id: string;
|
|
24
|
-
readonly auth: AuthDefinition;
|
|
25
|
-
readonly baseURL?: string;
|
|
26
|
-
readonly headers?: Record<string, string>;
|
|
27
|
-
readonly http?: HttpOptions;
|
|
28
|
-
}
|
|
29
|
-
export declare const model: (input: ModelInput) => ImageModel<OpenAIImageOptions>;
|
|
15
|
+
export type Request = ImageRequestFor<OpenAIImageOptions>;
|
|
16
|
+
export declare const protocol: MediaProtocol.Inline<Request, ImageResponse>;
|
|
17
|
+
export declare const model: (input: MediaRoute.ModelInput) => ImageModel<OpenAIImageOptions>;
|
|
30
18
|
export declare const OpenAIImages: {
|
|
31
|
-
readonly
|
|
19
|
+
readonly protocol: MediaProtocol.Inline<Request, ImageResponse>;
|
|
20
|
+
readonly model: (input: MediaRoute.ModelInput) => ImageModel<OpenAIImageOptions>;
|
|
32
21
|
};
|