@bastani/pi-ai 0.9.41-alpha.1 → 0.9.41-alpha.2

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 CHANGED
@@ -4,6 +4,18 @@ This package is a Bastani fork of `@earendil-works/pi-ai`. Upstream history at t
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.9.41-alpha.2] - 2026-10-22
8
+
9
+ ### Added
10
+
11
+ - Added image input to more System One classifiers: `ClassifierContext.images` now also works with Jev and Clef on `cloudflare-workers-ai` and with OpenRouter decision models whose catalog lists image input, next to Jev on `typesafe`. Each service gets its own wire form: TypeSafe `assets` (PNG and JPEG) with the `TypeSafe-Danger-Zone-Images` opt-in header, Clef `images` data URLs (PNG, JPEG, and WebP, at most four), or OpenRouter `image_url` parts in the state. A text-only model's error now names the model: `System One API does not support image input for <model>`.
12
+ - Added `compat` to System One classifier models (`SystemOneCompat`, with `imageInput` and `sendImageOptInHeader`). Built-in catalog entries set it explicitly; models without it detect the form from the provider, `baseUrl`, and model ID.
13
+ - Added the `@cf/cloudflare/clef-omni` and `typesafe/jev-1.14.0` classifier models to `cloudflare-workers-ai`.
14
+
15
+ ### Fixed
16
+
17
+ - Fixed Cloudflare Workers AI classifier catalog data: Clef Flash now costs $0.038 per million input tokens with a 24,576-token context window, and Jev costs $0.042 per million input tokens instead of being listed as free.
18
+
7
19
  ## [0.9.41-alpha.1] - 2026-10-21
8
20
 
9
21
  ### Breaking Changes
package/README.md CHANGED
@@ -935,8 +935,8 @@ Classifier models consume structured JSON state and answer one or more typed que
935
935
  | Provider | Model IDs | Auth |
936
936
  | --- | --- | --- |
937
937
  | `typesafe` | `jev-latest` | `TYPESAFE_API_KEY` |
938
- | `openrouter` | `typesafe/jev-1.13`, `~typesafe/jev-latest` | `OPENROUTER_API_KEY` or OpenRouter OAuth |
939
- | `cloudflare-workers-ai` | `typesafe/jev`, `@cf/cloudflare/clef`, `@cf/cloudflare/clef-flash` | `CLOUDFLARE_API_KEY` and `CLOUDFLARE_ACCOUNT_ID` |
938
+ | `openrouter` | `typesafe/jev-1.13`, `typesafe/jev-1.14`, `~typesafe/jev-latest`, `cloudflare/clef`, `cloudflare/clef-flash`, `cloudflare/clef-omni` | `OPENROUTER_API_KEY` or OpenRouter OAuth |
939
+ | `cloudflare-workers-ai` | `typesafe/jev`, `typesafe/jev-1.14.0`, `@cf/cloudflare/clef`, `@cf/cloudflare/clef-flash`, `@cf/cloudflare/clef-omni` | `CLOUDFLARE_API_KEY` and `CLOUDFLARE_ACCOUNT_ID` |
940
940
  | `vercel-ai-gateway` | `typesafe-ai/jev` | `AI_GATEWAY_API_KEY` |
941
941
  | `opencode` | `jev-1.13`, `jev-1.13-free` | `OPENCODE_API_KEY` |
942
942
 
@@ -978,6 +978,18 @@ System One results include `usage` when the service reports input or output toke
978
978
 
979
979
  `ClassifierOptions.temperature` divides the answer logits by the given value before they are normalized; values above 1 soften the distribution. APIs that cannot apply it, such as System One, ignore it.
980
980
 
981
+ ### System One images
982
+
983
+ `ClassifierContext.images` holds image blocks (`{ type: "image", data, mimeType }`) judged together with the state. Only models whose `input` includes `"image"` accept them; `classify()` returns an error result for other models. System One services take images in three forms. The model's `compat.imageInput` chooses the form (`"assets"`, `"images"`, or `"state-image-urls"`), and `compat.sendImageOptInHeader` controls TypeSafe's opt-in header:
984
+
985
+ - **TypeSafe** (`typesafe`, and Jev on Workers AI) takes PNG and JPEG images as `assets`, each under a fresh UUID reference that is also listed in order under `state.images`, so a state with its own `images` field cannot send images. Images are experimental on TypeSafe's own API and need the `TypeSafe-Danger-Zone-Images: true` header, which the API sends with requests that carry images.
986
+ - **Clef on Workers AI** takes up to four PNG, JPEG, or WebP images as an `images` list of data URLs, next to the unchanged state.
987
+ - **OpenRouter** takes images as `{ type: "image_url", image_url: { url } }` parts before the state: `[part, ..., state]`. It answers requests with `assets` but drops the images, without an error.
988
+
989
+ Built-in catalog entries set `compat` explicitly, so they keep their form when `baseUrl` points at a proxy. For models without it, the APIs detect the form: OpenRouter by provider `openrouter` or a `baseUrl` containing `openrouter.ai`, Clef by a model ID starting with `@cf/cloudflare/clef`, and TypeSafe's form with the opt-in header otherwise. A custom provider that reaches OpenRouter through a proxy matches neither check and must set `compat.imageInput: "state-image-urls"` and `compat.sendImageOptInHeader: false`; otherwise OpenRouter drops its images.
990
+
991
+ Vercel AI Gateway and OpenCode list their Jev models with text input only.
992
+
981
993
  ### Chat models on llama.cpp
982
994
 
983
995
  The `llama-cpp-classify` API turns a chat model served by llama.cpp's `llama-server` into a classifier. Each question becomes one chat prompt: the state, every question of the request, the state again, and the question with its answers under single-token labels (letters for a choice, `Yes`/`No` for a bool, digits for a score). The prompt up to the final question is shared by all questions of a request, so the server's prompt cache evaluates the state once per request. The server returns the log-probabilities of the next token, and the answer is the softmax over the label tokens. Choices support up to 62 options and scores up to 10 levels. The model's `baseUrl` is the server URL; a trailing `/v1` is ignored. In router mode, the model ID selects the model.
