@linxiraos/pi-ai 1.2.1 → 1.3.1
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/CHANGELOG.md +98 -0
- package/README.md +4 -2
- package/THIRD-PARTY-NOTICES.txt +55 -5
- package/dist/types/auth/cascade.d.ts +5 -3
- package/dist/types/auth/pool.d.ts +3 -3
- package/dist/types/auth/select.d.ts +5 -1
- package/dist/types/auth/types.d.ts +15 -5
- package/dist/types/auth/usage-cache.d.ts +3 -1
- package/dist/types/auth-gateway/clients.d.ts +67 -0
- package/dist/types/auth-gateway/dispatch.d.ts +28 -10
- package/dist/types/auth-gateway/http.d.ts +2 -0
- package/dist/types/auth-gateway/index.d.ts +1 -0
- package/dist/types/auth-gateway/server.d.ts +9 -4
- package/dist/types/auth-gateway/session-state.d.ts +6 -0
- package/dist/types/auth-gateway/types.d.ts +16 -2
- package/dist/types/dialect/coercion.d.ts +6 -0
- package/dist/types/dialect/deepseek.d.ts +13 -0
- package/dist/types/error/classes.d.ts +1 -1
- package/dist/types/error/flags.d.ts +15 -0
- package/dist/types/error/index.d.ts +1 -0
- package/dist/types/error/network-cause.d.ts +40 -0
- package/dist/types/images/shared.d.ts +26 -2
- package/dist/types/index.d.ts +1 -0
- package/dist/types/judgment/native.d.ts +62 -3
- package/dist/types/judgment/systemone.d.ts +9 -2
- package/dist/types/judgment/text.d.ts +11 -5
- package/dist/types/judgment/types.d.ts +37 -6
- package/dist/types/judgment/wire.d.ts +18 -3
- package/dist/types/provider-session-state.d.ts +2 -2
- package/dist/types/providers/anthropic-images.d.ts +7 -0
- package/dist/types/providers/anthropic-signature.d.ts +12 -2
- package/dist/types/providers/anthropic-state.d.ts +7 -0
- package/dist/types/providers/anthropic.d.ts +21 -6
- package/dist/types/providers/connection-prewarm.d.ts +15 -0
- package/dist/types/providers/cursor/exec-modern.d.ts +1 -1
- package/dist/types/providers/cursor-pi-args.d.ts +13 -0
- package/dist/types/providers/cursor.d.ts +7 -0
- package/dist/types/providers/ollama.d.ts +6 -1
- package/dist/types/providers/openai-codex-responses.d.ts +6 -0
- package/dist/types/providers/openai-responses.d.ts +9 -0
- package/dist/types/providers/openai-shared.d.ts +30 -0
- package/dist/types/providers/systemone-server.d.ts +7 -3
- package/dist/types/providers/transform-messages.d.ts +28 -5
- package/dist/types/registry/oauth/google-antigravity.d.ts +1 -1
- package/dist/types/stream.d.ts +0 -2
- package/dist/types/types.d.ts +33 -9
- package/dist/types/usage/muse-code.d.ts +1 -1
- package/dist/types/utils/dsml-leak.d.ts +17 -13
- package/dist/types/utils/narration.d.ts +9 -0
- package/dist/types/utils/openai-http.d.ts +2 -0
- package/dist/types/utils/request-body.d.ts +11 -0
- package/dist/types/utils/request-compression.d.ts +21 -0
- package/dist/types/utils/tool-call-arguments.d.ts +2 -1
- package/dist/types/utils/transport-fetch.d.ts +8 -0
- package/dist/types/utils.d.ts +1 -1
- package/package.json +7 -7
- package/src/auth/cascade.ts +23 -13
- package/src/auth/pool.ts +3 -3
- package/src/auth/refresh.ts +39 -12
- package/src/auth/select.ts +8 -2
- package/src/auth/types.ts +16 -5
- package/src/auth/usage-cache.ts +23 -7
- package/src/auth/usage.ts +2 -4
- package/src/auth-broker/remote-store.ts +31 -6
- package/src/auth-gateway/clients.ts +318 -0
- package/src/auth-gateway/dispatch.ts +66 -35
- package/src/auth-gateway/http.ts +11 -5
- package/src/auth-gateway/index.ts +1 -0
- package/src/auth-gateway/routes/embeddings.ts +4 -10
- package/src/auth-gateway/routes/images.ts +6 -10
- package/src/auth-gateway/routes/rerank.ts +4 -10
- package/src/auth-gateway/routes/speech.ts +10 -10
- package/src/auth-gateway/routes/systemone.ts +22 -12
- package/src/auth-gateway/routes/transcriptions.ts +4 -2
- package/src/auth-gateway/routes/video.ts +12 -15
- package/src/auth-gateway/server.ts +136 -53
- package/src/auth-gateway/session-state.ts +14 -0
- package/src/auth-gateway/types.ts +16 -2
- package/src/dialect/coercion.ts +16 -0
- package/src/dialect/deepseek.ts +344 -81
- package/src/dialect/hermes.ts +7 -11
- package/src/dialect/qwen3.ts +3 -11
- package/src/error/classes.ts +3 -2
- package/src/error/flags.ts +24 -3
- package/src/error/index.ts +1 -0
- package/src/error/network-cause.ts +179 -0
- package/src/images/codex-format-directive.md +3 -0
- package/src/images/google-antigravity.ts +2 -1
- package/src/images/google-generative-ai.ts +4 -2
- package/src/images/openai-hosted.ts +25 -2
- package/src/images/openai-images.ts +11 -6
- package/src/images/shared.ts +77 -3
- package/src/index.ts +1 -0
- package/src/judgment/chat.ts +17 -1
- package/src/judgment/native.ts +238 -12
- package/src/judgment/openai-decisions.ts +30 -6
- package/src/judgment/systemone.ts +153 -11
- package/src/judgment/text.ts +26 -7
- package/src/judgment/types.ts +46 -6
- package/src/judgment/wire.ts +19 -3
- package/src/provider-session-state.ts +2 -2
- package/src/providers/amazon-bedrock.ts +4 -1
- package/src/providers/anthropic-client.ts +1 -1
- package/src/providers/anthropic-images.ts +263 -0
- package/src/providers/anthropic-messages-server.ts +84 -1
- package/src/providers/anthropic-signature.ts +35 -8
- package/src/providers/anthropic-state.ts +13 -0
- package/src/providers/anthropic.ts +479 -397
- package/src/providers/connection-prewarm.ts +78 -0
- package/src/providers/cowork-fetch.ts +5 -1
- package/src/providers/cursor/exec-modern.ts +1 -0
- package/src/providers/cursor-pi-args.ts +19 -0
- package/src/providers/cursor.ts +53 -13
- package/src/providers/devin.ts +10 -4
- package/src/providers/factory-droid/gemini.ts +9 -3
- package/src/providers/google-gemini-cli.ts +11 -1
- package/src/providers/google-shared.ts +2 -6
- package/src/providers/images-server.ts +6 -1
- package/src/providers/ollama.ts +23 -5
- package/src/providers/openai-anthropic-shim.ts +1 -0
- package/src/providers/openai-chat-server.ts +29 -0
- package/src/providers/openai-codex/access-programs.ts +2 -1
- package/src/providers/openai-codex-responses.ts +49 -82
- package/src/providers/openai-completions.ts +117 -19
- package/src/providers/openai-responses.ts +25 -2
- package/src/providers/openai-shared.ts +36 -5
- package/src/providers/pi-native-server.ts +1 -0
- package/src/providers/systemone-server.ts +72 -12
- package/src/providers/transform-messages.ts +104 -55
- package/src/registry/oauth/google-antigravity.ts +1 -1
- package/src/stream.ts +13 -73
- package/src/types.ts +33 -9
- package/src/usage/claude.ts +2 -9
- package/src/usage/github-copilot.ts +26 -4
- package/src/usage/muse-code.ts +115 -29
- package/src/usage/openai-codex.ts +13 -7
- package/src/usage/opencode-go.ts +7 -8
- package/src/usage/xai-oauth.ts +2 -9
- package/src/utils/dsml-leak.ts +88 -33
- package/src/utils/leaked-thinking-stream.ts +10 -0
- package/src/utils/narration.ts +18 -0
- package/src/utils/openai-http.ts +27 -18
- package/src/utils/request-body.ts +27 -0
- package/src/utils/request-compression.ts +202 -0
- package/src/utils/request-debug.ts +23 -9
- package/src/utils/schema/dereference.ts +51 -18
- package/src/utils/schema/draft.ts +10 -2
- package/src/utils/schema/json-schema-validator.ts +4 -10
- package/src/utils/schema/normalize.ts +6 -9
- package/src/utils/tool-call-arguments.ts +2 -1
- package/src/utils/transport-fetch.ts +37 -0
- package/src/utils/validation.ts +26 -16
- package/src/utils.ts +38 -23
package/src/judgment/native.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Each judgment API is one wire dialect: TypeSafe's System One route
|
|
6
6
|
* (`POST /v1/systemone`), OpenRouter Decisions (`POST /api/alpha/decisions`,
|
|
7
|
-
* same body), Cloudflare Workers AI
|
|
8
|
-
*
|
|
7
|
+
* same body), Cloudflare Workers AI (`POST /ai/run/{model}`, same body plus
|
|
8
|
+
* top-level data URL images; `systemone.ts`), and OpenAI Decisions
|
|
9
9
|
* (`POST /v1/decisions`, translated both ways; `openai-decisions.ts`).
|
|
10
10
|
* Credentials flow through {@link withAuth}, so a stored key rotates on
|
|
11
11
|
* 401/403 exactly like chat providers; transient 429/5xx responses retry with
|
|
@@ -19,16 +19,18 @@
|
|
|
19
19
|
* (`rules/auth/typesafe.kdl`), `TYPESAFE_BASE_URL` overrides the API root,
|
|
20
20
|
* `TYPESAFE_DEFAULT_MODEL` the model.
|
|
21
21
|
*/
|
|
22
|
+
import { resolveJudgmentImageInput, type JudgmentImageInput } from "@linxiraos/pi-catalog/compat/judgment-images";
|
|
22
23
|
import { resolveJudgmentQuestionsMax } from "@linxiraos/pi-catalog/compat/judgment-questions";
|
|
23
24
|
import { TYPESAFE_DEFAULT_BASE_URL } from "@linxiraos/pi-catalog/discovery";
|
|
24
25
|
import type { Api, FetchImpl } from "@linxiraos/pi-catalog/types";
|
|
25
|
-
import { $env, isRecord } from "@linxiraos/pi-utils";
|
|
26
|
+
import { $env, isRecord, parseImageMetadata } from "@linxiraos/pi-utils";
|
|
26
27
|
import { type ApiKey, withAuth } from "../auth-retry";
|
|
27
28
|
import * as AIError from "../error";
|
|
28
|
-
import
|
|
29
|
+
import { validateImageInput } from "../images/shared";
|
|
30
|
+
import type { ImageContent, Model, Usage } from "../types";
|
|
29
31
|
import { getRetryAfterMsFromHeaders } from "../utils/retry-after";
|
|
30
32
|
import { openaiDecisionsWire } from "./openai-decisions";
|
|
31
|
-
import { openrouterDecisionsWire, typesafeWire, workersAiWire } from "./systemone";
|
|
33
|
+
import { openrouterDecisionsWire, referencedImageAssets, typesafeWire, workersAiWire } from "./systemone";
|
|
32
34
|
import {
|
|
33
35
|
type Answer,
|
|
34
36
|
type Judge,
|
|
@@ -38,7 +40,8 @@ import {
|
|
|
38
40
|
type Questions,
|
|
39
41
|
tokenUsage,
|
|
40
42
|
} from "./types";
|
|
41
|
-
import type { JudgmentWire } from "./wire";
|
|
43
|
+
import type { JudgmentImageAssets, JudgmentWire } from "./wire";
|
|
44
|
+
export type { JudgmentImageAssets } from "./wire";
|
|
42
45
|
|
|
43
46
|
export const TYPESAFE_PROVIDER = "typesafe";
|
|
44
47
|
export const TYPESAFE_DEFAULT_MODEL = "jev-latest";
|
|
@@ -58,6 +61,97 @@ export function isJudgmentApi(api: Api): api is JudgmentApi {
|
|
|
58
61
|
return Object.hasOwn(JUDGMENT_WIRES, api);
|
|
59
62
|
}
|
|
60
63
|
|
|
64
|
+
/** Whether both the native protocol and catalog model support image input. */
|
|
65
|
+
export function nativeJudgmentSupportsImages(model: Model): boolean {
|
|
66
|
+
return isJudgmentApi(model.api) && JUDGMENT_WIRES[model.api].images !== undefined && model.input.includes("image");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Whether `model`'s native judgment takes images and its wire forwards box questions. The model itself may still refuse them (Box needs Jev 1.14+). */
|
|
70
|
+
export function nativeJudgmentSupportsBoxes(model: Model): boolean {
|
|
71
|
+
return nativeJudgmentSupportsImages(model) && isJudgmentApi(model.api) && JUDGMENT_WIRES[model.api].boxes === true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Every image bound `model`'s native judgment enforces: its wire's transport limits tightened by the
|
|
76
|
+
* catalog's model bounds. `undefined` when it takes no images. Native APIs reject images outside these
|
|
77
|
+
* rather than resizing them, so callers size images to them first.
|
|
78
|
+
*/
|
|
79
|
+
export function nativeJudgmentImageBounds(model: Model): JudgmentImageInput | undefined {
|
|
80
|
+
if (!isJudgmentApi(model.api) || !model.input.includes("image")) return undefined;
|
|
81
|
+
const wire = JUDGMENT_WIRES[model.api].images;
|
|
82
|
+
if (wire === undefined) return undefined;
|
|
83
|
+
const catalog = resolveJudgmentImageInput(model);
|
|
84
|
+
return catalog === undefined ? wire : intersectImageInput(wire, catalog);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Whether `model`'s native judgment API takes these exact images: image support within every {@link nativeJudgmentImageBounds}. */
|
|
88
|
+
export function nativeJudgmentAcceptsImages(model: Model, images: readonly ImageContent[]): boolean {
|
|
89
|
+
const bounds = nativeJudgmentImageBounds(model);
|
|
90
|
+
return bounds !== undefined && imageLimitViolation(images, bounds) === undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The bounds both `a` and `b` allow: shared MIME types, the lower of each cap, the higher of each floor. */
|
|
94
|
+
function intersectImageInput(a: JudgmentImageInput, b: JudgmentImageInput): JudgmentImageInput {
|
|
95
|
+
const bounds: JudgmentImageInput = {};
|
|
96
|
+
const mimeTypes =
|
|
97
|
+
a.mimeTypes && b.mimeTypes
|
|
98
|
+
? a.mimeTypes.filter(type => b.mimeTypes?.includes(type))
|
|
99
|
+
: (a.mimeTypes ?? b.mimeTypes);
|
|
100
|
+
if (mimeTypes !== undefined) bounds.mimeTypes = mimeTypes;
|
|
101
|
+
for (const cap of ["maxCount", "maxBytes", "maxTotalBytes", "maxPixels", "maxSide"] as const) {
|
|
102
|
+
const limit = a[cap] === undefined ? b[cap] : b[cap] === undefined ? a[cap] : Math.min(a[cap], b[cap]);
|
|
103
|
+
if (limit !== undefined) bounds[cap] = limit;
|
|
104
|
+
}
|
|
105
|
+
for (const floor of ["minPixels", "minSide"] as const) {
|
|
106
|
+
const limit =
|
|
107
|
+
a[floor] === undefined ? b[floor] : b[floor] === undefined ? a[floor] : Math.max(a[floor], b[floor]);
|
|
108
|
+
if (limit !== undefined) bounds[floor] = limit;
|
|
109
|
+
}
|
|
110
|
+
return bounds;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Reject images that `model`'s native judgment API cannot take — no image support, an unsupported or
|
|
115
|
+
* unreferenced asset, or a transport or catalog bound exceeded — before any credential or upstream work.
|
|
116
|
+
* @throws {AIError.ValidationError} naming the reason.
|
|
117
|
+
*/
|
|
118
|
+
export function checkNativeJudgmentImages(
|
|
119
|
+
model: Model,
|
|
120
|
+
request: JudgmentRequest,
|
|
121
|
+
imageAssets?: JudgmentImageAssets,
|
|
122
|
+
): void {
|
|
123
|
+
const label = `${model.provider}/${model.id}`;
|
|
124
|
+
if (isJudgmentApi(model.api)) {
|
|
125
|
+
checkJudgmentImages(
|
|
126
|
+
label,
|
|
127
|
+
JUDGMENT_WIRES[model.api],
|
|
128
|
+
model.input.includes("image"),
|
|
129
|
+
request,
|
|
130
|
+
imageAssets,
|
|
131
|
+
resolveJudgmentImageInput(model),
|
|
132
|
+
);
|
|
133
|
+
} else if ((request.images?.length ?? 0) > 0 || Object.keys(imageAssets ?? {}).length > 0) {
|
|
134
|
+
throw new AIError.ValidationError(`Native judgment backend ${label} does not support image inputs`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Reject box questions whose `image` is not an index into `request.images`: nothing would say which
|
|
140
|
+
* image their edges measure.
|
|
141
|
+
* @throws {AIError.ValidationError} naming the question.
|
|
142
|
+
*/
|
|
143
|
+
export function checkBoxQuestions(request: JudgmentRequest): void {
|
|
144
|
+
const count = request.images?.length ?? 0;
|
|
145
|
+
for (const id in request.questions) {
|
|
146
|
+
const question = request.questions[id];
|
|
147
|
+
if (question.type !== "box") continue;
|
|
148
|
+
if (Number.isInteger(question.image) && question.image >= 0 && question.image < count) continue;
|
|
149
|
+
throw new AIError.ValidationError(
|
|
150
|
+
`judgment: box question "${id}" names image ${question.image}, but the request has ${count} images`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
61
155
|
/** `TYPESAFE_BASE_URL` when set, else the public API root; trailing slashes stripped. */
|
|
62
156
|
export function typesafeBaseUrl(): string {
|
|
63
157
|
return ($env.TYPESAFE_BASE_URL?.trim() || TYPESAFE_DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
@@ -92,10 +186,19 @@ export interface NativeJudgeOptions extends NativeJudgeConnection {
|
|
|
92
186
|
* requests are split. Unset: no cap.
|
|
93
187
|
*/
|
|
94
188
|
maxQuestions?: number;
|
|
189
|
+
/**
|
|
190
|
+
* Whether images are enabled for this judge. `false` disables image input; `true` enables
|
|
191
|
+
* dialects that require explicit opt-in but cannot enable a wire without image support.
|
|
192
|
+
*/
|
|
193
|
+
supportsImages?: boolean;
|
|
194
|
+
/** Catalog-selected image bounds, applied in addition to the wire's transport constraints. */
|
|
195
|
+
imageInput?: JudgmentImageInput;
|
|
95
196
|
}
|
|
96
197
|
|
|
97
198
|
/** Per-call options for {@link NativeJudge.judge}. */
|
|
98
199
|
export interface NativeJudgeCallOptions extends JudgeOptions {
|
|
200
|
+
/** Assets keyed by their existing references; wires without asset transport reject them. */
|
|
201
|
+
imageAssets?: JudgmentImageAssets;
|
|
99
202
|
/**
|
|
100
203
|
* Called before a split judgment rejects, with the summed usage of the chunks that had already
|
|
101
204
|
* succeeded: billed upstream, but their answers are discarded with the failure. Not called when
|
|
@@ -128,8 +231,94 @@ function isAnswerOf(value: unknown, type: Answer["type"]): value is Answer {
|
|
|
128
231
|
return isRecord(value) && value.type === type;
|
|
129
232
|
}
|
|
130
233
|
|
|
234
|
+
/** The first documented cap `images` exceed, phrased to follow the backend label; `undefined` when within every cap. */
|
|
235
|
+
function imageLimitViolation(images: readonly ImageContent[], limits: JudgmentImageInput): string | undefined {
|
|
236
|
+
if (limits.maxCount !== undefined && images.length > limits.maxCount) {
|
|
237
|
+
return `accepts at most ${limits.maxCount} images, got ${images.length}`;
|
|
238
|
+
}
|
|
239
|
+
let totalBytes = 0;
|
|
240
|
+
const { minPixels, maxPixels, minSide, maxSide } = limits;
|
|
241
|
+
const checkDimensions =
|
|
242
|
+
minPixels !== undefined || maxPixels !== undefined || minSide !== undefined || maxSide !== undefined;
|
|
243
|
+
const needsBytes = limits.maxBytes !== undefined || limits.maxTotalBytes !== undefined;
|
|
244
|
+
for (const [index, image] of images.entries()) {
|
|
245
|
+
const mimeType = image.mimeType.toLowerCase();
|
|
246
|
+
if (limits.mimeTypes && !limits.mimeTypes.includes(mimeType)) {
|
|
247
|
+
return `does not accept ${mimeType} images (images[${index}]); use ${limits.mimeTypes.join(", ")}`;
|
|
248
|
+
}
|
|
249
|
+
if (!needsBytes && !checkDimensions) continue;
|
|
250
|
+
const bytes = Buffer.from(image.data, "base64");
|
|
251
|
+
totalBytes += bytes.length;
|
|
252
|
+
if (limits.maxBytes !== undefined && bytes.length > limits.maxBytes) {
|
|
253
|
+
return `accepts images up to ${limits.maxBytes} bytes; images[${index}] is ${bytes.length} bytes`;
|
|
254
|
+
}
|
|
255
|
+
if (!checkDimensions) continue;
|
|
256
|
+
const metadata = parseImageMetadata(bytes);
|
|
257
|
+
if (!metadata?.width || !metadata.height) continue;
|
|
258
|
+
const { width, height } = metadata;
|
|
259
|
+
const size = `images[${index}] is ${width}x${height}`;
|
|
260
|
+
if (maxPixels !== undefined && width * height > maxPixels)
|
|
261
|
+
return `accepts images up to ${maxPixels} pixels; ${size}`;
|
|
262
|
+
if (minPixels !== undefined && width * height < minPixels) {
|
|
263
|
+
return `accepts images of at least ${minPixels} pixels; ${size}`;
|
|
264
|
+
}
|
|
265
|
+
if (maxSide !== undefined && Math.max(width, height) > maxSide) {
|
|
266
|
+
return `accepts images up to ${maxSide} pixels per side; ${size}`;
|
|
267
|
+
}
|
|
268
|
+
if (minSide !== undefined && Math.min(width, height) < minSide) {
|
|
269
|
+
return `accepts images of at least ${minSide} pixels per side; ${size}`;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (limits.maxTotalBytes !== undefined && totalBytes > limits.maxTotalBytes) {
|
|
273
|
+
return `accepts at most ${limits.maxTotalBytes} image bytes per request, got ${totalBytes}`;
|
|
274
|
+
}
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Reject images `wire` would refuse: assets on a wire without asset transport, images when they are
|
|
280
|
+
* disabled, an asset `request` never references (TypeSafe refuses those), malformed data, or a
|
|
281
|
+
* documented cap exceeded.
|
|
282
|
+
* @throws {AIError.ValidationError} naming the first problem.
|
|
283
|
+
*/
|
|
284
|
+
function checkJudgmentImages(
|
|
285
|
+
label: string,
|
|
286
|
+
wire: JudgmentWire,
|
|
287
|
+
imagesEnabled: boolean,
|
|
288
|
+
request: JudgmentRequest,
|
|
289
|
+
imageAssets: JudgmentImageAssets | undefined,
|
|
290
|
+
modelImageInput?: JudgmentImageInput,
|
|
291
|
+
): void {
|
|
292
|
+
const assets = Object.values(imageAssets ?? {});
|
|
293
|
+
if (assets.length > 0 && wire.supportsImageAssets !== true) {
|
|
294
|
+
throw new AIError.ValidationError(`Native judgment backend ${label} does not support image assets`);
|
|
295
|
+
}
|
|
296
|
+
const images = [...(request.images ?? []), ...assets];
|
|
297
|
+
if (images.length === 0) return;
|
|
298
|
+
if (!imagesEnabled || wire.images === undefined) {
|
|
299
|
+
throw new AIError.ValidationError(`Native judgment backend ${label} does not support image inputs`);
|
|
300
|
+
}
|
|
301
|
+
if (imageAssets !== undefined && assets.length > 0) {
|
|
302
|
+
const referenced = referencedImageAssets(request, imageAssets);
|
|
303
|
+
for (const reference in imageAssets) {
|
|
304
|
+
if (Object.hasOwn(referenced, reference)) continue;
|
|
305
|
+
throw new AIError.ValidationError(
|
|
306
|
+
`Native judgment backend ${label}: image asset ${JSON.stringify(reference)} is not referenced in state, instructions, or criteria`,
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
for (const image of images) validateImageInput(image);
|
|
311
|
+
for (const limits of [wire.images, modelImageInput]) {
|
|
312
|
+
if (limits === undefined) continue;
|
|
313
|
+
const violation = imageLimitViolation(images, limits);
|
|
314
|
+
if (violation !== undefined) throw new AIError.ValidationError(`Native judgment backend ${label} ${violation}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
131
318
|
export class NativeJudge implements Judge {
|
|
132
319
|
readonly label: string;
|
|
320
|
+
/** True when the selected native wire and model or direct-call option permit image input. */
|
|
321
|
+
readonly supportsImages?: boolean;
|
|
133
322
|
readonly api: JudgmentApi;
|
|
134
323
|
readonly provider: string;
|
|
135
324
|
/** Wire model id sent with every request. */
|
|
@@ -141,6 +330,7 @@ export class NativeJudge implements Judge {
|
|
|
141
330
|
readonly #fetch: FetchImpl;
|
|
142
331
|
readonly #timeoutMs: number;
|
|
143
332
|
readonly #maxQuestions: number | undefined;
|
|
333
|
+
readonly #imageInput: JudgmentImageInput | undefined;
|
|
144
334
|
|
|
145
335
|
constructor(options: NativeJudgeOptions) {
|
|
146
336
|
this.#apiKey = options.apiKey;
|
|
@@ -156,6 +346,9 @@ export class NativeJudge implements Judge {
|
|
|
156
346
|
throw new RangeError(`NativeJudge maxQuestions must be a positive integer, got ${options.maxQuestions}`);
|
|
157
347
|
}
|
|
158
348
|
this.#maxQuestions = options.maxQuestions;
|
|
349
|
+
this.#imageInput = options.imageInput;
|
|
350
|
+
const optedIntoImages = options.supportsImages ?? this.#wire.imagesRequireOptIn !== true;
|
|
351
|
+
if (this.#wire.images !== undefined && optedIntoImages) this.supportsImages = true;
|
|
159
352
|
this.label = `${this.provider}/${this.model}`;
|
|
160
353
|
}
|
|
161
354
|
|
|
@@ -163,12 +356,32 @@ export class NativeJudge implements Judge {
|
|
|
163
356
|
request: JudgmentRequest<Q>,
|
|
164
357
|
options?: NativeJudgeCallOptions,
|
|
165
358
|
): Promise<JudgmentResult<Q>> {
|
|
359
|
+
checkJudgmentImages(
|
|
360
|
+
this.label,
|
|
361
|
+
this.#wire,
|
|
362
|
+
this.supportsImages === true,
|
|
363
|
+
request,
|
|
364
|
+
options?.imageAssets,
|
|
365
|
+
this.#imageInput,
|
|
366
|
+
);
|
|
367
|
+
checkBoxQuestions(request);
|
|
368
|
+
if (this.#wire.boxes !== true) {
|
|
369
|
+
for (const id in request.questions) {
|
|
370
|
+
if (request.questions[id].type !== "box") continue;
|
|
371
|
+
throw new AIError.ValidationError(`Native judgment backend ${this.label} does not answer box questions`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
166
374
|
const ids = Object.keys(request.questions);
|
|
167
375
|
const cap = this.#maxQuestions;
|
|
168
376
|
if (cap !== undefined && ids.length > cap) return this.#judgeChunked(request, ids, cap, options);
|
|
169
377
|
return this.#judgeOnce(request, options);
|
|
170
378
|
}
|
|
171
379
|
|
|
380
|
+
/** Effective image detail for cache identity after applying this wire's forwarding behavior. */
|
|
381
|
+
effectiveImageDetail(detail: ImageContent["detail"]): ImageContent["detail"] {
|
|
382
|
+
return this.#wire.ignoresImageDetail ? undefined : detail;
|
|
383
|
+
}
|
|
384
|
+
|
|
172
385
|
/**
|
|
173
386
|
* Answer `request` in requests of at most `cap` questions each, {@link CHUNK_PARALLEL} at a time;
|
|
174
387
|
* answers merge and usage sums. The first failing chunk aborts its in-flight siblings and stops
|
|
@@ -189,14 +402,17 @@ export class NativeJudge implements Judge {
|
|
|
189
402
|
}
|
|
190
403
|
const failed = new AbortController();
|
|
191
404
|
const signal = options?.signal ? AbortSignal.any([options.signal, failed.signal]) : failed.signal;
|
|
192
|
-
const
|
|
405
|
+
const imageAssets = options?.imageAssets;
|
|
193
406
|
const results: JudgmentResult[] = [];
|
|
194
407
|
let next = 0;
|
|
195
408
|
const drain = async (): Promise<void> => {
|
|
196
409
|
while (next < chunks.length && !signal.aborted) {
|
|
197
410
|
const index = next++;
|
|
411
|
+
const chunk = { ...request, questions: chunks[index] };
|
|
412
|
+
// TypeSafe refuses unreferenced assets: a chunk carries only those its own questions or the state use.
|
|
413
|
+
const chunkAssets = imageAssets === undefined ? undefined : referencedImageAssets(chunk, imageAssets);
|
|
198
414
|
try {
|
|
199
|
-
results[index] = await this.#judgeOnce({
|
|
415
|
+
results[index] = await this.#judgeOnce(chunk, { signal, imageAssets: chunkAssets });
|
|
200
416
|
} catch (error) {
|
|
201
417
|
failed.abort(error);
|
|
202
418
|
throw error;
|
|
@@ -234,11 +450,13 @@ export class NativeJudge implements Judge {
|
|
|
234
450
|
|
|
235
451
|
async #judgeOnce<Q extends Questions>(
|
|
236
452
|
request: JudgmentRequest<Q>,
|
|
237
|
-
options:
|
|
453
|
+
options: NativeJudgeCallOptions | undefined,
|
|
238
454
|
): Promise<JudgmentResult<Q>> {
|
|
239
|
-
const body = JSON.stringify(this.#wire.encode(request, this.model));
|
|
455
|
+
const body = JSON.stringify(this.#wire.encode(request, this.model, options?.imageAssets));
|
|
456
|
+
const hasImages = (request.images?.length ?? 0) > 0 || Object.keys(options?.imageAssets ?? {}).length > 0;
|
|
457
|
+
const requestHeaders = hasImages ? this.#wire.imageHeaders : undefined;
|
|
240
458
|
const signal = options?.signal;
|
|
241
|
-
const payload = await withAuth(this.#apiKey, key => this.#attempt(body, key, signal), { signal });
|
|
459
|
+
const payload = await withAuth(this.#apiKey, key => this.#attempt(body, key, signal, requestHeaders), { signal });
|
|
242
460
|
const decoded = this.#wire.decode(payload);
|
|
243
461
|
if (!decoded) {
|
|
244
462
|
throw new AIError.ProviderResponseError(`${this.label} returned a malformed judgment response`, {
|
|
@@ -274,10 +492,16 @@ export class NativeJudge implements Judge {
|
|
|
274
492
|
};
|
|
275
493
|
}
|
|
276
494
|
|
|
277
|
-
async #attempt(
|
|
495
|
+
async #attempt(
|
|
496
|
+
body: string,
|
|
497
|
+
key: string,
|
|
498
|
+
signal: AbortSignal | undefined,
|
|
499
|
+
requestHeaders: Readonly<Record<string, string>> | undefined,
|
|
500
|
+
): Promise<unknown> {
|
|
278
501
|
const target = this.#wire.target(this.baseUrl, this.model, key);
|
|
279
502
|
const headers: Record<string, string> = {
|
|
280
503
|
...this.#headers,
|
|
504
|
+
...requestHeaders,
|
|
281
505
|
Authorization: `Bearer ${target.token}`,
|
|
282
506
|
Accept: "application/json",
|
|
283
507
|
"Content-Type": "application/json",
|
|
@@ -329,5 +553,7 @@ export function createNativeJudge(model: Model, connection: NativeJudgeConnectio
|
|
|
329
553
|
model: model.requestModelId ?? model.id,
|
|
330
554
|
baseUrl: model.baseUrl,
|
|
331
555
|
maxQuestions: resolveJudgmentQuestionsMax(model),
|
|
556
|
+
supportsImages: nativeJudgmentSupportsImages(model),
|
|
557
|
+
imageInput: resolveJudgmentImageInput(model),
|
|
332
558
|
});
|
|
333
559
|
}
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
*
|
|
8
8
|
* | System One | Decisions |
|
|
9
9
|
* | ------------------------------------------- | --------------------------------------------- |
|
|
10
|
-
* | `state` (text or JSON) | `input`
|
|
10
|
+
* | `state` (text or JSON) | `input` text (JSON serialized), or `input_text` for images |
|
|
11
|
+
* | separate images | ordered `input_image` data URLs in user input |
|
|
11
12
|
* | question id | `name`, echoed on its answer |
|
|
12
13
|
* | `noul` + optional yes/no `criteria` | `predicate`; criteria fold into instructions |
|
|
13
14
|
* | `choice` `criteria` option → rubric | `choices: [{ value, description? }]` |
|
|
@@ -19,6 +20,8 @@
|
|
|
19
20
|
* tokens only, so cost is priced from the catalog model.
|
|
20
21
|
*/
|
|
21
22
|
import { isRecord, prompt } from "@linxiraos/pi-utils";
|
|
23
|
+
import * as AIError from "../error";
|
|
24
|
+
import { embeddedImageDataUrl } from "../images/shared";
|
|
22
25
|
import predicateTemplate from "./openai-decisions-predicate.md" with { type: "text" };
|
|
23
26
|
import { type Answer, type Question, tokenUsage } from "./types";
|
|
24
27
|
import { finiteNumber, type JudgmentWire } from "./wire";
|
|
@@ -58,6 +61,8 @@ function encodeQuestion(name: string, question: Question): DecisionQuestion {
|
|
|
58
61
|
instructions: question.instructions,
|
|
59
62
|
levels: question.criteria.map(label => ({ label })),
|
|
60
63
|
};
|
|
64
|
+
case "box":
|
|
65
|
+
throw new AIError.ValidationError(`OpenAI Decisions has no box question type (question "${name}")`);
|
|
61
66
|
}
|
|
62
67
|
}
|
|
63
68
|
|
|
@@ -99,12 +104,31 @@ function decodeAnswer(entry: Record<string, unknown>): Answer | undefined {
|
|
|
99
104
|
|
|
100
105
|
/** OpenAI's `/v1/decisions` route. */
|
|
101
106
|
export const openaiDecisionsWire: JudgmentWire = {
|
|
107
|
+
// https://developers.openai.com/api/reference/resources/decisions/methods/create/
|
|
108
|
+
images: { maxCount: 128 },
|
|
102
109
|
target: (baseUrl, _model, key) => ({ url: `${baseUrl}/decisions`, token: key }),
|
|
103
|
-
encode: (request, model) =>
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
110
|
+
encode: (request, model) => {
|
|
111
|
+
const state = typeof request.state === "string" ? request.state : JSON.stringify(request.state);
|
|
112
|
+
return {
|
|
113
|
+
model,
|
|
114
|
+
input: request.images?.length
|
|
115
|
+
? [
|
|
116
|
+
{
|
|
117
|
+
role: "user",
|
|
118
|
+
content: [
|
|
119
|
+
{ type: "input_text", text: state },
|
|
120
|
+
...request.images.map(image => ({
|
|
121
|
+
type: "input_image",
|
|
122
|
+
image_url: embeddedImageDataUrl(image),
|
|
123
|
+
...(image.detail !== undefined ? { detail: image.detail } : {}),
|
|
124
|
+
})),
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
]
|
|
128
|
+
: state,
|
|
129
|
+
questions: Object.entries(request.questions).map(([name, question]) => encodeQuestion(name, question)),
|
|
130
|
+
};
|
|
131
|
+
},
|
|
108
132
|
decode(payload) {
|
|
109
133
|
if (!isRecord(payload) || !Array.isArray(payload.answers)) return undefined;
|
|
110
134
|
const answers: Record<string, Answer> = {};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* TypeSafe
|
|
3
|
-
*
|
|
2
|
+
* TypeSafe and OpenRouter use the `{ model, state, questions }` body. Images
|
|
3
|
+
* join `state` without replacing it (see {@link stateWithImages}): TypeSafe as
|
|
4
|
+
* references to a top-level `assets` map, OpenRouter as `image_url` data URL
|
|
5
|
+
* parts. Cloudflare optionally accepts image data URLs in its envelope.
|
|
4
6
|
*
|
|
5
7
|
* - `typesafe`: `POST {baseUrl}/v1/systemone` — TypeSafe and every System One
|
|
6
8
|
* server (OpenJEV, Kev, self-hosted Clef, OpenCode Zen, Command Code).
|
|
@@ -8,15 +10,25 @@
|
|
|
8
10
|
* `/api/alpha`; usage adds the billed `cost` in USD.
|
|
9
11
|
* - `workers-ai-systemone`: Cloudflare Workers AI's `POST {baseUrl}/{model}`
|
|
10
12
|
* (`/client/v4/accounts/<account>/ai/run/@cf/cloudflare/clef`). The body
|
|
11
|
-
* names the model by its last path segment (`clef`),
|
|
12
|
-
*
|
|
13
|
-
*
|
|
13
|
+
* names the model by its last path segment (`clef`), adds a top-level array
|
|
14
|
+
* of base64 data URLs when images are supplied, and returns Cloudflare's
|
|
15
|
+
* `{ result, success, errors }` envelope. The credential is a Cloudflare
|
|
16
|
+
* API token plus the account id the URL needs.
|
|
14
17
|
*/
|
|
15
18
|
import { parseCloudflareCredential } from "@linxiraos/pi-catalog/wire/cloudflare";
|
|
16
19
|
import { $env, isRecord } from "@linxiraos/pi-utils";
|
|
17
20
|
import * as AIError from "../error";
|
|
18
|
-
import {
|
|
19
|
-
import
|
|
21
|
+
import { embeddedImageDataUrl } from "../images/shared";
|
|
22
|
+
import type { ImageContent } from "../types";
|
|
23
|
+
import {
|
|
24
|
+
type BoxQuestion,
|
|
25
|
+
type JsonValue,
|
|
26
|
+
type JudgmentRequest,
|
|
27
|
+
type Question,
|
|
28
|
+
type Questions,
|
|
29
|
+
tokenUsage,
|
|
30
|
+
} from "./types";
|
|
31
|
+
import { type DecodedJudgment, finiteNumber, type JudgmentImageAssets, type JudgmentWire } from "./wire";
|
|
20
32
|
|
|
21
33
|
function decodeSystemOne(payload: unknown): DecodedJudgment | undefined {
|
|
22
34
|
if (!isRecord(payload) || !isRecord(payload.answers)) return undefined;
|
|
@@ -29,23 +41,152 @@ function decodeSystemOne(payload: unknown): DecodedJudgment | undefined {
|
|
|
29
41
|
};
|
|
30
42
|
}
|
|
31
43
|
|
|
44
|
+
/**
|
|
45
|
+
* `state` with `items` added and every original value kept: arrays gain them at the end, objects
|
|
46
|
+
* under the first free `__omp_judgment_images[_N]` key, and a string root becomes `[state, ...items]`.
|
|
47
|
+
*/
|
|
48
|
+
function stateWithImages(state: JudgmentRequest["state"], items: readonly JsonValue[]): JudgmentRequest["state"] {
|
|
49
|
+
if (Array.isArray(state)) return [...state, ...items];
|
|
50
|
+
if (!isRecord(state)) return [state, ...items];
|
|
51
|
+
const baseKey = "__omp_judgment_images";
|
|
52
|
+
let key = baseKey;
|
|
53
|
+
for (let suffix = 1; Object.hasOwn(state, key); suffix++) key = `${baseKey}_${suffix}`;
|
|
54
|
+
return { ...state, [key]: items };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function* jsonStrings(value: unknown): Generator<string> {
|
|
58
|
+
if (typeof value === "string") yield value;
|
|
59
|
+
else if (Array.isArray(value)) for (const item of value) yield* jsonStrings(item);
|
|
60
|
+
else if (isRecord(value)) for (const key in value) yield* jsonStrings(value[key]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The subset of `assets` that `request` references. TypeSafe substitutes a reference wherever it
|
|
65
|
+
* occurs as text in a string of `state`, a question's `instructions`, or its `criteria` (never in
|
|
66
|
+
* object keys) and rejects a request carrying an unreferenced asset.
|
|
67
|
+
*/
|
|
68
|
+
export function referencedImageAssets(request: JudgmentRequest, assets: JudgmentImageAssets): JudgmentImageAssets {
|
|
69
|
+
const pending = new Set(Object.keys(assets));
|
|
70
|
+
const visit = (value: unknown): void => {
|
|
71
|
+
for (const text of jsonStrings(value)) {
|
|
72
|
+
if (pending.size === 0) return;
|
|
73
|
+
for (const reference of pending) if (text.includes(reference)) pending.delete(reference);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
visit(request.state);
|
|
77
|
+
for (const id in request.questions) {
|
|
78
|
+
const question = request.questions[id];
|
|
79
|
+
visit(question.instructions);
|
|
80
|
+
if (question.type !== "box") visit(question.criteria);
|
|
81
|
+
}
|
|
82
|
+
// `fromEntries` defines own properties, so a `__proto__` reference stays an ordinary key.
|
|
83
|
+
return Object.fromEntries(Object.entries(assets).filter(([reference]) => !pending.has(reference)));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface SystemOneImageAsset {
|
|
87
|
+
type: "image";
|
|
88
|
+
format: "png" | "jpeg";
|
|
89
|
+
data_base64: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function encodeSystemOneImage(image: ImageContent): SystemOneImageAsset {
|
|
93
|
+
const mimeType = image.mimeType.toLowerCase();
|
|
94
|
+
return {
|
|
95
|
+
type: "image",
|
|
96
|
+
format: mimeType === "image/png" ? "png" : "jpeg",
|
|
97
|
+
data_base64: Buffer.from(image.data, "base64").toString("base64"),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
type WireQuestion = Exclude<Question, BoxQuestion> | Omit<BoxQuestion, "image">;
|
|
102
|
+
|
|
103
|
+
/** `questions` as System One takes them: a box question's `image` only names the image its edges measure locally. */
|
|
104
|
+
function wireQuestions(questions: Questions): Record<string, WireQuestion> {
|
|
105
|
+
const wire: Record<string, WireQuestion> = {};
|
|
106
|
+
for (const id in questions) {
|
|
107
|
+
const question = questions[id];
|
|
108
|
+
wire[id] = question.type === "box" ? { type: "box", instructions: question.instructions } : question;
|
|
109
|
+
}
|
|
110
|
+
return wire;
|
|
111
|
+
}
|
|
112
|
+
|
|
32
113
|
/** A System One dialect posting the verbatim request to `path` under the base URL with the key as bearer. */
|
|
33
114
|
function systemOneWire(path: string): JudgmentWire {
|
|
34
115
|
return {
|
|
35
116
|
target: (baseUrl, _model, key) => ({ url: `${baseUrl}${path}`, token: key }),
|
|
36
|
-
encode: (request, model) => ({ state: request.state, model, questions: request.questions }),
|
|
117
|
+
encode: (request, model) => ({ state: request.state, model, questions: wireQuestions(request.questions) }),
|
|
37
118
|
decode: decodeSystemOne,
|
|
38
119
|
};
|
|
39
120
|
}
|
|
40
121
|
|
|
41
122
|
/** TypeSafe's own route, shared by every System One server. */
|
|
42
|
-
export const typesafeWire =
|
|
123
|
+
export const typesafeWire: JudgmentWire = {
|
|
124
|
+
...systemOneWire("/v1/systemone"),
|
|
125
|
+
images: { mimeTypes: ["image/png", "image/jpeg"] },
|
|
126
|
+
supportsImageAssets: true,
|
|
127
|
+
imagesRequireOptIn: true,
|
|
128
|
+
imageHeaders: { "TypeSafe-Danger-Zone-Images": "true" },
|
|
129
|
+
ignoresImageDetail: true,
|
|
130
|
+
boxes: true,
|
|
131
|
+
encode(request, model, imageAssets) {
|
|
132
|
+
const suppliedAssets = Object.entries(imageAssets ?? {});
|
|
133
|
+
const images = request.images ?? [];
|
|
134
|
+
if (suppliedAssets.length === 0 && images.length === 0) {
|
|
135
|
+
return { state: request.state, model, questions: wireQuestions(request.questions) };
|
|
136
|
+
}
|
|
43
137
|
|
|
44
|
-
|
|
45
|
-
|
|
138
|
+
const assets: Record<string, SystemOneImageAsset> = Object.fromEntries(
|
|
139
|
+
suppliedAssets.map(([reference, image]) => [reference, encodeSystemOneImage(image)]),
|
|
140
|
+
);
|
|
141
|
+
const references: string[] = [];
|
|
142
|
+
for (const image of images) {
|
|
143
|
+
let reference = crypto.randomUUID();
|
|
144
|
+
while (Object.hasOwn(assets, reference)) reference = crypto.randomUUID();
|
|
145
|
+
assets[reference] = encodeSystemOneImage(image);
|
|
146
|
+
references.push(reference);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
state: references.length ? stateWithImages(request.state, references) : request.state,
|
|
151
|
+
model,
|
|
152
|
+
questions: wireQuestions(request.questions),
|
|
153
|
+
assets,
|
|
154
|
+
};
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/** OpenRouter Decisions: System One body under `/api/alpha`, with image parts preserved in state. */
|
|
159
|
+
export const openrouterDecisionsWire: JudgmentWire = {
|
|
160
|
+
...systemOneWire("/decisions"),
|
|
161
|
+
images: {},
|
|
162
|
+
imagesRequireOptIn: true,
|
|
163
|
+
boxes: true,
|
|
164
|
+
encode(request, model) {
|
|
165
|
+
const imageParts: JsonValue[] = (request.images ?? []).map(image => ({
|
|
166
|
+
type: "image_url",
|
|
167
|
+
image_url: {
|
|
168
|
+
url: embeddedImageDataUrl(image),
|
|
169
|
+
...(image.detail !== undefined ? { detail: image.detail === "original" ? "auto" : image.detail } : {}),
|
|
170
|
+
},
|
|
171
|
+
}));
|
|
172
|
+
return {
|
|
173
|
+
state: imageParts.length ? stateWithImages(request.state, imageParts) : request.state,
|
|
174
|
+
model,
|
|
175
|
+
questions: wireQuestions(request.questions),
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
};
|
|
46
179
|
|
|
47
180
|
/** Cloudflare Workers AI decision models (Clef) through the account-scoped `ai/run` route. */
|
|
48
181
|
export const workersAiWire: JudgmentWire = {
|
|
182
|
+
// Clef's `images` extension: https://developers.cloudflare.com/workers-ai/models/clef/
|
|
183
|
+
images: {
|
|
184
|
+
mimeTypes: ["image/png", "image/jpeg", "image/webp"],
|
|
185
|
+
maxCount: 4,
|
|
186
|
+
maxBytes: 4 * 1024 * 1024,
|
|
187
|
+
maxTotalBytes: 8 * 1024 * 1024,
|
|
188
|
+
maxPixels: 16_000_000,
|
|
189
|
+
},
|
|
49
190
|
target(baseUrl, model, key) {
|
|
50
191
|
const credential = parseCloudflareCredential(key);
|
|
51
192
|
const accountId = credential?.accountId ?? $env.CLOUDFLARE_ACCOUNT_ID?.trim();
|
|
@@ -60,6 +201,7 @@ export const workersAiWire: JudgmentWire = {
|
|
|
60
201
|
state: request.state,
|
|
61
202
|
model: model.slice(model.lastIndexOf("/") + 1),
|
|
62
203
|
questions: request.questions,
|
|
204
|
+
...(request.images?.length ? { images: request.images.map(embeddedImageDataUrl) } : {}),
|
|
63
205
|
}),
|
|
64
206
|
decode: payload => (isRecord(payload) ? decodeSystemOne(payload.result) : undefined),
|
|
65
207
|
};
|