@mccaskie-labs/workbench-ai 0.0.0-stage → 0.1.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/dist/fake.d.ts ADDED
@@ -0,0 +1,85 @@
1
+ import { type WorkbenchEnv } from "@mccaskie-labs/workbench-config";
2
+ import type { Ai } from "./types.ts";
3
+ export interface FakeReply {
4
+ /** Reply text. For `structured`, give `json` instead. */
5
+ text?: string;
6
+ /** Serialised as the reply text. */
7
+ json?: unknown;
8
+ stopReason?: "end_turn" | "max_tokens" | "refusal" | "stop_sequence" | "pause_turn";
9
+ refusalCategory?: string | null;
10
+ /** The resolved model to report; by default the alias's target from the manifest, or the id asked for. */
11
+ model?: string;
12
+ inputTokens?: number;
13
+ outputTokens?: number;
14
+ costUsd?: number;
15
+ /** Answer with a Gateway error instead, e.g. `{ status: 402, code: "budget_exceeded" }`. */
16
+ error?: {
17
+ status: number;
18
+ code: string;
19
+ message?: string;
20
+ month?: string;
21
+ spentUsd?: number;
22
+ capUsd?: number;
23
+ };
24
+ }
25
+ export interface FakeCall {
26
+ path: string;
27
+ method: string;
28
+ job: string | null;
29
+ promptVersion: string | null;
30
+ subject: string | null;
31
+ /** The parsed JSON body (Anthropic Messages request), if any. */
32
+ body: Record<string, unknown> | null;
33
+ stream: boolean;
34
+ }
35
+ export type ReplyFor = FakeReply | ((call: FakeCall) => FakeReply);
36
+ export interface FakeScript {
37
+ /** Replies in order, one per `messages` call; the last one repeats. A function answers every call. */
38
+ replies?: ReplyFor[] | ((call: FakeCall, index: number) => FakeReply);
39
+ /** What `count_tokens` reports. Default 12. */
40
+ inputTokens?: number;
41
+ /** What `budget()` reports; `null` means the environment has no budget. */
42
+ budget?: {
43
+ status: "ok" | "warn" | "reached";
44
+ monthlyUsd: number;
45
+ spentUsd: number;
46
+ mode?: "enforce" | "shadow";
47
+ } | null;
48
+ env?: WorkbenchEnv;
49
+ /** What `images.generate` and `images.search` get back. */
50
+ images?: {
51
+ /** Bytes and model for every image; by default a 1x1 PNG and the alias's target from the manifest. */
52
+ generate?: {
53
+ bytes?: Uint8Array;
54
+ mimeType?: string;
55
+ model?: string;
56
+ costUsd?: number;
57
+ error?: FakeError;
58
+ };
59
+ /** Results for every search; by default one result per `num` (at most 3). */
60
+ search?: {
61
+ results?: Array<{
62
+ title?: string;
63
+ image_url: string;
64
+ width?: number;
65
+ height?: number;
66
+ }>;
67
+ error?: FakeError;
68
+ };
69
+ };
70
+ }
71
+ export interface FakeError {
72
+ status: number;
73
+ code: string;
74
+ message?: string;
75
+ }
76
+ export interface FakeAi extends Ai {
77
+ /** Every request the fake Gateway answered, in order. */
78
+ readonly calls: FakeCall[];
79
+ }
80
+ export declare function createFakeAi(script?: FakeScript): FakeAi;
81
+ /** The fake Gateway on its own, as a `fetch`, for tests that build their own client. */
82
+ export declare function fakeGateway(script?: FakeScript): {
83
+ fetch: typeof fetch;
84
+ calls: FakeCall[];
85
+ };
package/dist/fake.js ADDED
@@ -0,0 +1,215 @@
1
+ // createFakeAi: the real client, wired to a scripted stand-in for the Gateway.
2
+ // Tests and fresh clones run with no key and no network (audit F10), and the
3
+ // code under test goes through exactly the paths it takes in production:
4
+ // refusals still throw `refused`, truncations `truncated`, errors keep their
5
+ // Gateway codes. Every call is recorded.
6
+ import { capabilities } from "@mccaskie-labs/workbench-config";
7
+ import { createAi } from "./ai.js";
8
+ const FAKE_KEY = {
9
+ development: "wbk_dev_fakefakefake_" + "x".repeat(43),
10
+ staging: "wbk_stg_fakefakefake_" + "x".repeat(43),
11
+ production: "wbk_prd_fakefakefake_" + "x".repeat(43),
12
+ };
13
+ const ALIASES = new Map(capabilities.ai.allowed.aliases.map((a) => [a.alias, a.default_model]));
14
+ /** Image aliases from the manifest (capabilities/images.json), with their providers. */
15
+ const IMAGE_ALIASES = new Map(capabilities.images.allowed.aliases.map((a) => [a.alias, { provider: a.provider, model: a.default_model }]));
16
+ // A 1x1 transparent PNG.
17
+ const TINY_PNG = Uint8Array.from(atob("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGNgYGD4DwABBAEAwS2OUAAAAABJRU5ErkJggg=="), (c) => c.charCodeAt(0));
18
+ function v1Error(status, requestId, code, message) {
19
+ return json(status, requestId, { error: { code, message, request_id: requestId, retryable: status === 429 || status >= 502 } });
20
+ }
21
+ function fakeImage(script, body, requestId) {
22
+ const g = script.images?.generate ?? {};
23
+ if (g.error)
24
+ return v1Error(g.error.status, requestId, g.error.code, g.error.message ?? g.error.code);
25
+ const alias = String(body?.["model"] ?? "image-default");
26
+ const target = IMAGE_ALIASES.get(alias);
27
+ if (!target)
28
+ return v1Error(400, requestId, "model_not_allowed", `${alias} isn't an image alias`);
29
+ const refs = body?.["references"];
30
+ if (target.provider === "openai" && Array.isArray(refs) && refs.length > 0) {
31
+ return v1Error(400, requestId, "invalid_request", `${alias} is an OpenAI model, which takes no reference images`);
32
+ }
33
+ const bytes = g.bytes ?? TINY_PNG;
34
+ let bin = "";
35
+ for (const b of bytes)
36
+ bin += String.fromCharCode(b);
37
+ return json(200, requestId, {
38
+ mime_type: g.mimeType ?? "image/png",
39
+ data_base64: btoa(bin),
40
+ model: g.model ?? target.model,
41
+ cost_usd: g.costUsd ?? 0.0001,
42
+ request_id: requestId,
43
+ });
44
+ }
45
+ function fakeSearch(script, body, requestId) {
46
+ const s = script.images?.search ?? {};
47
+ if (s.error)
48
+ return v1Error(s.error.status, requestId, s.error.code, s.error.message ?? s.error.code);
49
+ const num = typeof body?.["num"] === "number" ? body["num"] : 10;
50
+ const results = s.results ?? Array.from({ length: Math.min(num, 3) }, (_, i) => ({ title: `Result ${i + 1}`, image_url: `https://images.fake/${i + 1}.jpg`, width: 1200, height: 800 }));
51
+ return json(200, requestId, {
52
+ results: results.map((r) => ({
53
+ title: r.title ?? "",
54
+ image_url: r.image_url,
55
+ thumbnail_url: r.image_url,
56
+ width: r.width ?? 0,
57
+ height: r.height ?? 0,
58
+ source: "fake",
59
+ page_url: "https://images.fake/",
60
+ })),
61
+ credits: num > 10 ? 2 : 1,
62
+ cost_usd: num > 10 ? 0.002 : 0.001,
63
+ request_id: requestId,
64
+ });
65
+ }
66
+ export function createFakeAi(script = {}) {
67
+ const env = script.env ?? "development";
68
+ const { fetch, calls } = fakeGateway(script);
69
+ const ai = createAi({ key: FAKE_KEY[env], env, url: "https://gateway.fake", fetch });
70
+ return Object.assign(ai, { calls });
71
+ }
72
+ /** The fake Gateway on its own, as a `fetch`, for tests that build their own client. */
73
+ export function fakeGateway(script = {}) {
74
+ const calls = [];
75
+ let n = 0;
76
+ const fetchImpl = (async (input, init) => {
77
+ const req = new Request(input, init);
78
+ const url = new URL(req.url);
79
+ const text = req.method === "GET" ? "" : await req.text();
80
+ let body = null;
81
+ try {
82
+ body = text ? JSON.parse(text) : null;
83
+ }
84
+ catch {
85
+ body = null;
86
+ }
87
+ const call = {
88
+ path: url.pathname,
89
+ method: req.method,
90
+ job: req.headers.get("x-workbench-job"),
91
+ promptVersion: req.headers.get("x-workbench-prompt-version"),
92
+ subject: req.headers.get("x-workbench-subject"),
93
+ body,
94
+ stream: body?.["stream"] === true,
95
+ };
96
+ calls.push(call);
97
+ const requestId = `req_fake_${String(calls.length).padStart(4, "0")}`;
98
+ if (url.pathname === "/v1/usage") {
99
+ const b = script.budget === undefined ? { status: "ok", monthlyUsd: 1, spentUsd: 0 } : script.budget;
100
+ return json(200, requestId, {
101
+ project: "fake",
102
+ environment: env(script),
103
+ month: new Date().toISOString().slice(0, 7),
104
+ spent_usd: b?.spentUsd ?? 0,
105
+ budget: b
106
+ ? { status: b.status, mode: b.mode ?? "enforce", monthly_usd: b.monthlyUsd, left_usd: Math.max(0, b.monthlyUsd - b.spentUsd) }
107
+ : null,
108
+ by: [],
109
+ request_id: requestId,
110
+ });
111
+ }
112
+ if (url.pathname === "/v1/images/generate")
113
+ return fakeImage(script, body, requestId);
114
+ if (url.pathname === "/v1/images/search")
115
+ return fakeSearch(script, body, requestId);
116
+ if (url.pathname === "/anthropic/v1/messages/count_tokens") {
117
+ return json(200, requestId, { input_tokens: script.inputTokens ?? 12 });
118
+ }
119
+ if (url.pathname !== "/anthropic/v1/messages") {
120
+ return anthropicError(404, requestId, "not_found", "No such endpoint on the fake Gateway");
121
+ }
122
+ const replies = script.replies;
123
+ const index = n++;
124
+ let reply;
125
+ if (typeof replies === "function")
126
+ reply = replies(call, index);
127
+ else {
128
+ const list = replies && replies.length > 0 ? replies : [{ text: "ok" }];
129
+ const r = list[Math.min(index, list.length - 1)];
130
+ reply = typeof r === "function" ? r(call) : r;
131
+ }
132
+ if (reply.error) {
133
+ const e = reply.error;
134
+ return anthropicError(e.status, requestId, e.code, e.message ?? e.code, {
135
+ ...(e.month !== undefined ? { month: e.month } : {}),
136
+ ...(e.spentUsd !== undefined ? { spent_usd: e.spentUsd } : {}),
137
+ ...(e.capUsd !== undefined ? { cap_usd: e.capUsd } : {}),
138
+ });
139
+ }
140
+ const asked = String(body?.["model"] ?? "");
141
+ const model = reply.model ?? ALIASES.get(asked) ?? asked;
142
+ const stopReason = reply.stopReason ?? "end_turn";
143
+ const content = reply.json !== undefined ? JSON.stringify(reply.json) : (reply.text ?? "");
144
+ const message = {
145
+ id: `msg_fake_${index}`,
146
+ type: "message",
147
+ role: "assistant",
148
+ model,
149
+ content: stopReason === "refusal" && !content ? [] : [{ type: "text", text: content, citations: null }],
150
+ stop_reason: stopReason,
151
+ stop_sequence: null,
152
+ stop_details: stopReason === "refusal" ? { type: "refusal", category: reply.refusalCategory ?? null, explanation: null } : null,
153
+ usage: {
154
+ input_tokens: reply.inputTokens ?? 10,
155
+ output_tokens: reply.outputTokens ?? 5,
156
+ cache_read_input_tokens: 0,
157
+ cache_creation_input_tokens: 0,
158
+ },
159
+ };
160
+ const meta = {
161
+ request_id: requestId,
162
+ model,
163
+ alias: ALIASES.has(asked) ? asked : null,
164
+ cost_usd: reply.costUsd ?? 0.0001,
165
+ refused: stopReason === "refusal",
166
+ attempts: 1,
167
+ };
168
+ if (call.stream)
169
+ return sse(requestId, message, meta);
170
+ return json(200, requestId, { ...message, workbench: meta });
171
+ });
172
+ return { fetch: fetchImpl, calls };
173
+ }
174
+ function env(script) {
175
+ return script.env ?? "development";
176
+ }
177
+ function json(status, requestId, body) {
178
+ return new Response(JSON.stringify(body), {
179
+ status,
180
+ headers: { "content-type": "application/json", "x-workbench-request-id": requestId },
181
+ });
182
+ }
183
+ function anthropicError(status, requestId, code, message, extra = {}) {
184
+ const type = status === 401 ? "authentication_error" : status === 403 ? "permission_error" : status === 429 ? "rate_limit_error" : status >= 500 ? "api_error" : "invalid_request_error";
185
+ return json(status, requestId, { type: "error", error: { type, message, workbench_code: code, request_id: requestId, ...extra } });
186
+ }
187
+ /** The Anthropic event sequence for one text block, then the Gateway's `workbench.usage`. */
188
+ function sse(requestId, message, meta) {
189
+ const events = [];
190
+ events.push(["message_start", { type: "message_start", message: { ...message, content: [], stop_reason: null, usage: { ...message.usage, output_tokens: 0 } } }]);
191
+ message.content.forEach((block, i) => {
192
+ events.push(["content_block_start", { type: "content_block_start", index: i, content_block: { type: "text", text: "", citations: null } }]);
193
+ // Two deltas per block, so a consumer sees more than one event.
194
+ const half = Math.ceil(block.text.length / 2);
195
+ for (const part of [block.text.slice(0, half), block.text.slice(half)]) {
196
+ events.push(["content_block_delta", { type: "content_block_delta", index: i, delta: { type: "text_delta", text: part } }]);
197
+ }
198
+ events.push(["content_block_stop", { type: "content_block_stop", index: i }]);
199
+ });
200
+ events.push([
201
+ "message_delta",
202
+ {
203
+ type: "message_delta",
204
+ delta: { stop_reason: message["stop_reason"], stop_sequence: null, stop_details: message["stop_details"] },
205
+ usage: { output_tokens: message.usage["output_tokens"] },
206
+ },
207
+ ]);
208
+ events.push(["message_stop", { type: "message_stop" }]);
209
+ events.push(["workbench.usage", meta]);
210
+ const text = events.map(([e, d]) => `event: ${e}\ndata: ${JSON.stringify(d)}\n\n`).join("");
211
+ return new Response(text, {
212
+ status: 200,
213
+ headers: { "content-type": "text/event-stream", "x-workbench-request-id": requestId },
214
+ });
215
+ }
@@ -0,0 +1,11 @@
1
+ import type { Provenance, StructuredResult, SystemBlock, TextResult } from "./types.ts";
2
+ /**
3
+ * A system prompt with its stable part cached and the volatile parts after the
4
+ * breakpoint, so they never invalidate the cache (lifted from Imprint's
5
+ * `cachedSystem`). Empty volatile parts are dropped.
6
+ */
7
+ export declare function cachedSystem(stable: string, ...volatile: string[]): SystemBlock[];
8
+ /** Text blocks with a cache breakpoint on the last one (Imprint's `cachedBlocks`). */
9
+ export declare function cachedBlocks(...texts: string[]): SystemBlock[];
10
+ /** The record to store with any output kept: resolved model, request id, prompt version, cost (audit F17). */
11
+ export declare function provenance(result: TextResult | StructuredResult<unknown>, promptVersion: string): Provenance;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * A system prompt with its stable part cached and the volatile parts after the
3
+ * breakpoint, so they never invalidate the cache (lifted from Imprint's
4
+ * `cachedSystem`). Empty volatile parts are dropped.
5
+ */
6
+ export function cachedSystem(stable, ...volatile) {
7
+ return [
8
+ { type: "text", text: stable, cache_control: { type: "ephemeral" } },
9
+ ...volatile.filter((v) => v !== "").map((text) => ({ type: "text", text })),
10
+ ];
11
+ }
12
+ /** Text blocks with a cache breakpoint on the last one (Imprint's `cachedBlocks`). */
13
+ export function cachedBlocks(...texts) {
14
+ const blocks = texts.filter((t) => t !== "").map((text) => ({ type: "text", text }));
15
+ const last = blocks[blocks.length - 1];
16
+ if (last)
17
+ last.cache_control = { type: "ephemeral" };
18
+ return blocks;
19
+ }
20
+ /** The record to store with any output kept: resolved model, request id, prompt version, cost (audit F17). */
21
+ export function provenance(result, promptVersion) {
22
+ return { model: result.model, requestId: result.requestId, promptVersion, costUsd: result.costUsd };
23
+ }
package/dist/http.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ import type { WorkbenchEnv } from "@mccaskie-labs/workbench-config";
2
+ export interface GatewayTarget {
3
+ url: string;
4
+ key: string;
5
+ env: WorkbenchEnv;
6
+ fetch: typeof fetch;
7
+ }
8
+ export interface GatewayCall {
9
+ method?: "GET" | "POST";
10
+ /** X-Workbench-Job; required by every metered endpoint. */
11
+ job?: string;
12
+ /** Sent as JSON. */
13
+ body?: unknown;
14
+ accept?: string;
15
+ headers?: Record<string, string>;
16
+ }
17
+ export interface GatewayResponse {
18
+ response: Response;
19
+ /** The Gateway's X-Workbench-Request-Id. */
20
+ requestId: string | undefined;
21
+ }
22
+ /** Calls `path` on the Gateway; resolves only on a 2xx, otherwise throws the coded error. */
23
+ export declare function gatewayFetch(target: GatewayTarget, path: string, call?: GatewayCall): Promise<GatewayResponse>;
package/dist/http.js ADDED
@@ -0,0 +1,33 @@
1
+ // Plain HTTPS calls to the Gateway's own /v1/* endpoints (usage, and the
2
+ // images endpoints from step 4.8). /anthropic/* goes through the official SDK
3
+ // instead. Errors become WorkbenchAiError with the Gateway's code.
4
+ import { REQUEST_ID_HEADER } from "./capture.js";
5
+ import { errorFromGateway, WorkbenchAiError } from "./errors.js";
6
+ /** Calls `path` on the Gateway; resolves only on a 2xx, otherwise throws the coded error. */
7
+ export async function gatewayFetch(target, path, call = {}) {
8
+ const headers = {
9
+ Authorization: `Bearer ${target.key}`,
10
+ "X-Workbench-Env": target.env,
11
+ Accept: call.accept ?? "application/json",
12
+ ...(call.job !== undefined ? { "X-Workbench-Job": call.job } : {}),
13
+ ...(call.body !== undefined ? { "Content-Type": "application/json" } : {}),
14
+ ...call.headers,
15
+ };
16
+ let response;
17
+ try {
18
+ response = await target.fetch(`${target.url}${path}`, {
19
+ method: call.method ?? (call.body !== undefined ? "POST" : "GET"),
20
+ headers,
21
+ ...(call.body !== undefined ? { body: JSON.stringify(call.body) } : {}),
22
+ });
23
+ }
24
+ catch {
25
+ throw new WorkbenchAiError("network", "Couldn't reach the Gateway");
26
+ }
27
+ const requestId = response.headers.get(REQUEST_ID_HEADER) ?? undefined;
28
+ if (!response.ok) {
29
+ const body = await response.json().catch(() => undefined);
30
+ throw errorFromGateway(response.status, body, requestId);
31
+ }
32
+ return { response, requestId };
33
+ }
@@ -0,0 +1,4 @@
1
+ import { type GatewayTarget } from "./http.ts";
2
+ import type { AiImages, AspectRatio } from "./types.ts";
3
+ export declare const ASPECT_RATIOS: readonly AspectRatio[];
4
+ export declare function createImages(target: GatewayTarget): AiImages;
package/dist/images.js ADDED
@@ -0,0 +1,93 @@
1
+ // ai.images: image generation and image search through the Gateway's
2
+ // /v1/images/* endpoints (docs/spec.md §2.3, §3). Plain HTTPS through
3
+ // gatewayFetch; errors keep the Gateway's codes as WorkbenchAiError.
4
+ import { WorkbenchAiError } from "./errors.js";
5
+ import { gatewayFetch } from "./http.js";
6
+ export const ASPECT_RATIOS = ["1:1", "3:2", "2:3", "4:3", "3:4", "16:9", "9:16"];
7
+ const JOB = /^[a-z0-9._-]{1,64}$/;
8
+ const SUBJECT = /^[a-z0-9_-]+:[^\s]{1,128}$/;
9
+ function headers(req) {
10
+ if (!JOB.test(req.job))
11
+ throw new WorkbenchAiError("invalid_request", "job must match [a-z0-9._-]{1,64}");
12
+ const h = {};
13
+ if (req.promptVersion !== undefined)
14
+ h["X-Workbench-Prompt-Version"] = req.promptVersion;
15
+ if (req.subject !== undefined) {
16
+ if (!SUBJECT.test(req.subject))
17
+ throw new WorkbenchAiError("invalid_request", "subject must be type:id");
18
+ h["X-Workbench-Subject"] = req.subject;
19
+ }
20
+ return h;
21
+ }
22
+ function toBase64(bytes) {
23
+ let s = "";
24
+ for (let i = 0; i < bytes.length; i += 0x8000)
25
+ s += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
26
+ return btoa(s);
27
+ }
28
+ function fromBase64(b64) {
29
+ const bin = atob(b64);
30
+ const out = new Uint8Array(bin.length);
31
+ for (let i = 0; i < bin.length; i++)
32
+ out[i] = bin.charCodeAt(i);
33
+ return out;
34
+ }
35
+ const num = (v) => (typeof v === "number" && Number.isFinite(v) ? v : 0);
36
+ const str = (v) => (typeof v === "string" ? v : "");
37
+ export function createImages(target) {
38
+ async function generate(req) {
39
+ if (!ASPECT_RATIOS.includes(req.aspectRatio)) {
40
+ throw new WorkbenchAiError("invalid_request", `aspectRatio must be one of ${ASPECT_RATIOS.join(" ")}`);
41
+ }
42
+ const { response, requestId } = await gatewayFetch(target, "/v1/images/generate", {
43
+ job: req.job,
44
+ headers: headers(req),
45
+ accept: "application/json",
46
+ body: {
47
+ prompt: req.prompt,
48
+ aspect_ratio: req.aspectRatio,
49
+ ...(req.model !== undefined ? { model: req.model } : {}),
50
+ ...(req.references?.length ? { references: req.references.map((r) => ({ mime_type: r.mimeType, data_base64: toBase64(r.bytes) })) } : {}),
51
+ },
52
+ });
53
+ const body = (await response.json().catch(() => null));
54
+ const data = str(body?.["data_base64"]);
55
+ if (!body || !data)
56
+ throw new WorkbenchAiError("upstream_error", "The Gateway returned no image", { ...(requestId ? { requestId } : {}) });
57
+ return {
58
+ bytes: fromBase64(data),
59
+ mimeType: str(body["mime_type"]) || "image/png",
60
+ model: str(body["model"]),
61
+ requestId: str(body["request_id"]) || requestId || "",
62
+ costUsd: num(body["cost_usd"]),
63
+ };
64
+ }
65
+ async function search(req) {
66
+ const { response, requestId } = await gatewayFetch(target, "/v1/images/search", {
67
+ job: req.job,
68
+ headers: headers(req),
69
+ body: {
70
+ q: req.q,
71
+ ...(req.num !== undefined ? { num: req.num } : {}),
72
+ ...(req.gl !== undefined ? { gl: req.gl } : {}),
73
+ ...(req.hl !== undefined ? { hl: req.hl } : {}),
74
+ },
75
+ });
76
+ const body = (await response.json().catch(() => null));
77
+ const results = (body?.results ?? []).map((r) => ({
78
+ title: str(r["title"]),
79
+ imageUrl: str(r["image_url"]),
80
+ thumbnailUrl: str(r["thumbnail_url"]),
81
+ width: num(r["width"]),
82
+ height: num(r["height"]),
83
+ source: str(r["source"]),
84
+ pageUrl: str(r["page_url"]),
85
+ }));
86
+ return Object.assign(results, {
87
+ requestId: str(body?.request_id) || requestId || "",
88
+ costUsd: num(body?.cost_usd),
89
+ credits: num(body?.credits),
90
+ });
91
+ }
92
+ return { generate, search };
93
+ }
@@ -0,0 +1,7 @@
1
+ export { createAi, missingRequired } from "./ai.ts";
2
+ export { createFakeAi, fakeGateway, type FakeScript, type FakeReply, type FakeCall, type FakeAi } from "./fake.ts";
3
+ export { cachedSystem, cachedBlocks, provenance } from "./helpers.ts";
4
+ export { WorkbenchAiError, type AiErrorCode, type AiErrorDetails } from "./errors.ts";
5
+ export { REQUEST_ID_HEADER, type GatewayMeta } from "./capture.ts";
6
+ export { ASPECT_RATIOS } from "./images.ts";
7
+ export type { Ai, AiImages, AiOptions, AspectRatio, GeneratedImage, ImageGenerateRequest, ImageSearchRequest, ImageSearchResult, ImageSearchResults, AiStream, BudgetStatus, ContentBlockParam, CountTokensRequest, Effort, MessageStreamEvent, Provenance, StructuredRequest, StructuredResult, SystemBlock, TextRequest, TextResult, Usage, ZodLike, } from "./types.ts";
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { createAi, missingRequired } from "./ai.js";
2
+ export { createFakeAi, fakeGateway } from "./fake.js";
3
+ export { cachedSystem, cachedBlocks, provenance } from "./helpers.js";
4
+ export { WorkbenchAiError } from "./errors.js";
5
+ export { REQUEST_ID_HEADER } from "./capture.js";
6
+ export { ASPECT_RATIOS } from "./images.js";
@@ -0,0 +1,166 @@
1
+ import type Anthropic from "@anthropic-ai/sdk";
2
+ import type { WorkbenchEnv } from "@mccaskie-labs/workbench-config";
3
+ export type SystemBlock = Anthropic.TextBlockParam;
4
+ export type ContentBlockParam = Anthropic.ContentBlockParam;
5
+ export type MessageStreamEvent = Anthropic.MessageStreamEvent;
6
+ export type Effort = "low" | "medium" | "high" | "xhigh" | "max";
7
+ export interface AiOptions {
8
+ /** The project's Gateway key for this environment (`WORKBENCH_GATEWAY_KEY`). */
9
+ key: string;
10
+ /** From `workbenchEnv(env)`. Must match the key's environment. */
11
+ env: WorkbenchEnv;
12
+ /** Gateway base URL; defaults to `DEFAULT_GATEWAY_URL`. Leave unset outside tests. */
13
+ url?: string;
14
+ fetch?: typeof fetch;
15
+ }
16
+ export interface TextRequest {
17
+ /** A Workbench alias (`claude-default`, `claude-best`, `claude-fast`) or an allowlisted model id. */
18
+ model: string;
19
+ /** What the call is for, `[a-z0-9._-]{1,64}`, e.g. `article.write`. Recorded with usage. */
20
+ job: string;
21
+ system?: string | SystemBlock[];
22
+ prompt: string | ContentBlockParam[];
23
+ /** Defaults to 16000 (64000 for `stream`). */
24
+ maxTokens?: number;
25
+ effort?: Effort;
26
+ promptVersion?: string;
27
+ /** `type:id` of the thing the call is about. */
28
+ subject?: string;
29
+ }
30
+ export interface StructuredRequest<T> extends TextRequest {
31
+ /** A zod schema. Its JSON Schema is sent as `output_config.format`; the reply is validated against it. */
32
+ schema: ZodLike<T>;
33
+ /** Required fields the model may leave empty without a re-ask. */
34
+ mayLack?: string[];
35
+ }
36
+ /** The part of a zod 4 schema the package uses. */
37
+ export interface ZodLike<T> {
38
+ safeParse(value: unknown): {
39
+ success: true;
40
+ data: T;
41
+ } | {
42
+ success: false;
43
+ error: {
44
+ message: string;
45
+ };
46
+ };
47
+ }
48
+ export interface Usage {
49
+ inputTokens: number;
50
+ outputTokens: number;
51
+ cacheReadTokens: number;
52
+ cacheWriteTokens: number;
53
+ }
54
+ export interface TextResult {
55
+ text: string;
56
+ /** The resolved model id (never the alias). Store it with the output (audit F17). */
57
+ model: string;
58
+ /** The Gateway's `X-Workbench-Request-Id`; matches a `usage` row. */
59
+ requestId: string;
60
+ usage: Usage;
61
+ costUsd: number;
62
+ }
63
+ export interface StructuredResult<T> {
64
+ data: T;
65
+ model: string;
66
+ requestId: string;
67
+ usage: Usage;
68
+ costUsd: number;
69
+ }
70
+ export interface CountTokensRequest {
71
+ model: string;
72
+ job: string;
73
+ system?: string | SystemBlock[];
74
+ prompt: string | ContentBlockParam[];
75
+ }
76
+ export interface BudgetStatus {
77
+ /** `ok` below the warning level, `warn` at or above it, `reached` at or above the cap. */
78
+ status: "ok" | "warn" | "reached";
79
+ /** Dollars left this month (never negative). */
80
+ leftUsd: number;
81
+ monthlyUsd: number;
82
+ spentUsd: number;
83
+ mode: "enforce" | "shadow";
84
+ /** `YYYY-MM`, UTC. */
85
+ month: string;
86
+ }
87
+ export interface AiStream extends AsyncIterable<MessageStreamEvent> {
88
+ /** Resolves once the stream ends; throws `refused` or `truncated` like `text`. */
89
+ finalMessage(): Promise<TextResult>;
90
+ }
91
+ export interface Ai {
92
+ /** The official SDK client pointed at the Gateway, for anything the helpers don't cover. */
93
+ readonly anthropic: Anthropic;
94
+ text(req: TextRequest): Promise<TextResult>;
95
+ structured<T>(req: StructuredRequest<T>): Promise<StructuredResult<T>>;
96
+ stream(req: TextRequest): AiStream;
97
+ countTokens(req: CountTokensRequest): Promise<{
98
+ inputTokens: number;
99
+ }>;
100
+ /** Budget status for this key's project and environment, cached 60 s. Throws if the environment has no budget. */
101
+ budget(): Promise<BudgetStatus>;
102
+ /** Image generation and image search (step 4.8). The key needs the images or search scope. */
103
+ readonly images: AiImages;
104
+ }
105
+ export type AspectRatio = "1:1" | "3:2" | "2:3" | "4:3" | "3:4" | "16:9" | "9:16";
106
+ export interface ImageGenerateRequest {
107
+ prompt: string;
108
+ aspectRatio: AspectRatio;
109
+ job: string;
110
+ /** An image alias: `image-default` (the default), `image-pro` or `image-openai`. */
111
+ model?: string;
112
+ /** Google aliases only; an OpenAI alias refuses them with `invalid_request`. */
113
+ references?: {
114
+ bytes: Uint8Array;
115
+ mimeType: string;
116
+ }[];
117
+ promptVersion?: string;
118
+ subject?: string;
119
+ }
120
+ export interface GeneratedImage {
121
+ bytes: Uint8Array;
122
+ /** `image/png` or `image/jpeg`. */
123
+ mimeType: string;
124
+ /** The resolved model id. Store it with the image (audit F17). */
125
+ model: string;
126
+ requestId: string;
127
+ costUsd: number;
128
+ }
129
+ export interface ImageSearchRequest {
130
+ q: string;
131
+ job: string;
132
+ /** 1 to 100; the Gateway's default is 10. More than 10 costs 2 credits. */
133
+ num?: number;
134
+ /** Country, e.g. `ph`. */
135
+ gl?: string;
136
+ /** Language, e.g. `en`. */
137
+ hl?: string;
138
+ subject?: string;
139
+ }
140
+ export interface ImageSearchResult {
141
+ title: string;
142
+ imageUrl: string;
143
+ thumbnailUrl: string;
144
+ width: number;
145
+ height: number;
146
+ /** The site's name. */
147
+ source: string;
148
+ /** The page the image is on; empty if Serper gave none. */
149
+ pageUrl: string;
150
+ }
151
+ /** The results, plus what the search cost. */
152
+ export interface ImageSearchResults extends Array<ImageSearchResult> {
153
+ requestId: string;
154
+ costUsd: number;
155
+ credits: number;
156
+ }
157
+ export interface AiImages {
158
+ generate(req: ImageGenerateRequest): Promise<GeneratedImage>;
159
+ search(req: ImageSearchRequest): Promise<ImageSearchResults>;
160
+ }
161
+ export interface Provenance {
162
+ model: string;
163
+ requestId: string;
164
+ promptVersion: string;
165
+ costUsd: number;
166
+ }