@@ -1 +1 @@
1
- {"version":3,"file":"cloudflare-workers-ai-system-one.d.ts","sourceRoot":"","sources":["../../src/api/cloudflare-workers-ai-system-one.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AA6CzE,6GAA6G;AAC7G,eAAO,MAAM,QAAQ,EAAE,kBAAkB,CAAC,iBAAiB,CACL,CAAC"}
1
+ {"version":3,"file":"cloudflare-workers-ai-system-one.d.ts","sourceRoot":"","sources":["../../src/api/cloudflare-workers-ai-system-one.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAqDzE,6GAA6G;AAC7G,eAAO,MAAM,QAAQ,EAAE,kBAAkB,CAAC,iBAAiB,CACL,CAAC"}
@@ -20,10 +20,18 @@ function cloudflareErrorMessage(errors) {
20
20
  * Cloudflare-hosted models such as `@cf/cloudflare/clef` return the output directly:
21
21
  * `{ success, result: { model, answers, usage } }`.
22
22
  * https://developers.cloudflare.com/workers-ai/models/clef/
23
+ *
24
+ * Clef models take images as an `images` list of data URLs. TypeSafe's models take them as
25
+ * System One `assets`, without TypeSafe's opt-in header. The catalog sets `compat.imageInput`;
26
+ * the model ID check only covers models without it.
23
27
  */
24
28
  const transport = {
25
29
  api: "cloudflare-workers-ai-system-one",
26
30
  label: LABEL,
31
+ detectCompat: (model) => ({
32
+ imageInput: model.id.startsWith("@cf/cloudflare/clef") ? "images" : "assets",
33
+ sendImageOptInHeader: false,
34
+ }),
27
35
  url: (model) => new URL("run", `${model.baseUrl.replace(/\/+$/u, "")}/`),
28
36
  payload: (model, request) => ({ model: model.id, input: request }),
29
37
  output: (body) => {
@@ -1 +1 @@
1
- {"version":3,"file":"cloudflare-workers-ai-system-one.js","sourceRoot":"","sources":["../../src/api/cloudflare-workers-ai-system-one.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAA2B,MAAM,wBAAwB,CAAC;AAEpF,MAAM,KAAK,GAAG,uBAAuB,CAAC;AAEtC,SAAS,sBAAsB,CAAC,MAAe;IAC9C,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3B,MAAM,QAAQ,GAAG,MAAM;aACrB,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;aAClG,MAAM,CAAC,CAAC,OAAO,EAAqB,EAAE,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC;QAChE,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,GAAG,KAAK,WAAW,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;IAC1E,CAAC;IACD,OAAO,GAAG,KAAK,iBAAiB,CAAC;AAClC,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,SAAS,GAAuB;IACrC,GAAG,EAAE,kCAAkC;IACvC,KAAK,EAAE,KAAK;IACZ,GAAG,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,KAAK,EAAE,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,GAAG,CAAC;IACxE,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;IAClE,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;QAChB,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,kCAAkC,CAAC,CAAC;QACjF,IAAI,IAAI,CAAC,OAAO,KAAK,KAAK;YAAE,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;QACjF,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC3B,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,kCAAkC,CAAC,CAAC;QACnF,IAAI,SAAS,IAAI,MAAM;YAAE,OAAO,MAAM,CAAC;QACvC,IAAI,MAAM,CAAC,KAAK,KAAK,WAAW,EAAE,CAAC;YAClC,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,iCAAiC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACnF,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,kCAAkC,CAAC,CAAC;QAC1F,OAAO,MAAM,CAAC,MAAM,CAAC;IACtB,CAAC;CACD,CAAC;AAEF,6GAA6G;AAC7G,MAAM,CAAC,MAAM,QAAQ,GAA0C,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,CAC1F,iBAAiB,CAAC,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC","sourcesContent":["import type { ClassifierFunction, ClassifierOptions } from \"../types.ts\";\nimport { isRecord } from \"./classifier-shared.ts\";\nimport { classifySystemOne, type SystemOneTransport } from \"./system-one-shared.ts\";\n\nconst LABEL = \"Cloudflare Workers AI\";\n\nfunction cloudflareErrorMessage(errors: unknown): string {\n\tif (Array.isArray(errors)) {\n\t\tconst messages = errors\n\t\t\t.map((error) => (isRecord(error) && typeof error.message === \"string\" ? error.message : undefined))\n\t\t\t.filter((message): message is string => message !== undefined);\n\t\tif (messages.length > 0) return `${LABEL} error: ${messages.join(\"; \")}`;\n\t}\n\treturn `${LABEL} request failed`;\n}\n\n/**\n * System One models on the Workers AI REST endpoint:\n * `POST /accounts/{account}/ai/run` with `{ model, input }`. The REST API wraps the model\n * output in Cloudflare's API envelope. Third-party models such as `typesafe/jev` add a run record:\n * `{ success, result: { state: \"Completed\", result: { answers, usage } } }`.\n * https://developers.cloudflare.com/ai/models/typesafe/jev/\n * Cloudflare-hosted models such as `@cf/cloudflare/clef` return the output directly:\n * `{ success, result: { model, answers, usage } }`.\n * https://developers.cloudflare.com/workers-ai/models/clef/\n */\nconst transport: SystemOneTransport = {\n\tapi: \"cloudflare-workers-ai-system-one\",\n\tlabel: LABEL,\n\turl: (model) => new URL(\"run\", `${model.baseUrl.replace(/\\/+$/u, \"\")}/`),\n\tpayload: (model, request) => ({ model: model.id, input: request }),\n\toutput: (body) => {\n\t\tif (!isRecord(body)) throw new Error(`${LABEL} returned an unexpected response`);\n\t\tif (body.success === false) throw new Error(cloudflareErrorMessage(body.errors));\n\t\tconst result = body.result;\n\t\tif (!isRecord(result)) throw new Error(`${LABEL} returned an unexpected response`);\n\t\tif (\"answers\" in result) return result;\n\t\tif (result.state !== \"Completed\") {\n\t\t\tthrow new Error(`${LABEL} run did not complete (state: ${String(result.state)})`);\n\t\t}\n\t\tif (!isRecord(result.result)) throw new Error(`${LABEL} returned an unexpected response`);\n\t\treturn result.result;\n\t},\n};\n\n/** Cloudflare Workers AI System One classification with public `bool` values mapped to wire-level `noul`. */\nexport const classify: ClassifierFunction<ClassifierOptions> = (model, context, options) =>\n\tclassifySystemOne(transport, model, context, options);\n"]}
1
+ {"version":3,"file":"cloudflare-workers-ai-system-one.js","sourceRoot":"","sources":["../../src/api/cloudflare-workers-ai-system-one.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAA2B,MAAM,wBAAwB,CAAC;AAEpF,MAAM,KAAK,GAAG,uBAAuB,CAAC;AAEtC,SAAS,sBAAsB,CAAC,MAAe;IAC9C,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3B,MAAM,QAAQ,GAAG,MAAM;aACrB,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;aAClG,MAAM,CAAC,CAAC,OAAO,EAAqB,EAAE,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC;QAChE,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,GAAG,KAAK,WAAW,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;IAC1E,CAAC;IACD,OAAO,GAAG,KAAK,iBAAiB,CAAC;AAClC,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,SAAS,GAAuB;IACrC,GAAG,EAAE,kCAAkC;IACvC,KAAK,EAAE,KAAK;IACZ,YAAY,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QACzB,UAAU,EAAE,KAAK,CAAC,EAAE,CAAC,UAAU,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ;QAC5E,oBAAoB,EAAE,KAAK;KAC3B,CAAC;IACF,GAAG,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,KAAK,EAAE,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,GAAG,CAAC;IACxE,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;IAClE,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;QAChB,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,kCAAkC,CAAC,CAAC;QACjF,IAAI,IAAI,CAAC,OAAO,KAAK,KAAK;YAAE,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;QACjF,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC3B,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,kCAAkC,CAAC,CAAC;QACnF,IAAI,SAAS,IAAI,MAAM;YAAE,OAAO,MAAM,CAAC;QACvC,IAAI,MAAM,CAAC,KAAK,KAAK,WAAW,EAAE,CAAC;YAClC,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,iCAAiC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACnF,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,kCAAkC,CAAC,CAAC;QAC1F,OAAO,MAAM,CAAC,MAAM,CAAC;IACtB,CAAC;CACD,CAAC;AAEF,6GAA6G;AAC7G,MAAM,CAAC,MAAM,QAAQ,GAA0C,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,CAC1F,iBAAiB,CAAC,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC","sourcesContent":["import type { ClassifierFunction, ClassifierOptions } from \"../types.ts\";\nimport { isRecord } from \"./classifier-shared.ts\";\nimport { classifySystemOne, type SystemOneTransport } from \"./system-one-shared.ts\";\n\nconst LABEL = \"Cloudflare Workers AI\";\n\nfunction cloudflareErrorMessage(errors: unknown): string {\n\tif (Array.isArray(errors)) {\n\t\tconst messages = errors\n\t\t\t.map((error) => (isRecord(error) && typeof error.message === \"string\" ? error.message : undefined))\n\t\t\t.filter((message): message is string => message !== undefined);\n\t\tif (messages.length > 0) return `${LABEL} error: ${messages.join(\"; \")}`;\n\t}\n\treturn `${LABEL} request failed`;\n}\n\n/**\n * System One models on the Workers AI REST endpoint:\n * `POST /accounts/{account}/ai/run` with `{ model, input }`. The REST API wraps the model\n * output in Cloudflare's API envelope. Third-party models such as `typesafe/jev` add a run record:\n * `{ success, result: { state: \"Completed\", result: { answers, usage } } }`.\n * https://developers.cloudflare.com/ai/models/typesafe/jev/\n * Cloudflare-hosted models such as `@cf/cloudflare/clef` return the output directly:\n * `{ success, result: { model, answers, usage } }`.\n * https://developers.cloudflare.com/workers-ai/models/clef/\n *\n * Clef models take images as an `images` list of data URLs. TypeSafe's models take them as\n * System One `assets`, without TypeSafe's opt-in header. The catalog sets `compat.imageInput`;\n * the model ID check only covers models without it.\n */\nconst transport: SystemOneTransport = {\n\tapi: \"cloudflare-workers-ai-system-one\",\n\tlabel: LABEL,\n\tdetectCompat: (model) => ({\n\t\timageInput: model.id.startsWith(\"@cf/cloudflare/clef\") ? \"images\" : \"assets\",\n\t\tsendImageOptInHeader: false,\n\t}),\n\turl: (model) => new URL(\"run\", `${model.baseUrl.replace(/\\/+$/u, \"\")}/`),\n\tpayload: (model, request) => ({ model: model.id, input: request }),\n\toutput: (body) => {\n\t\tif (!isRecord(body)) throw new Error(`${LABEL} returned an unexpected response`);\n\t\tif (body.success === false) throw new Error(cloudflareErrorMessage(body.errors));\n\t\tconst result = body.result;\n\t\tif (!isRecord(result)) throw new Error(`${LABEL} returned an unexpected response`);\n\t\tif (\"answers\" in result) return result;\n\t\tif (result.state !== \"Completed\") {\n\t\t\tthrow new Error(`${LABEL} run did not complete (state: ${String(result.state)})`);\n\t\t}\n\t\tif (!isRecord(result.result)) throw new Error(`${LABEL} returned an unexpected response`);\n\t\treturn result.result;\n\t},\n};\n\n/** Cloudflare Workers AI System One classification with public `bool` values mapped to wire-level `noul`. */\nexport const classify: ClassifierFunction<ClassifierOptions> = (model, context, options) =>\n\tclassifySystemOne(transport, model, context, options);\n"]}
@@ -1,4 +1,4 @@
1
- import type { ClassifierApi, ClassifierContext, ClassifierModel, ClassifierOptions, ClassifierResult } from "../types.ts";
1
+ import type { ClassifierApi, ClassifierContext, ClassifierModel, ClassifierOptions, ClassifierResult, SystemOneCompat } from "../types.ts";
2
2
  /** Base64 image in the System One request's top-level `assets` map. */
3
3
  export interface SystemOneImageAsset {
4
4
  type: "image";
@@ -7,25 +7,41 @@ export interface SystemOneImageAsset {
7
7
  }
8
8
  /** TypeSafe System One request body without the transport-specific envelope. */
9
9
  export interface SystemOneWireRequest {
10
- state: ClassifierContext["state"];
10
+ /** The context state, or with images in OpenRouter's form a list of image parts followed by the state. */
11
+ state: unknown;
11
12
  questions: Record<string, unknown>;
12
13
  /** Images keyed by the references listed in `state.images`. */
13
14
  assets?: Record<string, SystemOneImageAsset>;
15
+ /** Images as data URLs, Clef's extension to System One. */
16
+ images?: string[];
14
17
  }
18
+ /**
19
+ * How a service takes images (`SystemOneCompat.imageInput`):
20
+ * - `assets`: TypeSafe's `assets` map, keyed by a fresh UUID per image and listed in order under
21
+ * `state.images`. PNG and JPEG only.
22
+ * - `images`: Clef's `images` list of data URLs, which the model reads before the state.
23
+ * PNG, JPEG, and WebP, at most four.
24
+ * - `state-image-urls`: OpenRouter's form, data URL parts placed before the state:
25
+ * `state: [{ type: "image_url", image_url: { url } }, state]`. OpenRouter accepts this for every
26
+ * model and drops `assets` silently.
27
+ */
28
+ export type SystemOneImageInput = NonNullable<SystemOneCompat["imageInput"]>;
29
+ /** System One compatibility settings with every field resolved. */
30
+ export type ResolvedSystemOneCompat = Required<SystemOneCompat>;
15
31
  /** Differences between services that serve System One models. */
16
32
  export interface SystemOneTransport {
17
33
  /** Classifier API implemented by this transport. */
18
34
  api: ClassifierApi;
19
35
  /** Service name used in error messages. */
20
36
  label: string;
37
+ /** Compatibility settings for models that do not set them in `compat`. */
38
+ detectCompat(model: ClassifierModel<ClassifierApi>): ResolvedSystemOneCompat;
21
39
  /** Absolute request URL. */
22
40
  url(model: ClassifierModel<ClassifierApi>): URL;
23
41
  /** Wraps the System One request in the service's request envelope. */
24
42
  payload(model: ClassifierModel<ClassifierApi>, request: SystemOneWireRequest): unknown;
25
43
  /** Extracts the System One output, including answers and usage. */
26
44
  output(body: unknown): Record<string, unknown>;
27
- /** Whether the service accepts `context.images` as `assets` for this model. Images are rejected when omitted. */
28
- acceptsImages?(model: ClassifierModel<ClassifierApi>): boolean;
29
45
  }
30
46
  /** Runs one System One classification over the given transport. */
31
47
  export declare function classifySystemOne(transport: SystemOneTransport, model: ClassifierModel<ClassifierApi>, context: ClassifierContext, options: ClassifierOptions | undefined): Promise<ClassifierResult>;
@@ -1 +1 @@
1
- {"version":3,"file":"system-one-shared.d.ts","sourceRoot":"","sources":["../../src/api/system-one-shared.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEX,aAAa,EACb,iBAAiB,EACjB,eAAe,EACf,iBAAiB,EACjB,gBAAgB,EAChB,MAAM,aAAa,CAAC;AAIrB,uEAAuE;AACvE,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,KAAK,GAAG,KAAK,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,gFAAgF;AAChF,MAAM,WAAW,oBAAoB;IACpC,KAAK,EAAE,iBAAiB,CAAC,OAAO,CAAC,CAAC;IAClC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,+DAA+D;IAC/D,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;CAC7C;AAED,iEAAiE;AACjE,MAAM,WAAW,kBAAkB;IAClC,oDAAoD;IACpD,GAAG,EAAE,aAAa,CAAC;IACnB,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,4BAA4B;IAC5B,GAAG,CAAC,KAAK,EAAE,eAAe,CAAC,aAAa,CAAC,GAAG,GAAG,CAAC;IAChD,sEAAsE;IACtE,OAAO,CAAC,KAAK,EAAE,eAAe,CAAC,aAAa,CAAC,EAAE,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC;IACvF,mEAAmE;IACnE,MAAM,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/C,iHAAiH;IACjH,aAAa,CAAC,CAAC,KAAK,EAAE,eAAe,CAAC,aAAa,CAAC,GAAG,OAAO,CAAC;CAC/D;AAsHD,mEAAmE;AACnE,wBAAsB,iBAAiB,CACtC,SAAS,EAAE,kBAAkB,EAC7B,KAAK,EAAE,eAAe,CAAC,aAAa,CAAC,EACrC,OAAO,EAAE,iBAAiB,EAC1B,OAAO,EAAE,iBAAiB,GAAG,SAAS,GACpC,OAAO,CAAC,gBAAgB,CAAC,CAiC3B"}
1
+ {"version":3,"file":"system-one-shared.d.ts","sourceRoot":"","sources":["../../src/api/system-one-shared.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEX,aAAa,EACb,iBAAiB,EACjB,eAAe,EACf,iBAAiB,EACjB,gBAAgB,EAEhB,eAAe,EACf,MAAM,aAAa,CAAC;AAIrB,uEAAuE;AACvE,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,KAAK,GAAG,KAAK,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;CACpB;AAQD,gFAAgF;AAChF,MAAM,WAAW,oBAAoB;IACpC,0GAA0G;IAC1G,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,+DAA+D;IAC/D,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;IAC7C,2DAA2D;IAC3D,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,mBAAmB,GAAG,WAAW,CAAC,eAAe,CAAC,YAAY,CAAC,CAAC,CAAC;AAE7E,mEAAmE;AACnE,MAAM,MAAM,uBAAuB,GAAG,QAAQ,CAAC,eAAe,CAAC,CAAC;AAKhE,iEAAiE;AACjE,MAAM,WAAW,kBAAkB;IAClC,oDAAoD;IACpD,GAAG,EAAE,aAAa,CAAC;IACnB,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,0EAA0E;IAC1E,YAAY,CAAC,KAAK,EAAE,eAAe,CAAC,aAAa,CAAC,GAAG,uBAAuB,CAAC;IAC7E,4BAA4B;IAC5B,GAAG,CAAC,KAAK,EAAE,eAAe,CAAC,aAAa,CAAC,GAAG,GAAG,CAAC;IAChD,sEAAsE;IACtE,OAAO,CAAC,KAAK,EAAE,eAAe,CAAC,aAAa,CAAC,EAAE,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC;IACvF,mEAAmE;IACnE,MAAM,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/C;AAkJD,mEAAmE;AACnE,wBAAsB,iBAAiB,CACtC,SAAS,EAAE,kBAAkB,EAC7B,KAAK,EAAE,eAAe,CAAC,aAAa,CAAC,EACrC,OAAO,EAAE,iBAAiB,EAC1B,OAAO,EAAE,iBAAiB,GAAG,SAAS,GACpC,OAAO,CAAC,gBAAgB,CAAC,CA0C3B"}
@@ -1,17 +1,43 @@
1
1
  import { formatProviderError, normalizeProviderError } from "../utils/error-body.js";
2
2
  import { isRecord, parseClassifierUsage, postClassifierRequest, requiredNumber } from "./classifier-shared.js";
3
+ /** TypeSafe's opt-in header for its experimental image input. */
4
+ const IMAGE_OPT_IN_HEADERS = { "TypeSafe-Danger-Zone-Images": "true" };
3
5
  const IMAGES_STATE_FIELD = "images";
4
6
  const IMAGE_FORMATS = new Map([
5
7
  ["image/png", "png"],
6
8
  ["image/jpeg", "jpg"],
7
9
  ["image/jpg", "jpg"],
8
10
  ]);
9
- /** Lists each image once in `assets` under a fresh UUID and writes the UUIDs into `state.images`, in order. */
10
- function imageAssets(label, context) {
11
- const images = context.images ?? [];
12
- if (images.length === 0)
13
- return { state: context.state };
14
- if (Object.hasOwn(context.state, IMAGES_STATE_FIELD)) {
11
+ const DATA_URL_TYPES = new Set(["image/png", "image/jpeg", "image/webp"]);
12
+ /** Clef accepts at most this many images per request. */
13
+ const MAX_DATA_URL_IMAGES = 4;
14
+ function base64Data(image) {
15
+ return image.data.replace(/^data:[^,]*,/u, "");
16
+ }
17
+ /** Adds the context images to `request` in the service's form. */
18
+ function addImages(label, model, input, images, request) {
19
+ if (input === "images") {
20
+ if (images.length > MAX_DATA_URL_IMAGES) {
21
+ throw new Error(`${label} accepts at most ${MAX_DATA_URL_IMAGES} images for ${model.id}, got ${images.length}`);
22
+ }
23
+ request.images = images.map((image) => {
24
+ if (!DATA_URL_TYPES.has(image.mimeType)) {
25
+ throw new Error(`${label} accepts PNG, JPEG, and WebP images for ${model.id}, got ${image.mimeType}`);
26
+ }
27
+ return `data:${image.mimeType};base64,${base64Data(image)}`;
28
+ });
29
+ return;
30
+ }
31
+ if (input === "state-image-urls") {
32
+ const parts = images.map((image) => ({
33
+ type: "image_url",
34
+ image_url: { url: `data:${image.mimeType};base64,${base64Data(image)}` },
35
+ }));
36
+ request.state = [...parts, request.state];
37
+ return;
38
+ }
39
+ const state = request.state;
40
+ if (Object.hasOwn(state, IMAGES_STATE_FIELD)) {
15
41
  throw new Error(`${label} lists images in state.${IMAGES_STATE_FIELD}; rename that state field to send images`);
16
42
  }
17
43
  const assets = {};
@@ -19,13 +45,10 @@ function imageAssets(label, context) {
19
45
  const format = IMAGE_FORMATS.get(image.mimeType.toLowerCase());
20
46
  if (!format)
21
47
  throw new Error(`${label} accepts only PNG or JPEG images, got ${image.mimeType}`);
22
- assets[crypto.randomUUID()] = {
23
- type: "image",
24
- format,
25
- data_base64: image.data.replace(/^data:[^,]*,/u, ""),
26
- };
48
+ assets[crypto.randomUUID()] = { type: "image", format, data_base64: base64Data(image) };
27
49
  }
28
- return { state: { ...context.state, [IMAGES_STATE_FIELD]: Object.keys(assets) }, assets };
50
+ request.state = { ...state, [IMAGES_STATE_FIELD]: Object.keys(assets) };
51
+ request.assets = assets;
29
52
  }
30
53
  function unitInterval(label, value, field) {
31
54
  const number = requiredNumber(label, value, field);
@@ -100,15 +123,13 @@ function parseAnswers(label, value, context) {
100
123
  return Object.fromEntries(answers);
101
124
  }
102
125
  /** Maps public `bool` questions to TypeSafe's wire-level `noul` type. */
103
- function wireRequest(label, context) {
104
- const { state, assets } = imageAssets(label, context);
126
+ function wireRequest(context) {
105
127
  return {
106
- state,
128
+ state: context.state,
107
129
  questions: Object.fromEntries(Object.entries(context.questions).map(([id, question]) => [
108
130
  id,
109
131
  question.type === "bool" ? { ...question, type: "noul" } : question,
110
132
  ])),
111
- ...(assets ? { assets } : {}),
112
133
  };
113
134
  }
114
135
  /** Runs one System One classification over the given transport. */
@@ -124,10 +145,19 @@ export async function classifySystemOne(transport, model, context, options) {
124
145
  try {
125
146
  if (model.api !== transport.api)
126
147
  throw new Error(`Unsupported classifier API: ${model.api}`);
127
- if (context.images?.length && !transport.acceptsImages?.(model)) {
128
- throw new Error(`${transport.label} does not support image input`);
148
+ const request = wireRequest(context);
149
+ let requestOptions = options;
150
+ if (context.images?.length) {
151
+ if (!model.input.includes("image")) {
152
+ throw new Error(`${transport.label} does not support image input for ${model.id}`);
153
+ }
154
+ const detected = transport.detectCompat(model);
155
+ addImages(transport.label, model, model.compat?.imageInput ?? detected.imageInput, context.images, request);
156
+ if (model.compat?.sendImageOptInHeader ?? detected.sendImageOptInHeader) {
157
+ requestOptions = { ...options, headers: { ...IMAGE_OPT_IN_HEADERS, ...options?.headers } };
158
+ }
129
159
  }
130
- const body = await postClassifierRequest(transport.label, transport.url(model), model, transport.payload(model, wireRequest(transport.label, context)), options);
160
+ const body = await postClassifierRequest(transport.label, transport.url(model), model, transport.payload(model, request), requestOptions);
131
161
  const result = transport.output(body);
132
162
  // Malformed answers may still be billed, so retain usage before parsing them.
133
163
  const usage = parseClassifierUsage(result.usage, model);
@@ -1 +1 @@
1
- {"version":3,"file":"system-one-shared.js","sourceRoot":"","sources":["../../src/api/system-one-shared.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n\tClassifierAnswer,\n\tClassifierApi,\n\tClassifierContext,\n\tClassifierModel,\n\tClassifierOptions,\n\tClassifierResult,\n} from \"../types.ts\";\nimport { formatProviderError, normalizeProviderError } from \"../utils/error-body.ts\";\nimport { isRecord, parseClassifierUsage, postClassifierRequest, requiredNumber } from \"./classifier-shared.ts\";\n\n/** Base64 image in the System One request's top-level `assets` map. */\nexport interface SystemOneImageAsset {\n\ttype: \"image\";\n\tformat: \"png\" | \"jpg\";\n\tdata_base64: string;\n}\n\n/** TypeSafe System One request body without the transport-specific envelope. */\nexport interface SystemOneWireRequest {\n\tstate: ClassifierContext[\"state\"];\n\tquestions: Record<string, unknown>;\n\t/** Images keyed by the references listed in `state.images`. */\n\tassets?: Record<string, SystemOneImageAsset>;\n}\n\n/** Differences between services that serve System One models. */\nexport interface SystemOneTransport {\n\t/** Classifier API implemented by this transport. */\n\tapi: ClassifierApi;\n\t/** Service name used in error messages. */\n\tlabel: string;\n\t/** Absolute request URL. */\n\turl(model: ClassifierModel<ClassifierApi>): URL;\n\t/** Wraps the System One request in the service's request envelope. */\n\tpayload(model: ClassifierModel<ClassifierApi>, request: SystemOneWireRequest): unknown;\n\t/** Extracts the System One output, including answers and usage. */\n\toutput(body: unknown): Record<string, unknown>;\n\t/** Whether the service accepts `context.images` as `assets` for this model. Images are rejected when omitted. */\n\tacceptsImages?(model: ClassifierModel<ClassifierApi>): boolean;\n}\n\nconst IMAGES_STATE_FIELD = \"images\";\nconst IMAGE_FORMATS = new Map<string, SystemOneImageAsset[\"format\"]>([\n\t[\"image/png\", \"png\"],\n\t[\"image/jpeg\", \"jpg\"],\n\t[\"image/jpg\", \"jpg\"],\n]);\n\n/** Lists each image once in `assets` under a fresh UUID and writes the UUIDs into `state.images`, in order. */\nfunction imageAssets(label: string, context: ClassifierContext): Pick<SystemOneWireRequest, \"state\" | \"assets\"> {\n\tconst images = context.images ?? [];\n\tif (images.length === 0) return { state: context.state };\n\tif (Object.hasOwn(context.state, IMAGES_STATE_FIELD)) {\n\t\tthrow new Error(`${label} lists images in state.${IMAGES_STATE_FIELD}; rename that state field to send images`);\n\t}\n\tconst assets: Record<string, SystemOneImageAsset> = {};\n\tfor (const image of images) {\n\t\tconst format = IMAGE_FORMATS.get(image.mimeType.toLowerCase());\n\t\tif (!format) throw new Error(`${label} accepts only PNG or JPEG images, got ${image.mimeType}`);\n\t\tassets[crypto.randomUUID()] = {\n\t\t\ttype: \"image\",\n\t\t\tformat,\n\t\t\tdata_base64: image.data.replace(/^data:[^,]*,/u, \"\"),\n\t\t};\n\t}\n\treturn { state: { ...context.state, [IMAGES_STATE_FIELD]: Object.keys(assets) }, assets };\n}\n\n\nfunction unitInterval(label: string, value: unknown, field: string): number {\n\tconst number = requiredNumber(label, value, field);\n\tif (number < 0 || number > 1) throw new Error(`${label} returned an invalid ${field}`);\n\treturn number;\n}\n\nfunction probabilities(label: string, value: unknown, id: string): Record<string, number> {\n\tif (!isRecord(value)) throw new Error(`${label} returned invalid probabilities for ${id}`);\n\treturn Object.fromEntries(\n\t\tObject.entries(value).map(([key, probability]) => [\n\t\t\tkey,\n\t\t\tunitInterval(label, probability, `probability for ${id}.${key}`),\n\t\t]),\n\t);\n}\n\nfunction parseAnswers(label: string, value: unknown, context: ClassifierContext): Record<string, ClassifierAnswer> {\n\tif (!isRecord(value)) throw new Error(`${label} returned an unexpected response`);\n\tconst answers: Array<[string, ClassifierAnswer]> = [];\n\tfor (const [id, question] of Object.entries(context.questions)) {\n\t\tconst answer = value[id];\n\t\tif (!isRecord(answer)) throw new Error(`${label} did not return an answer for ${id}`);\n\t\tif (question.type === \"choice\") {\n\t\t\tif (\n\t\t\t\tanswer.type !== \"choice\" ||\n\t\t\t\ttypeof answer.choice !== \"string\" ||\n\t\t\t\t!Object.hasOwn(question.criteria, answer.choice)\n\t\t\t) {\n\t\t\t\tthrow new Error(`${label} did not return a valid choice answer for ${id}`);\n\t\t\t}\n\t\t\tconst choiceProbabilities = probabilities(label, answer.probabilities, id);\n\t\t\tif (Object.keys(choiceProbabilities).some((key) => !Object.hasOwn(question.criteria, key))) {\n\t\t\t\tthrow new Error(`${label} returned an unknown choice probability for ${id}`);\n\t\t\t}\n\t\t\tanswers.push([\n\t\t\t\tid,\n\t\t\t\t{\n\t\t\t\t\ttype: \"choice\",\n\t\t\t\t\tchoice: answer.choice,\n\t\t\t\t\tprobabilities: choiceProbabilities,\n\t\t\t\t\tconfidence: unitInterval(label, answer.confidence, `confidence for ${id}`),\n\t\t\t\t},\n\t\t\t]);\n\t\t} else if (question.type === \"score\") {\n\t\t\tif (answer.type !== \"score\") throw new Error(`${label} did not return a score answer for ${id}`);\n\t\t\tconst score = requiredNumber(label, answer.score, `score for ${id}`);\n\t\t\tif (score < 0 || score > question.criteria.length - 1) {\n\t\t\t\tthrow new Error(`${label} returned an invalid score for ${id}`);\n\t\t\t}\n\t\t\tanswers.push([\n\t\t\t\tid,\n\t\t\t\t{\n\t\t\t\t\ttype: \"score\",\n\t\t\t\t\tscore,\n\t\t\t\t\tconfidence: unitInterval(label, answer.confidence, `confidence for ${id}`),\n\t\t\t\t},\n\t\t\t]);\n\t\t} else {\n\t\t\tif (answer.type !== \"noul\") throw new Error(`${label} did not return a bool answer for ${id}`);\n\t\t\tanswers.push([\n\t\t\t\tid,\n\t\t\t\t{\n\t\t\t\t\ttype: \"bool\",\n\t\t\t\t\tprobability: unitInterval(label, answer.noul, `probability for ${id}`),\n\t\t\t\t},\n\t\t\t]);\n\t\t}\n\t}\n\treturn Object.fromEntries(answers);\n}\n\n\n/** Maps public `bool` questions to TypeSafe's wire-level `noul` type. */\nfunction wireRequest(label: string, context: ClassifierContext): SystemOneWireRequest {\n\tconst { state, assets } = imageAssets(label, context);\n\treturn {\n\t\tstate,\n\t\tquestions: Object.fromEntries(\n\t\t\tObject.entries(context.questions).map(([id, question]) => [\n\t\t\t\tid,\n\t\t\t\tquestion.type === \"bool\" ? { ...question, type: \"noul\" } : question,\n\t\t\t]),\n\t\t),\n\t\t...(assets ? { assets } : {}),\n\t};\n}\n\n\n/** Runs one System One classification over the given transport. */\nexport async function classifySystemOne(\n\ttransport: SystemOneTransport,\n\tmodel: ClassifierModel<ClassifierApi>,\n\tcontext: ClassifierContext,\n\toptions: ClassifierOptions | undefined,\n): Promise<ClassifierResult> {\n\tconst output: ClassifierResult = {\n\t\tapi: model.api,\n\t\tprovider: model.provider,\n\t\tmodel: model.id,\n\t\tanswers: {},\n\t\tstopReason: \"stop\",\n\t\ttimestamp: Date.now(),\n\t};\n\n\ttry {\n\t\tif (model.api !== transport.api) throw new Error(`Unsupported classifier API: ${model.api}`);\n\t\tif (context.images?.length && !transport.acceptsImages?.(model)) {\n\t\t\tthrow new Error(`${transport.label} does not support image input`);\n\t\t}\n\t\tconst body = await postClassifierRequest(\n\t\t\ttransport.label,\n\t\t\ttransport.url(model),\n\t\t\tmodel,\n\t\t\ttransport.payload(model, wireRequest(transport.label, context)),\n\t\t\toptions,\n\t\t);\n\t\tconst result = transport.output(body);\n\t\t// Malformed answers may still be billed, so retain usage before parsing them.\n\t\tconst usage = parseClassifierUsage(result.usage, model);\n\t\tif (usage) output.usage = usage;\n\t\toutput.answers = parseAnswers(transport.label, result.answers, context);\n\t\treturn output;\n\t} catch (error) {\n\t\toutput.stopReason = options?.signal?.aborted ? \"aborted\" : \"error\";\n\t\toutput.errorMessage = formatProviderError(normalizeProviderError(error), `${transport.label} error`);\n\t\treturn output;\n\t}\n}\n"]}
1
+ {"version":3,"file":"system-one-shared.js","sourceRoot":"","sources":["../../src/api/system-one-shared.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n\tClassifierAnswer,\n\tClassifierApi,\n\tClassifierContext,\n\tClassifierModel,\n\tClassifierOptions,\n\tClassifierResult,\n\tImageContent,\n\tSystemOneCompat,\n} from \"../types.ts\";\nimport { formatProviderError, normalizeProviderError } from \"../utils/error-body.ts\";\nimport { isRecord, parseClassifierUsage, postClassifierRequest, requiredNumber } from \"./classifier-shared.ts\";\n\n/** Base64 image in the System One request's top-level `assets` map. */\nexport interface SystemOneImageAsset {\n\ttype: \"image\";\n\tformat: \"png\" | \"jpg\";\n\tdata_base64: string;\n}\n\n/** An image in the state, OpenRouter's form. */\ninterface SystemOneImageUrlPart {\n\ttype: \"image_url\";\n\timage_url: { url: string };\n}\n\n/** TypeSafe System One request body without the transport-specific envelope. */\nexport interface SystemOneWireRequest {\n\t/** The context state, or with images in OpenRouter's form a list of image parts followed by the state. */\n\tstate: unknown;\n\tquestions: Record<string, unknown>;\n\t/** Images keyed by the references listed in `state.images`. */\n\tassets?: Record<string, SystemOneImageAsset>;\n\t/** Images as data URLs, Clef's extension to System One. */\n\timages?: string[];\n}\n\n/**\n * How a service takes images (`SystemOneCompat.imageInput`):\n * - `assets`: TypeSafe's `assets` map, keyed by a fresh UUID per image and listed in order under\n * `state.images`. PNG and JPEG only.\n * - `images`: Clef's `images` list of data URLs, which the model reads before the state.\n * PNG, JPEG, and WebP, at most four.\n * - `state-image-urls`: OpenRouter's form, data URL parts placed before the state:\n * `state: [{ type: \"image_url\", image_url: { url } }, state]`. OpenRouter accepts this for every\n * model and drops `assets` silently.\n */\nexport type SystemOneImageInput = NonNullable<SystemOneCompat[\"imageInput\"]>;\n\n/** System One compatibility settings with every field resolved. */\nexport type ResolvedSystemOneCompat = Required<SystemOneCompat>;\n\n/** TypeSafe's opt-in header for its experimental image input. */\nconst IMAGE_OPT_IN_HEADERS = { \"TypeSafe-Danger-Zone-Images\": \"true\" };\n\n/** Differences between services that serve System One models. */\nexport interface SystemOneTransport {\n\t/** Classifier API implemented by this transport. */\n\tapi: ClassifierApi;\n\t/** Service name used in error messages. */\n\tlabel: string;\n\t/** Compatibility settings for models that do not set them in `compat`. */\n\tdetectCompat(model: ClassifierModel<ClassifierApi>): ResolvedSystemOneCompat;\n\t/** Absolute request URL. */\n\turl(model: ClassifierModel<ClassifierApi>): URL;\n\t/** Wraps the System One request in the service's request envelope. */\n\tpayload(model: ClassifierModel<ClassifierApi>, request: SystemOneWireRequest): unknown;\n\t/** Extracts the System One output, including answers and usage. */\n\toutput(body: unknown): Record<string, unknown>;\n}\n\nconst IMAGES_STATE_FIELD = \"images\";\nconst IMAGE_FORMATS = new Map<string, SystemOneImageAsset[\"format\"]>([\n\t[\"image/png\", \"png\"],\n\t[\"image/jpeg\", \"jpg\"],\n\t[\"image/jpg\", \"jpg\"],\n]);\nconst DATA_URL_TYPES = new Set([\"image/png\", \"image/jpeg\", \"image/webp\"]);\n/** Clef accepts at most this many images per request. */\nconst MAX_DATA_URL_IMAGES = 4;\n\nfunction base64Data(image: ImageContent): string {\n\treturn image.data.replace(/^data:[^,]*,/u, \"\");\n}\n\n/** Adds the context images to `request` in the service's form. */\nfunction addImages(\n\tlabel: string,\n\tmodel: ClassifierModel<ClassifierApi>,\n\tinput: SystemOneImageInput,\n\timages: ImageContent[],\n\trequest: SystemOneWireRequest,\n): void {\n\tif (input === \"images\") {\n\t\tif (images.length > MAX_DATA_URL_IMAGES) {\n\t\t\tthrow new Error(\n\t\t\t\t`${label} accepts at most ${MAX_DATA_URL_IMAGES} images for ${model.id}, got ${images.length}`,\n\t\t\t);\n\t\t}\n\t\trequest.images = images.map((image) => {\n\t\t\tif (!DATA_URL_TYPES.has(image.mimeType)) {\n\t\t\t\tthrow new Error(`${label} accepts PNG, JPEG, and WebP images for ${model.id}, got ${image.mimeType}`);\n\t\t\t}\n\t\t\treturn `data:${image.mimeType};base64,${base64Data(image)}`;\n\t\t});\n\t\treturn;\n\t}\n\tif (input === \"state-image-urls\") {\n\t\tconst parts = images.map(\n\t\t\t(image): SystemOneImageUrlPart => ({\n\t\t\t\ttype: \"image_url\",\n\t\t\t\timage_url: { url: `data:${image.mimeType};base64,${base64Data(image)}` },\n\t\t\t}),\n\t\t);\n\t\trequest.state = [...parts, request.state];\n\t\treturn;\n\t}\n\tconst state = request.state as ClassifierContext[\"state\"];\n\tif (Object.hasOwn(state, IMAGES_STATE_FIELD)) {\n\t\tthrow new Error(`${label} lists images in state.${IMAGES_STATE_FIELD}; rename that state field to send images`);\n\t}\n\tconst assets: Record<string, SystemOneImageAsset> = {};\n\tfor (const image of images) {\n\t\tconst format = IMAGE_FORMATS.get(image.mimeType.toLowerCase());\n\t\tif (!format) throw new Error(`${label} accepts only PNG or JPEG images, got ${image.mimeType}`);\n\t\tassets[crypto.randomUUID()] = { type: \"image\", format, data_base64: base64Data(image) };\n\t}\n\trequest.state = { ...state, [IMAGES_STATE_FIELD]: Object.keys(assets) };\n\trequest.assets = assets;\n}\n\nfunction unitInterval(label: string, value: unknown, field: string): number {\n\tconst number = requiredNumber(label, value, field);\n\tif (number < 0 || number > 1) throw new Error(`${label} returned an invalid ${field}`);\n\treturn number;\n}\n\nfunction probabilities(label: string, value: unknown, id: string): Record<string, number> {\n\tif (!isRecord(value)) throw new Error(`${label} returned invalid probabilities for ${id}`);\n\treturn Object.fromEntries(\n\t\tObject.entries(value).map(([key, probability]) => [\n\t\t\tkey,\n\t\t\tunitInterval(label, probability, `probability for ${id}.${key}`),\n\t\t]),\n\t);\n}\n\nfunction parseAnswers(label: string, value: unknown, context: ClassifierContext): Record<string, ClassifierAnswer> {\n\tif (!isRecord(value)) throw new Error(`${label} returned an unexpected response`);\n\tconst answers: Array<[string, ClassifierAnswer]> = [];\n\tfor (const [id, question] of Object.entries(context.questions)) {\n\t\tconst answer = value[id];\n\t\tif (!isRecord(answer)) throw new Error(`${label} did not return an answer for ${id}`);\n\t\tif (question.type === \"choice\") {\n\t\t\tif (\n\t\t\t\tanswer.type !== \"choice\" ||\n\t\t\t\ttypeof answer.choice !== \"string\" ||\n\t\t\t\t!Object.hasOwn(question.criteria, answer.choice)\n\t\t\t) {\n\t\t\t\tthrow new Error(`${label} did not return a valid choice answer for ${id}`);\n\t\t\t}\n\t\t\tconst choiceProbabilities = probabilities(label, answer.probabilities, id);\n\t\t\tif (Object.keys(choiceProbabilities).some((key) => !Object.hasOwn(question.criteria, key))) {\n\t\t\t\tthrow new Error(`${label} returned an unknown choice probability for ${id}`);\n\t\t\t}\n\t\t\tanswers.push([\n\t\t\t\tid,\n\t\t\t\t{\n\t\t\t\t\ttype: \"choice\",\n\t\t\t\t\tchoice: answer.choice,\n\t\t\t\t\tprobabilities: choiceProbabilities,\n\t\t\t\t\tconfidence: unitInterval(label, answer.confidence, `confidence for ${id}`),\n\t\t\t\t},\n\t\t\t]);\n\t\t} else if (question.type === \"score\") {\n\t\t\tif (answer.type !== \"score\") throw new Error(`${label} did not return a score answer for ${id}`);\n\t\t\tconst score = requiredNumber(label, answer.score, `score for ${id}`);\n\t\t\tif (score < 0 || score > question.criteria.length - 1) {\n\t\t\t\tthrow new Error(`${label} returned an invalid score for ${id}`);\n\t\t\t}\n\t\t\tanswers.push([\n\t\t\t\tid,\n\t\t\t\t{\n\t\t\t\t\ttype: \"score\",\n\t\t\t\t\tscore,\n\t\t\t\t\tconfidence: unitInterval(label, answer.confidence, `confidence for ${id}`),\n\t\t\t\t},\n\t\t\t]);\n\t\t} else {\n\t\t\tif (answer.type !== \"noul\") throw new Error(`${label} did not return a bool answer for ${id}`);\n\t\t\tanswers.push([\n\t\t\t\tid,\n\t\t\t\t{\n\t\t\t\t\ttype: \"bool\",\n\t\t\t\t\tprobability: unitInterval(label, answer.noul, `probability for ${id}`),\n\t\t\t\t},\n\t\t\t]);\n\t\t}\n\t}\n\treturn Object.fromEntries(answers);\n}\n\n/** Maps public `bool` questions to TypeSafe's wire-level `noul` type. */\nfunction wireRequest(context: ClassifierContext): SystemOneWireRequest {\n\treturn {\n\t\tstate: context.state,\n\t\tquestions: Object.fromEntries(\n\t\t\tObject.entries(context.questions).map(([id, question]) => [\n\t\t\t\tid,\n\t\t\t\tquestion.type === \"bool\" ? { ...question, type: \"noul\" } : question,\n\t\t\t]),\n\t\t),\n\t};\n}\n\n/** Runs one System One classification over the given transport. */\nexport async function classifySystemOne(\n\ttransport: SystemOneTransport,\n\tmodel: ClassifierModel<ClassifierApi>,\n\tcontext: ClassifierContext,\n\toptions: ClassifierOptions | undefined,\n): Promise<ClassifierResult> {\n\tconst output: ClassifierResult = {\n\t\tapi: model.api,\n\t\tprovider: model.provider,\n\t\tmodel: model.id,\n\t\tanswers: {},\n\t\tstopReason: \"stop\",\n\t\ttimestamp: Date.now(),\n\t};\n\n\ttry {\n\t\tif (model.api !== transport.api) throw new Error(`Unsupported classifier API: ${model.api}`);\n\t\tconst request = wireRequest(context);\n\t\tlet requestOptions = options;\n\t\tif (context.images?.length) {\n\t\t\tif (!model.input.includes(\"image\")) {\n\t\t\t\tthrow new Error(`${transport.label} does not support image input for ${model.id}`);\n\t\t\t}\n\t\t\tconst detected = transport.detectCompat(model);\n\t\t\taddImages(transport.label, model, model.compat?.imageInput ?? detected.imageInput, context.images, request);\n\t\t\tif (model.compat?.sendImageOptInHeader ?? detected.sendImageOptInHeader) {\n\t\t\t\trequestOptions = { ...options, headers: { ...IMAGE_OPT_IN_HEADERS, ...options?.headers } };\n\t\t\t}\n\t\t}\n\t\tconst body = await postClassifierRequest(\n\t\t\ttransport.label,\n\t\t\ttransport.url(model),\n\t\t\tmodel,\n\t\t\ttransport.payload(model, request),\n\t\t\trequestOptions,\n\t\t);\n\t\tconst result = transport.output(body);\n\t\t// Malformed answers may still be billed, so retain usage before parsing them.\n\t\tconst usage = parseClassifierUsage(result.usage, model);\n\t\tif (usage) output.usage = usage;\n\t\toutput.answers = parseAnswers(transport.label, result.answers, context);\n\t\treturn output;\n\t} catch (error) {\n\t\toutput.stopReason = options?.signal?.aborted ? \"aborted\" : \"error\";\n\t\toutput.errorMessage = formatProviderError(normalizeProviderError(error), `${transport.label} error`);\n\t\treturn output;\n\t}\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"typesafe-system-one.d.ts","sourceRoot":"","sources":["../../src/api/typesafe-system-one.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAqBzE,gGAAgG;AAChG,eAAO,MAAM,QAAQ,EAAE,kBAAkB,CAAC,iBAAiB,CACL,CAAC"}
1
+ {"version":3,"file":"typesafe-system-one.d.ts","sourceRoot":"","sources":["../../src/api/typesafe-system-one.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AA6BzE,gGAAgG;AAChG,eAAO,MAAM,QAAQ,EAAE,kBAAkB,CAAC,iBAAiB,CACL,CAAC"}
@@ -2,12 +2,20 @@ import { isRecord } from "./classifier-shared.js";
2
2
  import { classifySystemOne } from "./system-one-shared.js";
3
3
  /**
4
4
  * TypeSafe's native System One protocol. OpenRouter serves the same protocol,
5
- * so both providers use this API with different base URLs. Only direct TypeSafe
6
- * models with image input send images, as referenced `assets`.
5
+ * so both providers use this API with different base URLs.
6
+ *
7
+ * TypeSafe takes images as referenced `assets`. They are experimental and need the
8
+ * `TypeSafe-Danger-Zone-Images` opt-in header; without it the API answers 400.
9
+ * OpenRouter drops `assets` silently and takes images as `image_url` parts in the state instead.
10
+ * Models behind a proxy that hides both the provider ID and the OpenRouter host must set
11
+ * `compat.imageInput`.
7
12
  */
8
13
  const transport = {
9
14
  api: "typesafe-system-one",
10
15
  label: "System One API",
16
+ detectCompat: (model) => model.provider === "openrouter" || model.baseUrl.includes("openrouter.ai")
17
+ ? { imageInput: "state-image-urls", sendImageOptInHeader: false }
18
+ : { imageInput: "assets", sendImageOptInHeader: true },
11
19
  url: (model) => new URL("systemone", `${model.baseUrl.replace(/\/+$/u, "")}/`),
12
20
  payload: (model, request) => ({ model: model.id, ...request }),
13
21
  output: (body) => {
@@ -15,7 +23,6 @@ const transport = {
15
23
  throw new Error("System One API returned an unexpected response");
16
24
  return body;
17
25
  },
18
- acceptsImages: (model) => model.provider === "typesafe" && model.input.includes("image"),
19
26
  };
20
27
  /** TypeSafe System One classification with public `bool` values mapped to wire-level `noul`. */
21
28
  export const classify = (model, context, options) => classifySystemOne(transport, model, context, options);
@@ -1 +1 @@
1
- {"version":3,"file":"typesafe-system-one.js","sourceRoot":"","sources":["../../src/api/typesafe-system-one.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAA2B,MAAM,wBAAwB,CAAC;AAEpF;;;;GAIG;AACH,MAAM,SAAS,GAAuB;IACrC,GAAG,EAAE,qBAAqB;IAC1B,KAAK,EAAE,gBAAgB;IACvB,GAAG,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,WAAW,EAAE,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,GAAG,CAAC;IAC9E,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,EAAE,GAAG,OAAO,EAAE,CAAC;IAC9D,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;QAChB,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;QACvF,OAAO,IAAI,CAAC;IACb,CAAC;IACD,aAAa,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,KAAK,UAAU,IAAI,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC;CACxF,CAAC;AAEF,gGAAgG;AAChG,MAAM,CAAC,MAAM,QAAQ,GAA0C,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,CAC1F,iBAAiB,CAAC,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC","sourcesContent":["import type { ClassifierFunction, ClassifierOptions } from \"../types.ts\";\nimport { isRecord } from \"./classifier-shared.ts\";\nimport { classifySystemOne, type SystemOneTransport } from \"./system-one-shared.ts\";\n\n/**\n * TypeSafe's native System One protocol. OpenRouter serves the same protocol,\n * so both providers use this API with different base URLs. Only direct TypeSafe\n * models with image input send images, as referenced `assets`.\n */\nconst transport: SystemOneTransport = {\n\tapi: \"typesafe-system-one\",\n\tlabel: \"System One API\",\n\turl: (model) => new URL(\"systemone\", `${model.baseUrl.replace(/\\/+$/u, \"\")}/`),\n\tpayload: (model, request) => ({ model: model.id, ...request }),\n\toutput: (body) => {\n\t\tif (!isRecord(body)) throw new Error(\"System One API returned an unexpected response\");\n\t\treturn body;\n\t},\n\tacceptsImages: (model) => model.provider === \"typesafe\" && model.input.includes(\"image\"),\n};\n\n/** TypeSafe System One classification with public `bool` values mapped to wire-level `noul`. */\nexport const classify: ClassifierFunction<ClassifierOptions> = (model, context, options) =>\n\tclassifySystemOne(transport, model, context, options);\n"]}
1
+ {"version":3,"file":"typesafe-system-one.js","sourceRoot":"","sources":["../../src/api/typesafe-system-one.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAA2B,MAAM,wBAAwB,CAAC;AAEpF;;;;;;;;;GASG;AACH,MAAM,SAAS,GAAuB;IACrC,GAAG,EAAE,qBAAqB;IAC1B,KAAK,EAAE,gBAAgB;IACvB,YAAY,EAAE,CAAC,KAAK,EAAE,EAAE,CACvB,KAAK,CAAC,QAAQ,KAAK,YAAY,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC;QACzE,CAAC,CAAC,EAAE,UAAU,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,KAAK,EAAE;QACjE,CAAC,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,oBAAoB,EAAE,IAAI,EAAE;IACxD,GAAG,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,WAAW,EAAE,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,GAAG,CAAC;IAC9E,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,EAAE,GAAG,OAAO,EAAE,CAAC;IAC9D,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;QAChB,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;QACvF,OAAO,IAAI,CAAC;IACb,CAAC;CACD,CAAC;AAEF,gGAAgG;AAChG,MAAM,CAAC,MAAM,QAAQ,GAA0C,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,CAC1F,iBAAiB,CAAC,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC","sourcesContent":["import type { ClassifierFunction, ClassifierOptions } from \"../types.ts\";\nimport { isRecord } from \"./classifier-shared.ts\";\nimport { classifySystemOne, type SystemOneTransport } from \"./system-one-shared.ts\";\n\n/**\n * TypeSafe's native System One protocol. OpenRouter serves the same protocol,\n * so both providers use this API with different base URLs.\n *\n * TypeSafe takes images as referenced `assets`. They are experimental and need the\n * `TypeSafe-Danger-Zone-Images` opt-in header; without it the API answers 400.\n * OpenRouter drops `assets` silently and takes images as `image_url` parts in the state instead.\n * Models behind a proxy that hides both the provider ID and the OpenRouter host must set\n * `compat.imageInput`.\n */\nconst transport: SystemOneTransport = {\n\tapi: \"typesafe-system-one\",\n\tlabel: \"System One API\",\n\tdetectCompat: (model) =>\n\t\tmodel.provider === \"openrouter\" || model.baseUrl.includes(\"openrouter.ai\")\n\t\t\t? { imageInput: \"state-image-urls\", sendImageOptInHeader: false }\n\t\t\t: { imageInput: \"assets\", sendImageOptInHeader: true },\n\turl: (model) => new URL(\"systemone\", `${model.baseUrl.replace(/\\/+$/u, \"\")}/`),\n\tpayload: (model, request) => ({ model: model.id, ...request }),\n\toutput: (body) => {\n\t\tif (!isRecord(body)) throw new Error(\"System One API returned an unexpected response\");\n\t\treturn body;\n\t},\n};\n\n/** TypeSafe System One classification with public `bool` values mapped to wire-level `noul`. */\nexport const classify: ClassifierFunction<ClassifierOptions> = (model, context, options) =>\n\tclassifySystemOne(transport, model, context, options);\n"]}
@@ -188,6 +188,10 @@ export declare const BedrockCompatSchema: Type.TObject<{
188
188
  export declare const MistralConversationsCompatSchema: Type.TObject<{
189
189
  supportsMidConvoSystemMessages: Type.TOptional<Type.TBoolean>;
190
190
  }>;
191
+ export declare const SystemOneCompatSchema: Type.TObject<{
192
+ imageInput: Type.TOptional<Type.TUnion<[Type.TLiteral<"assets">, Type.TLiteral<"images">, Type.TLiteral<"state-image-urls">]>>;
193
+ sendImageOptInHeader: Type.TOptional<Type.TBoolean>;
194
+ }>;
191
195
  type UnionToIntersection<Union> = (Union extends unknown ? (value: Union) => void : never) extends (value: infer Intersection) => void ? Intersection : never;
192
196
  type MergedCompatProperties<PropertyGroups extends readonly TProperties[], Overrides extends TProperties> = Omit<UnionToIntersection<PropertyGroups[number]>, keyof Overrides> & Overrides;
193
197
  export declare const CompatSchemasByApi: {
@@ -345,6 +349,14 @@ export declare const CompatSchemasByApi: {
345
349
  readonly "mistral-conversations": Type.TObject<{
346
350
  supportsMidConvoSystemMessages: Type.TOptional<Type.TBoolean>;
347
351
  }>;
352
+ readonly "typesafe-system-one": Type.TObject<{
353
+ imageInput: Type.TOptional<Type.TUnion<[Type.TLiteral<"assets">, Type.TLiteral<"images">, Type.TLiteral<"state-image-urls">]>>;
354
+ sendImageOptInHeader: Type.TOptional<Type.TBoolean>;
355
+ }>;
356
+ readonly "cloudflare-workers-ai-system-one": Type.TObject<{
357
+ imageInput: Type.TOptional<Type.TUnion<[Type.TLiteral<"assets">, Type.TLiteral<"images">, Type.TLiteral<"state-image-urls">]>>;
358
+ sendImageOptInHeader: Type.TOptional<Type.TBoolean>;
359
+ }>;
348
360
  };
349
361
  export declare const ProviderCompatSchema: Type.TObject<MergedCompatProperties<({
350
362
  supportsStore: Type.TOptional<Type.TBoolean>;
@@ -469,6 +481,9 @@ export declare const ProviderCompatSchema: Type.TObject<MergedCompatProperties<(
469
481
  supportsTemperature: Type.TOptional<Type.TBoolean>;
470
482
  } | {
471
483
  supportsMidConvoSystemMessages: Type.TOptional<Type.TBoolean>;
484
+ } | {
485
+ imageInput: Type.TOptional<Type.TUnion<[Type.TLiteral<"assets">, Type.TLiteral<"images">, Type.TLiteral<"state-image-urls">]>>;
486
+ sendImageOptInHeader: Type.TOptional<Type.TBoolean>;
472
487
  })[], {
473
488
  supportsDeveloperRole: Type.TOptional<Type.TBoolean>;
474
489
  supportsMidConvoSystemMessages: Type.TOptional<Type.TBoolean>;
@@ -509,5 +524,8 @@ export interface BedrockCompat extends Static<typeof BedrockCompatSchema> {
509
524
  /** Compatibility settings for the Mistral chat API. */
510
525
  export interface MistralConversationsCompat extends Static<typeof MistralConversationsCompatSchema> {
511
526
  }
527
+ /** Compatibility settings for System One classifier APIs. */
528
+ export interface SystemOneCompat extends Static<typeof SystemOneCompatSchema> {
529
+ }
512
530
  export {};
513
531
  //# sourceMappingURL=compat-schema.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"compat-schema.d.ts","sourceRoot":"","sources":["../../src/providers/compat-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAE,KAAK,WAAW,EAAuB,IAAI,EAAE,MAAM,SAAS,CAAC;AAOnF,eAAO,MAAM,2BAA2B,wGAEtC,CAAC;AASH,eAAO,MAAM,8BAA8B,kIAGzC,CAAC;AAEH,eAAO,MAAM,4BAA4B;;;IAavC,CAAC;AAWH,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAqGnC,CAAC;AAEF,eAAO,MAAM,0BAA0B;;;EAkBtC,CAAC;AAEF,eAAO,MAAM,mCAAmC;;;;;;;;;;;;;;;;EAO/C,CAAC;AAUF,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4KzC,CAAC;AAEF,eAAO,MAAM,2BAA2B;;;;;;;;;;;;EA8DvC,CAAC;AAEF,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAwGzC,CAAC;AAEF,eAAO,MAAM,mBAAmB;;;;EAkB/B,CAAC;AAEF,eAAO,MAAM,gCAAgC;;EAS5C,CAAC;AA6CF,KAAK,mBAAmB,CAAC,KAAK,IAAI,CAAC,KAAK,SAAS,OAAO,GAAG,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,KAAK,CAAC,SAAS,CAClG,KAAK,EAAE,MAAM,YAAY,KACrB,IAAI,GACN,YAAY,GACZ,KAAK,CAAC;AAET,KAAK,sBAAsB,CAAC,cAAc,SAAS,SAAS,WAAW,EAAE,EAAE,SAAS,SAAS,WAAW,IAAI,IAAI,CAC/G,mBAAmB,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,EAC3C,MAAM,SAAS,CACf,GACA,SAAS,CAAC;AAmCX,eAAO,MAAM,kBAAkB;aAC9B,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;aACpB,kBAAkB;;;;;;;;;;;;;aAClB,wBAAwB;;;;;;;;;;;;;aACxB,wBAAwB;;;;;;;;;;;;;aACxB,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;aACpB,yBAAyB;;;;;aACzB,uBAAuB;;;CACd,CAAC;AAOX,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAG/B,CAAC;AAEH,MAAM,MAAM,sBAAsB,GAAG,MAAM,CAAC,OAAO,4BAA4B,CAAC,CAAC;AACjF,yFAAyF;AACzF,MAAM,MAAM,wBAAwB,GAAG,MAAM,CAAC,OAAO,8BAA8B,CAAC,CAAC;AACrF,MAAM,MAAM,qBAAqB,GAAG,MAAM,CAAC,OAAO,2BAA2B,CAAC,CAAC;AAC/E,+CAA+C;AAC/C,MAAM,WAAW,iBAAkB,SAAQ,MAAM,CAAC,OAAO,uBAAuB,CAAC;CAAG;AACpF,6CAA6C;AAC7C,MAAM,WAAW,oBAAqB,SAAQ,MAAM,CAAC,OAAO,0BAA0B,CAAC;CAAG;AAC1F,mFAAmF;AACnF,MAAM,WAAW,6BAA8B,SAAQ,MAAM,CAAC,OAAO,mCAAmC,CAAC;CAAG;AAE5G,qEAAqE;AACrE,MAAM,WAAW,uBAAwB,SAAQ,MAAM,CAAC,OAAO,6BAA6B,CAAC;CAAG;AAChG,wDAAwD;AACxD,MAAM,WAAW,qBAAsB,SAAQ,MAAM,CAAC,OAAO,2BAA2B,CAAC;CAAG;AAC5F,qEAAqE;AACrE,MAAM,WAAW,uBAAwB,SAAQ,MAAM,CAAC,OAAO,6BAA6B,CAAC;CAAG;AAChG,wDAAwD;AACxD,MAAM,WAAW,aAAc,SAAQ,MAAM,CAAC,OAAO,mBAAmB,CAAC;CAAG;AAC5E,uDAAuD;AACvD,MAAM,WAAW,0BAA2B,SAAQ,MAAM,CAAC,OAAO,gCAAgC,CAAC;CAAG"}
1
+ {"version":3,"file":"compat-schema.d.ts","sourceRoot":"","sources":["../../src/providers/compat-schema.ts"],"names":[],"mappings":"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"}
@@ -341,6 +341,14 @@ export const MistralConversationsCompatSchema = Type.Object({
341
341
  default: false,
342
342
  }),
343
343
  }, { description: "Compatibility settings for the Mistral chat API.", additionalProperties: true });
344
+ export const SystemOneCompatSchema = Type.Object({
345
+ imageInput: Type.Optional(Type.Union([Type.Literal("assets"), Type.Literal("images"), Type.Literal("state-image-urls")], {
346
+ description: 'How the service takes classifier images. "assets" sends TypeSafe\'s assets map (PNG and JPEG) and references the images from the state, as TypeSafe and Jev on Workers AI expect. "images" sends Clef\'s images list of data URLs (PNG, JPEG, and WebP, at most four), as Clef on Workers AI expects. "state-image-urls" places image_url parts before the state, as OpenRouter expects; OpenRouter drops assets without an error. Auto-detected from the provider, baseUrl, and model ID. Classifier models read it from the provider-level compat; models and modelOverrides entries apply to chat models only.',
347
+ })),
348
+ sendImageOptInHeader: optionalCompatBoolean({
349
+ description: "Whether requests with images send the TypeSafe-Danger-Zone-Images: true header, which TypeSafe's own API requires for its experimental image input. Defaults to true for the typesafe-system-one API unless the model is detected as OpenRouter.",
350
+ }),
351
+ }, { description: "Compatibility settings for System One classifier APIs.", additionalProperties: true });
344
352
  const ProviderCompatPropertyOverrides = {
345
353
  supportsDeveloperRole: optionalCompatBoolean({
346
354
  description: "Whether the provider supports the developer role instead of system. Defaults are API-specific.",
@@ -412,6 +420,8 @@ export const CompatSchemasByApi = {
412
420
  "anthropic-messages": AnthropicMessagesCompatSchema,
413
421
  "bedrock-converse-stream": BedrockCompatSchema,
414
422
  "mistral-conversations": MistralConversationsCompatSchema,
423
+ "typesafe-system-one": SystemOneCompatSchema,
424
+ "cloudflare-workers-ai-system-one": SystemOneCompatSchema,
415
425
  };
416
426
  const ProviderCompatProperties = mergeCompatProperties([...new Set(Object.values(CompatSchemasByApi))].map((schema) => schema.properties), ProviderCompatPropertyOverrides);
417
427
  export const ProviderCompatSchema = Type.Object(ProviderCompatProperties, {