@seanhogg/builderforce-sdk 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -297,6 +297,14 @@ try {
297
297
  } catch (error) {
298
298
  if (error instanceof BuilderforceApiError) {
299
299
  console.error(error.status, error.code, error.requestId, error.message);
300
+ // `vendor` + `model` identify the upstream the gateway dispatched against
301
+ // — set on every error where an upstream was selected (single-attempt
302
+ // 429s, timeouts, `model_unavailable`, …). Unset only for pre-dispatch
303
+ // errors (auth failures, validation, tenant-cap 429s) — distinguish by
304
+ // checking presence of `error.vendor`, not by parsing model-id prefixes.
305
+ if (error.vendor) {
306
+ console.error(`upstream: ${error.vendor}/${error.model}`);
307
+ }
300
308
  }
301
309
  }
302
310
  ```
@@ -350,7 +358,7 @@ const usage = await client.usage.get({ days: 30 });
350
358
 
351
359
  ## Routing — `model` is a hint, gateway has final say
352
360
 
353
- The gateway owns model selection. When you pass a `model`, the gateway treats it as a **hint** — it puts that id at the head of its candidate chain so it's tried first, but it retains the right to substitute on cooldown, vendor outage, or plan-tier mismatch. **Always read `_builderforce.resolvedModel` if you need to know what actually ran.**
361
+ The gateway owns model selection. When you pass a `model`, the gateway treats it as a **hint** — it puts that id at the head of its candidate chain so it's tried first, but it retains the right to substitute on cooldown, vendor outage, or plan-tier mismatch. **Always read `_builderforce.resolvedModel` if you need to know what actually ran.** Pair it with `_builderforce.resolvedVendor` (the upstream that owns the resolved model — `'openrouter'`, `'cerebras'`, `'nvidia'`, `'ollama'`, `'googleai'`, …) for per-vendor cost / latency aggregation without parsing the model-id prefix.
354
362
 
355
363
  ```ts
356
364
  const res = await client.chat.completions.create({
package/dist/index.cjs CHANGED
@@ -23,7 +23,8 @@ __export(index_exports, {
23
23
  BuilderforceApiError: () => BuilderforceApiError,
24
24
  BuilderforceClient: () => BuilderforceClient,
25
25
  ChatCompletionStream: () => ChatCompletionStream,
26
- EmbeddingsApi: () => EmbeddingsApi
26
+ EmbeddingsApi: () => EmbeddingsApi,
27
+ ImagesApi: () => ImagesApi
27
28
  });
28
29
  module.exports = __toCommonJS(index_exports);
29
30
 
@@ -131,6 +132,31 @@ var EmbeddingsApi = class {
131
132
  }
132
133
  };
133
134
 
135
+ // src/application/ImagesApi.ts
136
+ function splitTransportOptions3(params) {
137
+ const { timeoutMs, signal, idempotencyKey, ...rest } = params;
138
+ const headers = {};
139
+ if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;
140
+ return {
141
+ body: rest,
142
+ request: {
143
+ timeoutMs,
144
+ signal,
145
+ ...Object.keys(headers).length > 0 ? { headers } : {}
146
+ }
147
+ };
148
+ }
149
+ var ImagesApi = class {
150
+ http;
151
+ constructor(http) {
152
+ this.http = http;
153
+ }
154
+ generate(params) {
155
+ const { body, request } = splitTransportOptions3(params);
156
+ return this.http.postJson("/llm/v1/images/generations", body, request);
157
+ }
158
+ };
159
+
134
160
  // src/application/ModelsApi.ts
135
161
  var ModelsApi = class {
136
162
  http;
@@ -169,6 +195,33 @@ var BuilderforceApiError = class extends Error {
169
195
  terminal;
170
196
  /** Seconds the consumer should wait before retrying — server-supplied. */
171
197
  retryAfter;
198
+ /**
199
+ * Cascade attempts that failed before this error was returned — populated
200
+ * when the gateway returns `429 cascade_exhausted` with a `details.failovers`
201
+ * array. Each entry includes the vendor that owns the model so callers can
202
+ * detect single-vendor saturation (e.g. all attempts on `openrouter`).
203
+ */
204
+ failovers;
205
+ /**
206
+ * Upstream vendor the gateway dispatched against (`'openrouter' | 'cerebras'
207
+ * | 'nvidia' | 'ollama' | 'googleai' | …`). Set on every error where the
208
+ * gateway selected an upstream — including single-attempt failures that
209
+ * never ran a cascade (timeouts, single-vendor 429s, `model_unavailable`).
210
+ *
211
+ * Unset only for pre-dispatch errors where no vendor was ever selected:
212
+ * `401`/`403` auth failures, `400` validation failures, `409` idempotent
213
+ * replay, and tenant-cap 429s (`plan_token_limit_exceeded`,
214
+ * `claw_token_limit_exceeded`) — those caps are per-tenant, not per-model.
215
+ *
216
+ * Sourced from the gateway's catalog lookup so consumers never have to
217
+ * parse the model id to recover vendor identity.
218
+ */
219
+ vendor;
220
+ /**
221
+ * Model id the gateway dispatched against — set whenever `vendor` is set.
222
+ * Pair with `vendor` for per-attempt observability without prefix parsing.
223
+ */
224
+ model;
172
225
  constructor(message, status, code, details, requestId, extras) {
173
226
  super(message);
174
227
  this.name = "BuilderforceApiError";
@@ -178,6 +231,23 @@ var BuilderforceApiError = class extends Error {
178
231
  this.requestId = requestId;
179
232
  this.terminal = extras?.terminal;
180
233
  this.retryAfter = extras?.retryAfter;
234
+ this.vendor = extras?.vendor;
235
+ this.model = extras?.model;
236
+ if (details && typeof details === "object") {
237
+ const f = details.failovers;
238
+ if (Array.isArray(f)) {
239
+ const cleaned = [];
240
+ for (const entry of f) {
241
+ if (entry && typeof entry === "object") {
242
+ const e = entry;
243
+ if (typeof e.model === "string" && typeof e.vendor === "string" && typeof e.code === "number") {
244
+ cleaned.push({ model: e.model, vendor: e.vendor, code: e.code });
245
+ }
246
+ }
247
+ }
248
+ if (cleaned.length > 0) this.failovers = cleaned;
249
+ }
250
+ }
181
251
  }
182
252
  };
183
253
  var HttpClient = class {
@@ -190,7 +260,7 @@ var HttpClient = class {
190
260
  this.baseUrl = options.baseUrl.replace(/\/$/, "");
191
261
  const fetchImpl = options.fetchFn ?? fetch;
192
262
  this.fetchFn = fetchImpl.bind(globalThis);
193
- this.defaultTimeoutMs = options.timeoutMs ?? 6e4;
263
+ this.defaultTimeoutMs = options.timeoutMs ?? 18e4;
194
264
  }
195
265
  async getJson(path, options) {
196
266
  const res = await this.fetchWithTimeout(`${this.baseUrl}${path}`, {
@@ -261,18 +331,25 @@ var HttpClient = class {
261
331
  const headerRetryAfter = parsePositiveInt(res.headers.get("retry-after"));
262
332
  try {
263
333
  const payload = await res.json();
264
- const isNested = payload !== null && typeof payload === "object" && payload.success === false && typeof payload.error === "object" && payload.error !== null;
265
- const inner = isNested ? payload.error : payload;
334
+ const errorObj = payload !== null && typeof payload === "object" && typeof payload.error === "object" && payload.error !== null ? payload.error : null;
335
+ const isWrapped = payload !== null && typeof payload === "object" && payload.success === false && errorObj !== null;
336
+ const inner = isWrapped || errorObj !== null ? errorObj : payload;
266
337
  const message = typeof inner?.message === "string" && inner.message || typeof inner?.error === "string" && inner.error || fallback;
338
+ const code = typeof inner?.code === "number" ? String(inner.code) : inner?.code;
339
+ const detailsObj = inner?.details && typeof inner.details === "object" ? inner.details : null;
340
+ const vendor = typeof inner?.vendor === "string" ? inner.vendor : typeof detailsObj?.vendor === "string" ? detailsObj.vendor : void 0;
341
+ const model = typeof inner?.model === "string" ? inner.model : typeof detailsObj?.model === "string" ? detailsObj.model : typeof detailsObj?.requestedModel === "string" ? detailsObj.requestedModel : void 0;
267
342
  return new BuilderforceApiError(
268
343
  message,
269
344
  res.status,
270
- inner?.code,
345
+ code,
271
346
  inner?.details,
272
347
  requestId,
273
348
  {
274
349
  terminal: inner?.terminal,
275
- retryAfter: headerRetryAfter ?? inner?.retryAfter
350
+ retryAfter: headerRetryAfter ?? inner?.retryAfter,
351
+ vendor,
352
+ model
276
353
  }
277
354
  );
278
355
  } catch {
@@ -315,6 +392,7 @@ function combineSignals(...signals) {
315
392
  var BuilderforceClient = class {
316
393
  chat;
317
394
  embeddings;
395
+ images;
318
396
  models;
319
397
  usage;
320
398
  constructor(options) {
@@ -336,6 +414,7 @@ var BuilderforceClient = class {
336
414
  completions: new ChatCompletionsApi(http)
337
415
  };
338
416
  this.embeddings = new EmbeddingsApi(http);
417
+ this.images = new ImagesApi(http);
339
418
  this.models = new ModelsApi(http);
340
419
  this.usage = new UsageApi(http);
341
420
  }
@@ -345,6 +424,7 @@ var BuilderforceClient = class {
345
424
  BuilderforceApiError,
346
425
  BuilderforceClient,
347
426
  ChatCompletionStream,
348
- EmbeddingsApi
427
+ EmbeddingsApi,
428
+ ImagesApi
349
429
  });
350
430
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/infrastructure/sse.ts","../src/application/ChatCompletionsApi.ts","../src/application/EmbeddingsApi.ts","../src/application/ModelsApi.ts","../src/application/UsageApi.ts","../src/infrastructure/httpClient.ts","../src/BuilderforceClient.ts"],"sourcesContent":["export { BuilderforceClient, type BuilderforceClientOptions } from './BuilderforceClient';\n\nexport type {\n // Roles & content\n ChatRole,\n ChatMessage,\n ContentPart,\n TextContentPart,\n ImageUrlContentPart,\n // Tool calling\n ToolSpec,\n ToolCall,\n ToolCallFunction,\n ToolCallDelta,\n ToolChoice,\n FunctionDefinition,\n // Structured output\n ResponseFormat,\n JsonSchemaSpec,\n // Per-call options\n PerCallOptions,\n // Chat completions\n ChatCompletionCreateParams,\n ChatCompletionChunk,\n ChatCompletionResponse,\n // Models / usage\n ModelsListResponse,\n UsageByModel,\n UsageByDay,\n UsageByUser,\n UsageResponse,\n UsageGetParams,\n // Embeddings\n EmbeddingsCreateParams,\n EmbeddingsResponse,\n EmbeddingObject,\n} from './domain/types';\n\nexport { ChatCompletionStream } from './application/ChatCompletionsApi';\nexport { EmbeddingsApi } from './application/EmbeddingsApi';\nexport { BuilderforceApiError } from './infrastructure/httpClient';\n","export async function* parseSseJson<T>(\n stream: ReadableStream<Uint8Array>,\n): AsyncGenerator<T, void, unknown> {\n const reader = stream.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() ?? '';\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed.startsWith('data: ')) continue;\n\n const data = trimmed.slice(6).trim();\n if (data === '[DONE]') return;\n\n try {\n yield JSON.parse(data) as T;\n } catch {\n // Skip malformed chunks instead of breaking the stream.\n }\n }\n }\n}\n","import type { ChatCompletionChunk, ChatCompletionCreateParams, ChatCompletionResponse } from '../domain/types';\nimport { HttpClient, type RequestOptions } from '../infrastructure/httpClient';\nimport { parseSseJson } from '../infrastructure/sse';\n\nexport class ChatCompletionStream implements AsyncIterable<ChatCompletionChunk> {\n private readonly stream: ReadableStream<Uint8Array>;\n\n constructor(stream: ReadableStream<Uint8Array>) {\n this.stream = stream;\n }\n\n [Symbol.asyncIterator](): AsyncIterator<ChatCompletionChunk, void, unknown> {\n return parseSseJson<ChatCompletionChunk>(this.stream);\n }\n\n async toText(): Promise<string> {\n let full = '';\n for await (const chunk of this) {\n const delta = chunk.choices?.[0]?.delta?.content;\n if (typeof delta === 'string') {\n full += delta;\n }\n }\n return full;\n }\n}\n\n/**\n * Pull SDK-level transport options (timeout, signal, idempotency key) out of\n * the params object so they don't get JSON-serialized into the request body.\n * Returns the request options AND the cleaned-up body.\n */\nfunction splitTransportOptions(params: ChatCompletionCreateParams): {\n body: Record<string, unknown>;\n request: RequestOptions;\n} {\n const { timeoutMs, signal, idempotencyKey, ...rest } = params;\n const headers: Record<string, string> = {};\n if (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey;\n return {\n body: rest as unknown as Record<string, unknown>,\n request: {\n timeoutMs,\n signal,\n ...(Object.keys(headers).length > 0 ? { headers } : {}),\n },\n };\n}\n\nexport class ChatCompletionsApi {\n private readonly http: HttpClient;\n\n constructor(http: HttpClient) {\n this.http = http;\n }\n\n async create(params: ChatCompletionCreateParams & { stream: true }): Promise<ChatCompletionStream>;\n async create(params: ChatCompletionCreateParams & { stream?: false | undefined }): Promise<ChatCompletionResponse>;\n async create(\n params: ChatCompletionCreateParams,\n ): Promise<ChatCompletionResponse | ChatCompletionStream> {\n const { body, request } = splitTransportOptions(params);\n\n if (params.stream) {\n const response = await this.http.postRaw('/llm/v1/chat/completions', body, request);\n if (!response.body) {\n throw new Error('Streaming response body is missing');\n }\n return new ChatCompletionStream(response.body);\n }\n\n return this.http.postJson<ChatCompletionResponse>('/llm/v1/chat/completions', body, request);\n }\n}\n","import type { EmbeddingsCreateParams, EmbeddingsResponse } from '../domain/types';\nimport { HttpClient, type RequestOptions } from '../infrastructure/httpClient';\n\n/**\n * Pull SDK-level transport options out of the params so they don't ride\n * along inside the JSON body. Same shape as ChatCompletionsApi (DRY pattern).\n */\nfunction splitTransportOptions(params: EmbeddingsCreateParams): {\n body: Record<string, unknown>;\n request: RequestOptions;\n} {\n const { timeoutMs, signal, idempotencyKey, ...rest } = params;\n const headers: Record<string, string> = {};\n if (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey;\n return {\n body: rest as unknown as Record<string, unknown>,\n request: {\n timeoutMs,\n signal,\n ...(Object.keys(headers).length > 0 ? { headers } : {}),\n },\n };\n}\n\nexport class EmbeddingsApi {\n private readonly http: HttpClient;\n\n constructor(http: HttpClient) {\n this.http = http;\n }\n\n /**\n * Create one or more text embeddings. Wired to OpenRouter (default model\n * `nvidia/llama-nemotron-embed-vl-1b-v2:free`). Override via `model`.\n */\n create(params: EmbeddingsCreateParams): Promise<EmbeddingsResponse> {\n const { body, request } = splitTransportOptions(params);\n return this.http.postJson<EmbeddingsResponse>('/llm/v1/embeddings', body, request);\n }\n}\n","import type { ModelsListResponse } from '../domain/types';\nimport { HttpClient } from '../infrastructure/httpClient';\n\nexport class ModelsApi {\n private readonly http: HttpClient;\n\n constructor(http: HttpClient) {\n this.http = http;\n }\n\n list(): Promise<ModelsListResponse> {\n return this.http.getJson<ModelsListResponse>('/llm/v1/models');\n }\n}\n","import type { UsageGetParams, UsageResponse } from '../domain/types';\nimport { HttpClient } from '../infrastructure/httpClient';\n\nexport class UsageApi {\n private readonly http: HttpClient;\n\n constructor(http: HttpClient) {\n this.http = http;\n }\n\n get(params: UsageGetParams = {}): Promise<UsageResponse> {\n const query = typeof params.days === 'number' ? `?days=${encodeURIComponent(String(params.days))}` : '';\n return this.http.getJson<UsageResponse>(`/llm/v1/usage${query}`);\n }\n}\n","export class BuilderforceApiError extends Error {\n public readonly status: number;\n public readonly code?: string;\n public readonly details?: unknown;\n public readonly requestId?: string;\n /**\n * `true` when the gateway has signalled this error will not resolve by\n * retrying on a different model — e.g. plan or per-claw daily token cap\n * exhausted (those caps are per-tenant, not per-model). Consumer-side\n * fallback chains should short-circuit when this is set.\n */\n public readonly terminal?: boolean;\n /** Seconds the consumer should wait before retrying — server-supplied. */\n public readonly retryAfter?: number;\n\n constructor(\n message: string,\n status: number,\n code?: string,\n details?: unknown,\n requestId?: string,\n extras?: { terminal?: boolean; retryAfter?: number },\n ) {\n super(message);\n this.name = 'BuilderforceApiError';\n this.status = status;\n this.code = code;\n this.details = details;\n this.requestId = requestId;\n this.terminal = extras?.terminal;\n this.retryAfter = extras?.retryAfter;\n }\n}\n\nexport interface HttpClientOptions {\n apiKey: string;\n baseUrl: string;\n fetchFn?: typeof fetch;\n /** Default per-request timeout in ms. Overridable per call. */\n timeoutMs?: number;\n}\n\n/** Per-request overrides — passed by the API layer, not by SDK consumers directly. */\nexport interface RequestOptions {\n /** Override the client default timeout for just this request. */\n timeoutMs?: number;\n /** Caller-provided AbortSignal. Linked together with the SDK's internal timeout\n * signal — whichever fires first aborts the request. */\n signal?: AbortSignal;\n /** Extra headers to merge in (e.g. `Idempotency-Key`). */\n headers?: Record<string, string>;\n}\n\nexport class HttpClient {\n private readonly apiKey: string;\n private readonly baseUrl: string;\n private readonly fetchFn: typeof fetch;\n private readonly defaultTimeoutMs: number;\n\n constructor(options: HttpClientOptions) {\n this.apiKey = options.apiKey;\n this.baseUrl = options.baseUrl.replace(/\\/$/, '');\n // Bind to `globalThis` so calling via `this.fetchFn(...)` doesn't trip\n // Cloudflare Workers' \"Illegal invocation\" check — the platform's fetch\n // requires the global receiver, not an instance method `this`. Affects\n // any environment that ships a strict-receiver fetch (Workers, Bun, etc.)\n // and is harmless on Node + browsers.\n const fetchImpl = options.fetchFn ?? fetch;\n this.fetchFn = fetchImpl.bind(globalThis);\n this.defaultTimeoutMs = options.timeoutMs ?? 60_000;\n }\n\n async getJson<T>(path: string, options?: RequestOptions): Promise<T> {\n const res = await this.fetchWithTimeout(`${this.baseUrl}${path}`, {\n method: 'GET',\n headers: this.mergeHeaders(options),\n }, options);\n return this.parseJsonResponse<T>(res);\n }\n\n async postJson<T>(path: string, body: unknown, options?: RequestOptions): Promise<T> {\n const res = await this.fetchWithTimeout(`${this.baseUrl}${path}`, {\n method: 'POST',\n headers: this.mergeHeaders(options, { 'Content-Type': 'application/json' }),\n body: JSON.stringify(body),\n }, options);\n return this.parseJsonResponse<T>(res);\n }\n\n async postRaw(path: string, body: unknown, options?: RequestOptions): Promise<Response> {\n const res = await this.fetchWithTimeout(`${this.baseUrl}${path}`, {\n method: 'POST',\n headers: this.mergeHeaders(options, { 'Content-Type': 'application/json' }),\n body: JSON.stringify(body),\n }, options);\n if (!res.ok) {\n throw await this.toApiError(res);\n }\n return res;\n }\n\n private mergeHeaders(options?: RequestOptions, base?: Record<string, string>): Record<string, string> {\n return {\n Authorization: `Bearer ${this.apiKey}`,\n ...(base ?? {}),\n ...(options?.headers ?? {}),\n };\n }\n\n /**\n * Wrap a fetch in a combined abort signal: an internal timeout AND any\n * caller-provided signal. Either firing aborts the request. Single source of\n * abort plumbing — every method routes through here (DRY).\n */\n private async fetchWithTimeout(\n input: RequestInfo | URL,\n init: RequestInit,\n options?: RequestOptions,\n ): Promise<Response> {\n const timeoutMs = options?.timeoutMs ?? this.defaultTimeoutMs;\n const timeoutCtl = new AbortController();\n const timer = setTimeout(() => timeoutCtl.abort(), timeoutMs);\n\n // Combine internal timeout signal + caller signal. Native AbortSignal.any\n // (Node 20+ / modern Workers) is preferred; fall back to manual linking.\n const signal = combineSignals(timeoutCtl.signal, options?.signal);\n\n try {\n return await this.fetchFn(input, { ...init, signal });\n } catch (error) {\n if (timeoutCtl.signal.aborted) {\n throw new BuilderforceApiError(`Request timed out after ${timeoutMs}ms`, 408, 'timeout');\n }\n if (options?.signal?.aborted) {\n throw new BuilderforceApiError('Request aborted by caller', 499, 'aborted');\n }\n throw error;\n } finally {\n clearTimeout(timer);\n }\n }\n\n private async parseJsonResponse<T>(res: Response): Promise<T> {\n if (!res.ok) {\n throw await this.toApiError(res);\n }\n return res.json() as Promise<T>;\n }\n\n private async toApiError(res: Response): Promise<BuilderforceApiError> {\n const fallback = `Request failed (${res.status})`;\n const requestId = res.headers.get('x-request-id') ?? undefined;\n\n // Prefer server-supplied `Retry-After` header (seconds) when present; the\n // body's `retryAfter` is a fallback for environments that strip headers.\n const headerRetryAfter = parsePositiveInt(res.headers.get('retry-after'));\n\n try {\n const payload = await res.json() as Record<string, unknown> | null;\n\n // Two envelope shapes are in the wild:\n //\n // Flat — { error: \"msg\", code, details, terminal?, retryAfter? }\n // (gateway's documented shape — plan_token_limit_exceeded etc.)\n // Nested — { success: false, error: { code, message, details } }\n // (consumer-side wrappers around the gateway, e.g. some\n // tenant proxies emit AI_RATE_LIMITED / AI_UNAVAILABLE\n // envelopes that re-wrap the upstream error)\n //\n // Detect via the `success: false` discriminator and unwrap when nested\n // so `error.code` / `error.message` / `error.details` always populate.\n const isNested =\n payload !== null\n && typeof payload === 'object'\n && payload.success === false\n && typeof payload.error === 'object'\n && payload.error !== null;\n\n const inner = (isNested ? payload.error : payload) as {\n error?: string;\n message?: string;\n code?: string;\n details?: unknown;\n terminal?: boolean;\n retryAfter?: number;\n } | null;\n\n const message =\n (typeof inner?.message === 'string' && inner.message)\n || (typeof inner?.error === 'string' && inner.error)\n || fallback;\n\n return new BuilderforceApiError(\n message,\n res.status,\n inner?.code,\n inner?.details,\n requestId,\n {\n terminal: inner?.terminal,\n retryAfter: headerRetryAfter ?? inner?.retryAfter,\n },\n );\n } catch {\n const text = await res.text().catch(() => '');\n return new BuilderforceApiError(\n text || fallback,\n res.status,\n undefined,\n undefined,\n requestId,\n headerRetryAfter !== undefined ? { retryAfter: headerRetryAfter } : undefined,\n );\n }\n }\n}\n\nfunction parsePositiveInt(s: string | null): number | undefined {\n if (s == null) return undefined;\n const n = Number(s);\n return Number.isFinite(n) && n >= 0 ? Math.floor(n) : undefined;\n}\n\n/**\n * Combine multiple AbortSignals into one. Uses native `AbortSignal.any` when\n * available (Node 20+, modern Workers); falls back to manual event linking.\n */\nfunction combineSignals(...signals: Array<AbortSignal | undefined>): AbortSignal {\n const live = signals.filter((s): s is AbortSignal => s !== undefined);\n if (live.length === 1) return live[0]!;\n\n const anyImpl = (AbortSignal as unknown as { any?: (signals: AbortSignal[]) => AbortSignal }).any;\n if (typeof anyImpl === 'function') {\n return anyImpl(live);\n }\n\n const ctl = new AbortController();\n for (const s of live) {\n if (s.aborted) { ctl.abort(s.reason); break; }\n s.addEventListener('abort', () => ctl.abort(s.reason), { once: true });\n }\n return ctl.signal;\n}\n","import { ChatCompletionsApi } from './application/ChatCompletionsApi';\nimport { EmbeddingsApi } from './application/EmbeddingsApi';\nimport { ModelsApi } from './application/ModelsApi';\nimport { UsageApi } from './application/UsageApi';\nimport { BuilderforceApiError, HttpClient } from './infrastructure/httpClient';\n\nexport interface BuilderforceClientOptions {\n apiKey: string;\n baseUrl?: string;\n fetch?: typeof fetch;\n /** Default request timeout in ms (default 60_000). Per-call override available\n * via `chat.completions.create({ timeoutMs })` and `embeddings.create({ timeoutMs })`. */\n timeoutMs?: number;\n}\n\nexport class BuilderforceClient {\n public readonly chat: {\n completions: ChatCompletionsApi;\n };\n public readonly embeddings: EmbeddingsApi;\n public readonly models: ModelsApi;\n public readonly usage: UsageApi;\n\n constructor(options: BuilderforceClientOptions) {\n const apiKey = options.apiKey?.trim();\n if (!apiKey) {\n throw new BuilderforceApiError(\n 'BuilderforceClient requires a non-empty apiKey',\n 400,\n 'missing_api_key',\n );\n }\n\n const http = new HttpClient({\n apiKey,\n baseUrl: options.baseUrl ?? 'https://api.builderforce.ai',\n fetchFn: options.fetch,\n timeoutMs: options.timeoutMs,\n });\n\n this.chat = {\n completions: new ChatCompletionsApi(http),\n };\n this.embeddings = new EmbeddingsApi(http);\n this.models = new ModelsApi(http);\n this.usage = new UsageApi(http);\n }\n}\n"],"mappings":"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","names":["splitTransportOptions"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/infrastructure/sse.ts","../src/application/ChatCompletionsApi.ts","../src/application/EmbeddingsApi.ts","../src/application/ImagesApi.ts","../src/application/ModelsApi.ts","../src/application/UsageApi.ts","../src/infrastructure/httpClient.ts","../src/BuilderforceClient.ts"],"sourcesContent":["export { BuilderforceClient, type BuilderforceClientOptions } from './BuilderforceClient';\n\nexport type {\n // Roles & content\n ChatRole,\n ChatMessage,\n ContentPart,\n TextContentPart,\n ImageUrlContentPart,\n // Tool calling\n ToolSpec,\n ToolCall,\n ToolCallFunction,\n ToolCallDelta,\n ToolChoice,\n FunctionDefinition,\n // Structured output\n ResponseFormat,\n JsonSchemaSpec,\n // Per-call options\n PerCallOptions,\n // Chat completions\n ChatCompletionCreateParams,\n ChatCompletionChunk,\n ChatCompletionResponse,\n FailoverEvent,\n // Models / usage\n ModelsListResponse,\n UsageByModel,\n UsageByDay,\n UsageByUser,\n UsageResponse,\n UsageGetParams,\n // Embeddings\n EmbeddingsCreateParams,\n EmbeddingsResponse,\n EmbeddingObject,\n // Image generation\n ImageGenerationCreateParams,\n ImageGenerationResponse,\n ImageGenerationDataEntry,\n} from './domain/types';\n\nexport { ChatCompletionStream } from './application/ChatCompletionsApi';\nexport { EmbeddingsApi } from './application/EmbeddingsApi';\nexport { ImagesApi } from './application/ImagesApi';\nexport { BuilderforceApiError } from './infrastructure/httpClient';\n","export async function* parseSseJson<T>(\n stream: ReadableStream<Uint8Array>,\n): AsyncGenerator<T, void, unknown> {\n const reader = stream.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() ?? '';\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed.startsWith('data: ')) continue;\n\n const data = trimmed.slice(6).trim();\n if (data === '[DONE]') return;\n\n try {\n yield JSON.parse(data) as T;\n } catch {\n // Skip malformed chunks instead of breaking the stream.\n }\n }\n }\n}\n","import type { ChatCompletionChunk, ChatCompletionCreateParams, ChatCompletionResponse } from '../domain/types';\nimport { HttpClient, type RequestOptions } from '../infrastructure/httpClient';\nimport { parseSseJson } from '../infrastructure/sse';\n\nexport class ChatCompletionStream implements AsyncIterable<ChatCompletionChunk> {\n private readonly stream: ReadableStream<Uint8Array>;\n\n constructor(stream: ReadableStream<Uint8Array>) {\n this.stream = stream;\n }\n\n [Symbol.asyncIterator](): AsyncIterator<ChatCompletionChunk, void, unknown> {\n return parseSseJson<ChatCompletionChunk>(this.stream);\n }\n\n async toText(): Promise<string> {\n let full = '';\n for await (const chunk of this) {\n const delta = chunk.choices?.[0]?.delta?.content;\n if (typeof delta === 'string') {\n full += delta;\n }\n }\n return full;\n }\n}\n\n/**\n * Pull SDK-level transport options (timeout, signal, idempotency key) out of\n * the params object so they don't get JSON-serialized into the request body.\n * Returns the request options AND the cleaned-up body.\n */\nfunction splitTransportOptions(params: ChatCompletionCreateParams): {\n body: Record<string, unknown>;\n request: RequestOptions;\n} {\n const { timeoutMs, signal, idempotencyKey, ...rest } = params;\n const headers: Record<string, string> = {};\n if (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey;\n return {\n body: rest as unknown as Record<string, unknown>,\n request: {\n timeoutMs,\n signal,\n ...(Object.keys(headers).length > 0 ? { headers } : {}),\n },\n };\n}\n\nexport class ChatCompletionsApi {\n private readonly http: HttpClient;\n\n constructor(http: HttpClient) {\n this.http = http;\n }\n\n async create(params: ChatCompletionCreateParams & { stream: true }): Promise<ChatCompletionStream>;\n async create(params: ChatCompletionCreateParams & { stream?: false | undefined }): Promise<ChatCompletionResponse>;\n async create(\n params: ChatCompletionCreateParams,\n ): Promise<ChatCompletionResponse | ChatCompletionStream> {\n const { body, request } = splitTransportOptions(params);\n\n if (params.stream) {\n const response = await this.http.postRaw('/llm/v1/chat/completions', body, request);\n if (!response.body) {\n throw new Error('Streaming response body is missing');\n }\n return new ChatCompletionStream(response.body);\n }\n\n return this.http.postJson<ChatCompletionResponse>('/llm/v1/chat/completions', body, request);\n }\n}\n","import type { EmbeddingsCreateParams, EmbeddingsResponse } from '../domain/types';\nimport { HttpClient, type RequestOptions } from '../infrastructure/httpClient';\n\n/**\n * Pull SDK-level transport options out of the params so they don't ride\n * along inside the JSON body. Same shape as ChatCompletionsApi (DRY pattern).\n */\nfunction splitTransportOptions(params: EmbeddingsCreateParams): {\n body: Record<string, unknown>;\n request: RequestOptions;\n} {\n const { timeoutMs, signal, idempotencyKey, ...rest } = params;\n const headers: Record<string, string> = {};\n if (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey;\n return {\n body: rest as unknown as Record<string, unknown>,\n request: {\n timeoutMs,\n signal,\n ...(Object.keys(headers).length > 0 ? { headers } : {}),\n },\n };\n}\n\nexport class EmbeddingsApi {\n private readonly http: HttpClient;\n\n constructor(http: HttpClient) {\n this.http = http;\n }\n\n /**\n * Create one or more text embeddings. Wired to OpenRouter (default model\n * `nvidia/llama-nemotron-embed-vl-1b-v2:free`). Override via `model`.\n */\n create(params: EmbeddingsCreateParams): Promise<EmbeddingsResponse> {\n const { body, request } = splitTransportOptions(params);\n return this.http.postJson<EmbeddingsResponse>('/llm/v1/embeddings', body, request);\n }\n}\n","import type { ImageGenerationCreateParams, ImageGenerationResponse } from '../domain/types';\nimport { HttpClient, type RequestOptions } from '../infrastructure/httpClient';\n\n/**\n * Pull SDK-level transport options out of the params so they don't ride\n * along inside the JSON body. Same shape as ChatCompletionsApi / EmbeddingsApi\n * (DRY pattern — every API class uses the same splitter).\n */\nfunction splitTransportOptions(params: ImageGenerationCreateParams): {\n body: Record<string, unknown>;\n request: RequestOptions;\n} {\n const { timeoutMs, signal, idempotencyKey, ...rest } = params;\n const headers: Record<string, string> = {};\n if (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey;\n return {\n body: rest as unknown as Record<string, unknown>,\n request: {\n timeoutMs,\n signal,\n ...(Object.keys(headers).length > 0 ? { headers } : {}),\n },\n };\n}\n\n/**\n * `client.images.generate({ prompt, ... })` — OpenAI-compatible image generation\n * routed through the Builderforce gateway. The gateway cascades free Together\n * vendors → premium FluxAPI fallback so callers always see a successful\n * response unless every upstream is saturated. Read\n * `_builderforce.resolvedModel` / `resolvedVendor` to detect which vendor\n * served the request.\n *\n * Image generations are billed against the tenant's daily token budget at a\n * flat per-image rate (currently ~1000 tokens/image — deliberately conservative).\n * Hitting the cap returns the same `429 plan_token_limit_exceeded` envelope\n * as chat — caller code that already handles that path needs no changes.\n */\nexport class ImagesApi {\n private readonly http: HttpClient;\n\n constructor(http: HttpClient) {\n this.http = http;\n }\n\n generate(params: ImageGenerationCreateParams): Promise<ImageGenerationResponse> {\n const { body, request } = splitTransportOptions(params);\n return this.http.postJson<ImageGenerationResponse>('/llm/v1/images/generations', body, request);\n }\n}\n","import type { ModelsListResponse } from '../domain/types';\nimport { HttpClient } from '../infrastructure/httpClient';\n\nexport class ModelsApi {\n private readonly http: HttpClient;\n\n constructor(http: HttpClient) {\n this.http = http;\n }\n\n list(): Promise<ModelsListResponse> {\n return this.http.getJson<ModelsListResponse>('/llm/v1/models');\n }\n}\n","import type { UsageGetParams, UsageResponse } from '../domain/types';\nimport { HttpClient } from '../infrastructure/httpClient';\n\nexport class UsageApi {\n private readonly http: HttpClient;\n\n constructor(http: HttpClient) {\n this.http = http;\n }\n\n get(params: UsageGetParams = {}): Promise<UsageResponse> {\n const query = typeof params.days === 'number' ? `?days=${encodeURIComponent(String(params.days))}` : '';\n return this.http.getJson<UsageResponse>(`/llm/v1/usage${query}`);\n }\n}\n","import type { FailoverEvent } from '../domain/types';\n\nexport class BuilderforceApiError extends Error {\n public readonly status: number;\n public readonly code?: string;\n public readonly details?: unknown;\n public readonly requestId?: string;\n /**\n * `true` when the gateway has signalled this error will not resolve by\n * retrying on a different model — e.g. plan or per-claw daily token cap\n * exhausted (those caps are per-tenant, not per-model). Consumer-side\n * fallback chains should short-circuit when this is set.\n */\n public readonly terminal?: boolean;\n /** Seconds the consumer should wait before retrying — server-supplied. */\n public readonly retryAfter?: number;\n /**\n * Cascade attempts that failed before this error was returned — populated\n * when the gateway returns `429 cascade_exhausted` with a `details.failovers`\n * array. Each entry includes the vendor that owns the model so callers can\n * detect single-vendor saturation (e.g. all attempts on `openrouter`).\n */\n public readonly failovers?: FailoverEvent[];\n /**\n * Upstream vendor the gateway dispatched against (`'openrouter' | 'cerebras'\n * | 'nvidia' | 'ollama' | 'googleai' | …`). Set on every error where the\n * gateway selected an upstream — including single-attempt failures that\n * never ran a cascade (timeouts, single-vendor 429s, `model_unavailable`).\n *\n * Unset only for pre-dispatch errors where no vendor was ever selected:\n * `401`/`403` auth failures, `400` validation failures, `409` idempotent\n * replay, and tenant-cap 429s (`plan_token_limit_exceeded`,\n * `claw_token_limit_exceeded`) — those caps are per-tenant, not per-model.\n *\n * Sourced from the gateway's catalog lookup so consumers never have to\n * parse the model id to recover vendor identity.\n */\n public readonly vendor?: string;\n /**\n * Model id the gateway dispatched against — set whenever `vendor` is set.\n * Pair with `vendor` for per-attempt observability without prefix parsing.\n */\n public readonly model?: string;\n\n constructor(\n message: string,\n status: number,\n code?: string,\n details?: unknown,\n requestId?: string,\n extras?: { terminal?: boolean; retryAfter?: number; vendor?: string; model?: string },\n ) {\n super(message);\n this.name = 'BuilderforceApiError';\n this.status = status;\n this.code = code;\n this.details = details;\n this.requestId = requestId;\n this.terminal = extras?.terminal;\n this.retryAfter = extras?.retryAfter;\n this.vendor = extras?.vendor;\n this.model = extras?.model;\n // Pull typed failovers out of `details.failovers` when the gateway\n // supplied them. Validation is light — drop entries missing required\n // fields so consumers never get a partially-populated row.\n if (details && typeof details === 'object') {\n const f = (details as { failovers?: unknown }).failovers;\n if (Array.isArray(f)) {\n const cleaned: FailoverEvent[] = [];\n for (const entry of f) {\n if (entry && typeof entry === 'object') {\n const e = entry as { model?: unknown; vendor?: unknown; code?: unknown };\n if (typeof e.model === 'string' && typeof e.vendor === 'string' && typeof e.code === 'number') {\n cleaned.push({ model: e.model, vendor: e.vendor, code: e.code });\n }\n }\n }\n if (cleaned.length > 0) this.failovers = cleaned;\n }\n }\n }\n}\n\nexport interface HttpClientOptions {\n apiKey: string;\n baseUrl: string;\n fetchFn?: typeof fetch;\n /** Default per-request timeout in ms. Overridable per call. */\n timeoutMs?: number;\n}\n\n/** Per-request overrides — passed by the API layer, not by SDK consumers directly. */\nexport interface RequestOptions {\n /** Override the client default timeout for just this request. */\n timeoutMs?: number;\n /** Caller-provided AbortSignal. Linked together with the SDK's internal timeout\n * signal — whichever fires first aborts the request. */\n signal?: AbortSignal;\n /** Extra headers to merge in (e.g. `Idempotency-Key`). */\n headers?: Record<string, string>;\n}\n\nexport class HttpClient {\n private readonly apiKey: string;\n private readonly baseUrl: string;\n private readonly fetchFn: typeof fetch;\n private readonly defaultTimeoutMs: number;\n\n constructor(options: HttpClientOptions) {\n this.apiKey = options.apiKey;\n this.baseUrl = options.baseUrl.replace(/\\/$/, '');\n // Bind to `globalThis` so calling via `this.fetchFn(...)` doesn't trip\n // Cloudflare Workers' \"Illegal invocation\" check — the platform's fetch\n // requires the global receiver, not an instance method `this`. Affects\n // any environment that ships a strict-receiver fetch (Workers, Bun, etc.)\n // and is harmless on Node + browsers.\n const fetchImpl = options.fetchFn ?? fetch;\n this.fetchFn = fetchImpl.bind(globalThis);\n // 180s aligns the outer SDK budget with the premium routing path on the\n // gateway: per-vendor 60s × up to 3 PREMIUM-tier attempts = ~180s. Customers\n // running tailor / job-extract style long-context calls were hitting the\n // previous 60s cap before the gateway could finish its premium cascade.\n // Per-call `timeoutMs` still overrides for callers that want a tighter UX.\n this.defaultTimeoutMs = options.timeoutMs ?? 180_000;\n }\n\n async getJson<T>(path: string, options?: RequestOptions): Promise<T> {\n const res = await this.fetchWithTimeout(`${this.baseUrl}${path}`, {\n method: 'GET',\n headers: this.mergeHeaders(options),\n }, options);\n return this.parseJsonResponse<T>(res);\n }\n\n async postJson<T>(path: string, body: unknown, options?: RequestOptions): Promise<T> {\n const res = await this.fetchWithTimeout(`${this.baseUrl}${path}`, {\n method: 'POST',\n headers: this.mergeHeaders(options, { 'Content-Type': 'application/json' }),\n body: JSON.stringify(body),\n }, options);\n return this.parseJsonResponse<T>(res);\n }\n\n async postRaw(path: string, body: unknown, options?: RequestOptions): Promise<Response> {\n const res = await this.fetchWithTimeout(`${this.baseUrl}${path}`, {\n method: 'POST',\n headers: this.mergeHeaders(options, { 'Content-Type': 'application/json' }),\n body: JSON.stringify(body),\n }, options);\n if (!res.ok) {\n throw await this.toApiError(res);\n }\n return res;\n }\n\n private mergeHeaders(options?: RequestOptions, base?: Record<string, string>): Record<string, string> {\n return {\n Authorization: `Bearer ${this.apiKey}`,\n ...(base ?? {}),\n ...(options?.headers ?? {}),\n };\n }\n\n /**\n * Wrap a fetch in a combined abort signal: an internal timeout AND any\n * caller-provided signal. Either firing aborts the request. Single source of\n * abort plumbing — every method routes through here (DRY).\n */\n private async fetchWithTimeout(\n input: RequestInfo | URL,\n init: RequestInit,\n options?: RequestOptions,\n ): Promise<Response> {\n const timeoutMs = options?.timeoutMs ?? this.defaultTimeoutMs;\n const timeoutCtl = new AbortController();\n const timer = setTimeout(() => timeoutCtl.abort(), timeoutMs);\n\n // Combine internal timeout signal + caller signal. Native AbortSignal.any\n // (Node 20+ / modern Workers) is preferred; fall back to manual linking.\n const signal = combineSignals(timeoutCtl.signal, options?.signal);\n\n try {\n return await this.fetchFn(input, { ...init, signal });\n } catch (error) {\n if (timeoutCtl.signal.aborted) {\n throw new BuilderforceApiError(`Request timed out after ${timeoutMs}ms`, 408, 'timeout');\n }\n if (options?.signal?.aborted) {\n throw new BuilderforceApiError('Request aborted by caller', 499, 'aborted');\n }\n throw error;\n } finally {\n clearTimeout(timer);\n }\n }\n\n private async parseJsonResponse<T>(res: Response): Promise<T> {\n if (!res.ok) {\n throw await this.toApiError(res);\n }\n return res.json() as Promise<T>;\n }\n\n private async toApiError(res: Response): Promise<BuilderforceApiError> {\n const fallback = `Request failed (${res.status})`;\n const requestId = res.headers.get('x-request-id') ?? undefined;\n\n // Prefer server-supplied `Retry-After` header (seconds) when present; the\n // body's `retryAfter` is a fallback for environments that strip headers.\n const headerRetryAfter = parsePositiveInt(res.headers.get('retry-after'));\n\n try {\n const payload = await res.json() as Record<string, unknown> | null;\n\n // Three envelope shapes are in the wild:\n //\n // Flat — { error: \"msg\", code, details, terminal?, retryAfter? }\n // (gateway's documented shape — plan_token_limit_exceeded etc.)\n // OpenAI — { error: { message, code, type, details } }\n // (cascade-exhausted 429 on both chat and image surfaces —\n // matches OpenAI's error envelope convention)\n // Wrapped — { success: false, error: { code, message, details } }\n // (consumer-side wrappers around the gateway, e.g. some\n // tenant proxies emit AI_RATE_LIMITED / AI_UNAVAILABLE\n // envelopes that re-wrap the upstream error)\n //\n // Unwrap to a single `inner` shape so `details.failovers` etc. always\n // populate on `BuilderforceApiError` regardless of which envelope the\n // gateway picked. Single parsing site for every surface (DRY).\n const errorObj =\n payload !== null\n && typeof payload === 'object'\n && typeof payload.error === 'object'\n && payload.error !== null\n ? payload.error as Record<string, unknown>\n : null;\n\n const isWrapped =\n payload !== null\n && typeof payload === 'object'\n && payload.success === false\n && errorObj !== null;\n\n const inner = (isWrapped || errorObj !== null ? errorObj : payload) as {\n error?: string;\n message?: string;\n code?: string | number;\n details?: unknown;\n terminal?: boolean;\n retryAfter?: number;\n vendor?: string;\n model?: string;\n } | null;\n\n const message =\n (typeof inner?.message === 'string' && inner.message)\n || (typeof inner?.error === 'string' && inner.error)\n || fallback;\n\n // Coerce numeric error codes (e.g. cascade-exhausted emits `code: 429`)\n // to string so `BuilderforceApiError.code` stays a stable type for\n // consumer-side switches. String codes pass through unchanged.\n const code = typeof inner?.code === 'number' ? String(inner.code) : inner?.code;\n\n // Vendor/model may travel at the top of the error envelope (gateway\n // dispatched against an upstream) OR be embedded in details (e.g.\n // `model_unavailable` carries `details.requestedModel`). Prefer the\n // top-level fields; fall back to details so we still extract a model\n // from the strict-pin 503 envelope without a gateway change.\n const detailsObj = (inner?.details && typeof inner.details === 'object')\n ? inner.details as { vendor?: unknown; model?: unknown; requestedModel?: unknown }\n : null;\n const vendor = typeof inner?.vendor === 'string'\n ? inner.vendor\n : (typeof detailsObj?.vendor === 'string' ? detailsObj.vendor : undefined);\n const model = typeof inner?.model === 'string'\n ? inner.model\n : (typeof detailsObj?.model === 'string'\n ? detailsObj.model\n : (typeof detailsObj?.requestedModel === 'string' ? detailsObj.requestedModel : undefined));\n\n return new BuilderforceApiError(\n message,\n res.status,\n code,\n inner?.details,\n requestId,\n {\n terminal: inner?.terminal,\n retryAfter: headerRetryAfter ?? inner?.retryAfter,\n vendor,\n model,\n },\n );\n } catch {\n const text = await res.text().catch(() => '');\n return new BuilderforceApiError(\n text || fallback,\n res.status,\n undefined,\n undefined,\n requestId,\n headerRetryAfter !== undefined ? { retryAfter: headerRetryAfter } : undefined,\n );\n }\n }\n}\n\nfunction parsePositiveInt(s: string | null): number | undefined {\n if (s == null) return undefined;\n const n = Number(s);\n return Number.isFinite(n) && n >= 0 ? Math.floor(n) : undefined;\n}\n\n/**\n * Combine multiple AbortSignals into one. Uses native `AbortSignal.any` when\n * available (Node 20+, modern Workers); falls back to manual event linking.\n */\nfunction combineSignals(...signals: Array<AbortSignal | undefined>): AbortSignal {\n const live = signals.filter((s): s is AbortSignal => s !== undefined);\n if (live.length === 1) return live[0]!;\n\n const anyImpl = (AbortSignal as unknown as { any?: (signals: AbortSignal[]) => AbortSignal }).any;\n if (typeof anyImpl === 'function') {\n return anyImpl(live);\n }\n\n const ctl = new AbortController();\n for (const s of live) {\n if (s.aborted) { ctl.abort(s.reason); break; }\n s.addEventListener('abort', () => ctl.abort(s.reason), { once: true });\n }\n return ctl.signal;\n}\n","import { ChatCompletionsApi } from './application/ChatCompletionsApi';\nimport { EmbeddingsApi } from './application/EmbeddingsApi';\nimport { ImagesApi } from './application/ImagesApi';\nimport { ModelsApi } from './application/ModelsApi';\nimport { UsageApi } from './application/UsageApi';\nimport { BuilderforceApiError, HttpClient } from './infrastructure/httpClient';\n\nexport interface BuilderforceClientOptions {\n apiKey: string;\n baseUrl?: string;\n fetch?: typeof fetch;\n /** Default request timeout in ms (default 60_000). Per-call override available\n * via `chat.completions.create({ timeoutMs })` and `embeddings.create({ timeoutMs })`. */\n timeoutMs?: number;\n}\n\nexport class BuilderforceClient {\n public readonly chat: {\n completions: ChatCompletionsApi;\n };\n public readonly embeddings: EmbeddingsApi;\n public readonly images: ImagesApi;\n public readonly models: ModelsApi;\n public readonly usage: UsageApi;\n\n constructor(options: BuilderforceClientOptions) {\n const apiKey = options.apiKey?.trim();\n if (!apiKey) {\n throw new BuilderforceApiError(\n 'BuilderforceClient requires a non-empty apiKey',\n 400,\n 'missing_api_key',\n );\n }\n\n const http = new HttpClient({\n apiKey,\n baseUrl: options.baseUrl ?? 'https://api.builderforce.ai',\n fetchFn: options.fetch,\n timeoutMs: options.timeoutMs,\n });\n\n this.chat = {\n completions: new ChatCompletionsApi(http),\n };\n this.embeddings = new EmbeddingsApi(http);\n this.images = new ImagesApi(http);\n this.models = new ModelsApi(http);\n this.usage = new UsageApi(http);\n }\n}\n"],"mappings":"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","names":["splitTransportOptions","splitTransportOptions"]}
package/dist/index.d.cts CHANGED
@@ -1,4 +1,20 @@
1
1
  type ChatRole = 'system' | 'user' | 'assistant' | 'tool';
2
+ /**
3
+ * One model attempt that failed before the resolved model succeeded.
4
+ * Surfaced on successful responses in `_builderforce.failovers` (when retries
5
+ * happened) and on cascade-exhausted errors in `error.details.failovers`.
6
+ *
7
+ * `vendor` lets callers detect when every failure concentrated on one
8
+ * upstream — e.g. all `openrouter` means a saturated shared key, not a
9
+ * model-specific issue.
10
+ */
11
+ interface FailoverEvent {
12
+ model: string;
13
+ /** `'openrouter' | 'cerebras' | 'nvidia' | 'ollama'` */
14
+ vendor: string;
15
+ /** HTTP status code, or 0 for embedded errors / network failures. */
16
+ code: number;
17
+ }
2
18
  interface TextContentPart {
3
19
  type: 'text';
4
20
  text: string;
@@ -184,8 +200,22 @@ interface ChatCompletionResponse {
184
200
  _builderforce?: {
185
201
  /** The model the gateway dispatched against. Equals `request.model` when caller pinned. */
186
202
  resolvedModel?: string;
203
+ /**
204
+ * Vendor that owns the resolved model (`'openrouter' | 'cerebras' | 'nvidia'
205
+ * | 'ollama' | 'googleai' | …`). Sourced from the gateway's catalog so
206
+ * consumers doing per-vendor cost / latency aggregation get a single field
207
+ * to group by without parsing model-id prefixes.
208
+ */
209
+ resolvedVendor?: string;
187
210
  /** How many vendor retries happened inside the failover chain. */
188
211
  retries?: number;
212
+ /**
213
+ * Per-attempt breakdown of the cascade — present only when `retries > 0`.
214
+ * Each entry is one model the gateway tried that failed before the
215
+ * resolved model succeeded. Use `vendor` to detect single-vendor
216
+ * concentration (e.g. all failures on `openrouter` = saturated key).
217
+ */
218
+ failovers?: FailoverEvent[];
189
219
  pool?: number;
190
220
  product?: string;
191
221
  effectivePlan?: string;
@@ -301,6 +331,66 @@ interface EmbeddingsResponse {
301
331
  };
302
332
  [key: string]: unknown;
303
333
  }
334
+ interface ImageGenerationCreateParams extends PerCallOptions {
335
+ /**
336
+ * Model hint — gateway-owned routing. Bare ids resolve via catalog lookup;
337
+ * vendor-prefixed ids (`together/<id>`, `fluxapi/flux-kontext-pro`) pin to a
338
+ * specific vendor. When unset, the gateway picks from the tenant-plan image
339
+ * pool starting with the free tier.
340
+ */
341
+ model?: string;
342
+ /** Required text prompt. */
343
+ prompt: string;
344
+ /** OpenAI-compatible size string: "1024x1024", "1792x1024", "1024x1792", etc.
345
+ * Mapped to each vendor's native dimension format (FluxAPI receives an
346
+ * `aspectRatio`; Together receives `width`/`height`). */
347
+ size?: string;
348
+ /** Number of images to generate (default 1). Vendors that don't support
349
+ * batching silently clamp to 1 — read `data.length` to confirm. */
350
+ n?: number;
351
+ /** "url" (default) returns hosted URLs; "b64_json" returns base64-encoded image bytes. */
352
+ response_format?: 'url' | 'b64_json';
353
+ /** Opaque telemetry slug — same semantics as chat. Persisted to `llm_usage_log.use_case`. */
354
+ useCase?: string;
355
+ /** Free-form attribution metadata — same semantics as chat. */
356
+ metadata?: Record<string, string>;
357
+ [key: string]: unknown;
358
+ }
359
+ interface ImageGenerationDataEntry {
360
+ url?: string;
361
+ b64_json?: string;
362
+ /** Vendor-side prompt revision (some vendors auto-rewrite for safety / quality). */
363
+ revised_prompt?: string;
364
+ }
365
+ interface ImageGenerationResponse {
366
+ /** ISO seconds timestamp — OpenAI-compatible. */
367
+ created: number;
368
+ /** One entry per generated image. */
369
+ data: ImageGenerationDataEntry[];
370
+ /** The model that actually served the request (same as `_builderforce.resolvedModel`). */
371
+ model?: string;
372
+ _builderforce?: {
373
+ /** The model the gateway dispatched against. */
374
+ resolvedModel?: string;
375
+ /** Vendor that owns the resolved model — `'together' | 'fluxapi'`. */
376
+ resolvedVendor?: string;
377
+ /** How many vendor retries happened before the resolved vendor succeeded. */
378
+ retries?: number;
379
+ /** Per-attempt breakdown, present only when `retries > 0`. */
380
+ failovers?: FailoverEvent[];
381
+ product?: string;
382
+ effectivePlan?: string;
383
+ /** True when superadmin premium-override is active for this tenant. */
384
+ premium?: boolean;
385
+ /** Echo of `request.useCase`. */
386
+ useCase?: string;
387
+ /** Echo of `request.metadata`. */
388
+ metadata?: Record<string, string>;
389
+ /** Mirror of the `x-request-id` response header. */
390
+ requestId?: string;
391
+ };
392
+ [key: string]: unknown;
393
+ }
304
394
 
305
395
  declare class BuilderforceApiError extends Error {
306
396
  readonly status: number;
@@ -316,9 +406,38 @@ declare class BuilderforceApiError extends Error {
316
406
  readonly terminal?: boolean;
317
407
  /** Seconds the consumer should wait before retrying — server-supplied. */
318
408
  readonly retryAfter?: number;
409
+ /**
410
+ * Cascade attempts that failed before this error was returned — populated
411
+ * when the gateway returns `429 cascade_exhausted` with a `details.failovers`
412
+ * array. Each entry includes the vendor that owns the model so callers can
413
+ * detect single-vendor saturation (e.g. all attempts on `openrouter`).
414
+ */
415
+ readonly failovers?: FailoverEvent[];
416
+ /**
417
+ * Upstream vendor the gateway dispatched against (`'openrouter' | 'cerebras'
418
+ * | 'nvidia' | 'ollama' | 'googleai' | …`). Set on every error where the
419
+ * gateway selected an upstream — including single-attempt failures that
420
+ * never ran a cascade (timeouts, single-vendor 429s, `model_unavailable`).
421
+ *
422
+ * Unset only for pre-dispatch errors where no vendor was ever selected:
423
+ * `401`/`403` auth failures, `400` validation failures, `409` idempotent
424
+ * replay, and tenant-cap 429s (`plan_token_limit_exceeded`,
425
+ * `claw_token_limit_exceeded`) — those caps are per-tenant, not per-model.
426
+ *
427
+ * Sourced from the gateway's catalog lookup so consumers never have to
428
+ * parse the model id to recover vendor identity.
429
+ */
430
+ readonly vendor?: string;
431
+ /**
432
+ * Model id the gateway dispatched against — set whenever `vendor` is set.
433
+ * Pair with `vendor` for per-attempt observability without prefix parsing.
434
+ */
435
+ readonly model?: string;
319
436
  constructor(message: string, status: number, code?: string, details?: unknown, requestId?: string, extras?: {
320
437
  terminal?: boolean;
321
438
  retryAfter?: number;
439
+ vendor?: string;
440
+ model?: string;
322
441
  });
323
442
  }
324
443
  interface HttpClientOptions {
@@ -385,6 +504,25 @@ declare class EmbeddingsApi {
385
504
  create(params: EmbeddingsCreateParams): Promise<EmbeddingsResponse>;
386
505
  }
387
506
 
507
+ /**
508
+ * `client.images.generate({ prompt, ... })` — OpenAI-compatible image generation
509
+ * routed through the Builderforce gateway. The gateway cascades free Together
510
+ * vendors → premium FluxAPI fallback so callers always see a successful
511
+ * response unless every upstream is saturated. Read
512
+ * `_builderforce.resolvedModel` / `resolvedVendor` to detect which vendor
513
+ * served the request.
514
+ *
515
+ * Image generations are billed against the tenant's daily token budget at a
516
+ * flat per-image rate (currently ~1000 tokens/image — deliberately conservative).
517
+ * Hitting the cap returns the same `429 plan_token_limit_exceeded` envelope
518
+ * as chat — caller code that already handles that path needs no changes.
519
+ */
520
+ declare class ImagesApi {
521
+ private readonly http;
522
+ constructor(http: HttpClient);
523
+ generate(params: ImageGenerationCreateParams): Promise<ImageGenerationResponse>;
524
+ }
525
+
388
526
  declare class ModelsApi {
389
527
  private readonly http;
390
528
  constructor(http: HttpClient);
@@ -410,9 +548,10 @@ declare class BuilderforceClient {
410
548
  completions: ChatCompletionsApi;
411
549
  };
412
550
  readonly embeddings: EmbeddingsApi;
551
+ readonly images: ImagesApi;
413
552
  readonly models: ModelsApi;
414
553
  readonly usage: UsageApi;
415
554
  constructor(options: BuilderforceClientOptions);
416
555
  }
417
556
 
418
- export { BuilderforceApiError, BuilderforceClient, type BuilderforceClientOptions, type ChatCompletionChunk, type ChatCompletionCreateParams, type ChatCompletionResponse, ChatCompletionStream, type ChatMessage, type ChatRole, type ContentPart, type EmbeddingObject, EmbeddingsApi, type EmbeddingsCreateParams, type EmbeddingsResponse, type FunctionDefinition, type ImageUrlContentPart, type JsonSchemaSpec, type ModelsListResponse, type PerCallOptions, type ResponseFormat, type TextContentPart, type ToolCall, type ToolCallDelta, type ToolCallFunction, type ToolChoice, type ToolSpec, type UsageByDay, type UsageByModel, type UsageByUser, type UsageGetParams, type UsageResponse };
557
+ export { BuilderforceApiError, BuilderforceClient, type BuilderforceClientOptions, type ChatCompletionChunk, type ChatCompletionCreateParams, type ChatCompletionResponse, ChatCompletionStream, type ChatMessage, type ChatRole, type ContentPart, type EmbeddingObject, EmbeddingsApi, type EmbeddingsCreateParams, type EmbeddingsResponse, type FailoverEvent, type FunctionDefinition, type ImageGenerationCreateParams, type ImageGenerationDataEntry, type ImageGenerationResponse, type ImageUrlContentPart, ImagesApi, type JsonSchemaSpec, type ModelsListResponse, type PerCallOptions, type ResponseFormat, type TextContentPart, type ToolCall, type ToolCallDelta, type ToolCallFunction, type ToolChoice, type ToolSpec, type UsageByDay, type UsageByModel, type UsageByUser, type UsageGetParams, type UsageResponse };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,20 @@
1
1
  type ChatRole = 'system' | 'user' | 'assistant' | 'tool';
2
+ /**
3
+ * One model attempt that failed before the resolved model succeeded.
4
+ * Surfaced on successful responses in `_builderforce.failovers` (when retries
5
+ * happened) and on cascade-exhausted errors in `error.details.failovers`.
6
+ *
7
+ * `vendor` lets callers detect when every failure concentrated on one
8
+ * upstream — e.g. all `openrouter` means a saturated shared key, not a
9
+ * model-specific issue.
10
+ */
11
+ interface FailoverEvent {
12
+ model: string;
13
+ /** `'openrouter' | 'cerebras' | 'nvidia' | 'ollama'` */
14
+ vendor: string;
15
+ /** HTTP status code, or 0 for embedded errors / network failures. */
16
+ code: number;
17
+ }
2
18
  interface TextContentPart {
3
19
  type: 'text';
4
20
  text: string;
@@ -184,8 +200,22 @@ interface ChatCompletionResponse {
184
200
  _builderforce?: {
185
201
  /** The model the gateway dispatched against. Equals `request.model` when caller pinned. */
186
202
  resolvedModel?: string;
203
+ /**
204
+ * Vendor that owns the resolved model (`'openrouter' | 'cerebras' | 'nvidia'
205
+ * | 'ollama' | 'googleai' | …`). Sourced from the gateway's catalog so
206
+ * consumers doing per-vendor cost / latency aggregation get a single field
207
+ * to group by without parsing model-id prefixes.
208
+ */
209
+ resolvedVendor?: string;
187
210
  /** How many vendor retries happened inside the failover chain. */
188
211
  retries?: number;
212
+ /**
213
+ * Per-attempt breakdown of the cascade — present only when `retries > 0`.
214
+ * Each entry is one model the gateway tried that failed before the
215
+ * resolved model succeeded. Use `vendor` to detect single-vendor
216
+ * concentration (e.g. all failures on `openrouter` = saturated key).
217
+ */
218
+ failovers?: FailoverEvent[];
189
219
  pool?: number;
190
220
  product?: string;
191
221
  effectivePlan?: string;
@@ -301,6 +331,66 @@ interface EmbeddingsResponse {
301
331
  };
302
332
  [key: string]: unknown;
303
333
  }
334
+ interface ImageGenerationCreateParams extends PerCallOptions {
335
+ /**
336
+ * Model hint — gateway-owned routing. Bare ids resolve via catalog lookup;
337
+ * vendor-prefixed ids (`together/<id>`, `fluxapi/flux-kontext-pro`) pin to a
338
+ * specific vendor. When unset, the gateway picks from the tenant-plan image
339
+ * pool starting with the free tier.
340
+ */
341
+ model?: string;
342
+ /** Required text prompt. */
343
+ prompt: string;
344
+ /** OpenAI-compatible size string: "1024x1024", "1792x1024", "1024x1792", etc.
345
+ * Mapped to each vendor's native dimension format (FluxAPI receives an
346
+ * `aspectRatio`; Together receives `width`/`height`). */
347
+ size?: string;
348
+ /** Number of images to generate (default 1). Vendors that don't support
349
+ * batching silently clamp to 1 — read `data.length` to confirm. */
350
+ n?: number;
351
+ /** "url" (default) returns hosted URLs; "b64_json" returns base64-encoded image bytes. */
352
+ response_format?: 'url' | 'b64_json';
353
+ /** Opaque telemetry slug — same semantics as chat. Persisted to `llm_usage_log.use_case`. */
354
+ useCase?: string;
355
+ /** Free-form attribution metadata — same semantics as chat. */
356
+ metadata?: Record<string, string>;
357
+ [key: string]: unknown;
358
+ }
359
+ interface ImageGenerationDataEntry {
360
+ url?: string;
361
+ b64_json?: string;
362
+ /** Vendor-side prompt revision (some vendors auto-rewrite for safety / quality). */
363
+ revised_prompt?: string;
364
+ }
365
+ interface ImageGenerationResponse {
366
+ /** ISO seconds timestamp — OpenAI-compatible. */
367
+ created: number;
368
+ /** One entry per generated image. */
369
+ data: ImageGenerationDataEntry[];
370
+ /** The model that actually served the request (same as `_builderforce.resolvedModel`). */
371
+ model?: string;
372
+ _builderforce?: {
373
+ /** The model the gateway dispatched against. */
374
+ resolvedModel?: string;
375
+ /** Vendor that owns the resolved model — `'together' | 'fluxapi'`. */
376
+ resolvedVendor?: string;
377
+ /** How many vendor retries happened before the resolved vendor succeeded. */
378
+ retries?: number;
379
+ /** Per-attempt breakdown, present only when `retries > 0`. */
380
+ failovers?: FailoverEvent[];
381
+ product?: string;
382
+ effectivePlan?: string;
383
+ /** True when superadmin premium-override is active for this tenant. */
384
+ premium?: boolean;
385
+ /** Echo of `request.useCase`. */
386
+ useCase?: string;
387
+ /** Echo of `request.metadata`. */
388
+ metadata?: Record<string, string>;
389
+ /** Mirror of the `x-request-id` response header. */
390
+ requestId?: string;
391
+ };
392
+ [key: string]: unknown;
393
+ }
304
394
 
305
395
  declare class BuilderforceApiError extends Error {
306
396
  readonly status: number;
@@ -316,9 +406,38 @@ declare class BuilderforceApiError extends Error {
316
406
  readonly terminal?: boolean;
317
407
  /** Seconds the consumer should wait before retrying — server-supplied. */
318
408
  readonly retryAfter?: number;
409
+ /**
410
+ * Cascade attempts that failed before this error was returned — populated
411
+ * when the gateway returns `429 cascade_exhausted` with a `details.failovers`
412
+ * array. Each entry includes the vendor that owns the model so callers can
413
+ * detect single-vendor saturation (e.g. all attempts on `openrouter`).
414
+ */
415
+ readonly failovers?: FailoverEvent[];
416
+ /**
417
+ * Upstream vendor the gateway dispatched against (`'openrouter' | 'cerebras'
418
+ * | 'nvidia' | 'ollama' | 'googleai' | …`). Set on every error where the
419
+ * gateway selected an upstream — including single-attempt failures that
420
+ * never ran a cascade (timeouts, single-vendor 429s, `model_unavailable`).
421
+ *
422
+ * Unset only for pre-dispatch errors where no vendor was ever selected:
423
+ * `401`/`403` auth failures, `400` validation failures, `409` idempotent
424
+ * replay, and tenant-cap 429s (`plan_token_limit_exceeded`,
425
+ * `claw_token_limit_exceeded`) — those caps are per-tenant, not per-model.
426
+ *
427
+ * Sourced from the gateway's catalog lookup so consumers never have to
428
+ * parse the model id to recover vendor identity.
429
+ */
430
+ readonly vendor?: string;
431
+ /**
432
+ * Model id the gateway dispatched against — set whenever `vendor` is set.
433
+ * Pair with `vendor` for per-attempt observability without prefix parsing.
434
+ */
435
+ readonly model?: string;
319
436
  constructor(message: string, status: number, code?: string, details?: unknown, requestId?: string, extras?: {
320
437
  terminal?: boolean;
321
438
  retryAfter?: number;
439
+ vendor?: string;
440
+ model?: string;
322
441
  });
323
442
  }
324
443
  interface HttpClientOptions {
@@ -385,6 +504,25 @@ declare class EmbeddingsApi {
385
504
  create(params: EmbeddingsCreateParams): Promise<EmbeddingsResponse>;
386
505
  }
387
506
 
507
+ /**
508
+ * `client.images.generate({ prompt, ... })` — OpenAI-compatible image generation
509
+ * routed through the Builderforce gateway. The gateway cascades free Together
510
+ * vendors → premium FluxAPI fallback so callers always see a successful
511
+ * response unless every upstream is saturated. Read
512
+ * `_builderforce.resolvedModel` / `resolvedVendor` to detect which vendor
513
+ * served the request.
514
+ *
515
+ * Image generations are billed against the tenant's daily token budget at a
516
+ * flat per-image rate (currently ~1000 tokens/image — deliberately conservative).
517
+ * Hitting the cap returns the same `429 plan_token_limit_exceeded` envelope
518
+ * as chat — caller code that already handles that path needs no changes.
519
+ */
520
+ declare class ImagesApi {
521
+ private readonly http;
522
+ constructor(http: HttpClient);
523
+ generate(params: ImageGenerationCreateParams): Promise<ImageGenerationResponse>;
524
+ }
525
+
388
526
  declare class ModelsApi {
389
527
  private readonly http;
390
528
  constructor(http: HttpClient);
@@ -410,9 +548,10 @@ declare class BuilderforceClient {
410
548
  completions: ChatCompletionsApi;
411
549
  };
412
550
  readonly embeddings: EmbeddingsApi;
551
+ readonly images: ImagesApi;
413
552
  readonly models: ModelsApi;
414
553
  readonly usage: UsageApi;
415
554
  constructor(options: BuilderforceClientOptions);
416
555
  }
417
556
 
418
- export { BuilderforceApiError, BuilderforceClient, type BuilderforceClientOptions, type ChatCompletionChunk, type ChatCompletionCreateParams, type ChatCompletionResponse, ChatCompletionStream, type ChatMessage, type ChatRole, type ContentPart, type EmbeddingObject, EmbeddingsApi, type EmbeddingsCreateParams, type EmbeddingsResponse, type FunctionDefinition, type ImageUrlContentPart, type JsonSchemaSpec, type ModelsListResponse, type PerCallOptions, type ResponseFormat, type TextContentPart, type ToolCall, type ToolCallDelta, type ToolCallFunction, type ToolChoice, type ToolSpec, type UsageByDay, type UsageByModel, type UsageByUser, type UsageGetParams, type UsageResponse };
557
+ export { BuilderforceApiError, BuilderforceClient, type BuilderforceClientOptions, type ChatCompletionChunk, type ChatCompletionCreateParams, type ChatCompletionResponse, ChatCompletionStream, type ChatMessage, type ChatRole, type ContentPart, type EmbeddingObject, EmbeddingsApi, type EmbeddingsCreateParams, type EmbeddingsResponse, type FailoverEvent, type FunctionDefinition, type ImageGenerationCreateParams, type ImageGenerationDataEntry, type ImageGenerationResponse, type ImageUrlContentPart, ImagesApi, type JsonSchemaSpec, type ModelsListResponse, type PerCallOptions, type ResponseFormat, type TextContentPart, type ToolCall, type ToolCallDelta, type ToolCallFunction, type ToolChoice, type ToolSpec, type UsageByDay, type UsageByModel, type UsageByUser, type UsageGetParams, type UsageResponse };
package/dist/index.mjs CHANGED
@@ -102,6 +102,31 @@ var EmbeddingsApi = class {
102
102
  }
103
103
  };
104
104
 
105
+ // src/application/ImagesApi.ts
106
+ function splitTransportOptions3(params) {
107
+ const { timeoutMs, signal, idempotencyKey, ...rest } = params;
108
+ const headers = {};
109
+ if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;
110
+ return {
111
+ body: rest,
112
+ request: {
113
+ timeoutMs,
114
+ signal,
115
+ ...Object.keys(headers).length > 0 ? { headers } : {}
116
+ }
117
+ };
118
+ }
119
+ var ImagesApi = class {
120
+ http;
121
+ constructor(http) {
122
+ this.http = http;
123
+ }
124
+ generate(params) {
125
+ const { body, request } = splitTransportOptions3(params);
126
+ return this.http.postJson("/llm/v1/images/generations", body, request);
127
+ }
128
+ };
129
+
105
130
  // src/application/ModelsApi.ts
106
131
  var ModelsApi = class {
107
132
  http;
@@ -140,6 +165,33 @@ var BuilderforceApiError = class extends Error {
140
165
  terminal;
141
166
  /** Seconds the consumer should wait before retrying — server-supplied. */
142
167
  retryAfter;
168
+ /**
169
+ * Cascade attempts that failed before this error was returned — populated
170
+ * when the gateway returns `429 cascade_exhausted` with a `details.failovers`
171
+ * array. Each entry includes the vendor that owns the model so callers can
172
+ * detect single-vendor saturation (e.g. all attempts on `openrouter`).
173
+ */
174
+ failovers;
175
+ /**
176
+ * Upstream vendor the gateway dispatched against (`'openrouter' | 'cerebras'
177
+ * | 'nvidia' | 'ollama' | 'googleai' | …`). Set on every error where the
178
+ * gateway selected an upstream — including single-attempt failures that
179
+ * never ran a cascade (timeouts, single-vendor 429s, `model_unavailable`).
180
+ *
181
+ * Unset only for pre-dispatch errors where no vendor was ever selected:
182
+ * `401`/`403` auth failures, `400` validation failures, `409` idempotent
183
+ * replay, and tenant-cap 429s (`plan_token_limit_exceeded`,
184
+ * `claw_token_limit_exceeded`) — those caps are per-tenant, not per-model.
185
+ *
186
+ * Sourced from the gateway's catalog lookup so consumers never have to
187
+ * parse the model id to recover vendor identity.
188
+ */
189
+ vendor;
190
+ /**
191
+ * Model id the gateway dispatched against — set whenever `vendor` is set.
192
+ * Pair with `vendor` for per-attempt observability without prefix parsing.
193
+ */
194
+ model;
143
195
  constructor(message, status, code, details, requestId, extras) {
144
196
  super(message);
145
197
  this.name = "BuilderforceApiError";
@@ -149,6 +201,23 @@ var BuilderforceApiError = class extends Error {
149
201
  this.requestId = requestId;
150
202
  this.terminal = extras?.terminal;
151
203
  this.retryAfter = extras?.retryAfter;
204
+ this.vendor = extras?.vendor;
205
+ this.model = extras?.model;
206
+ if (details && typeof details === "object") {
207
+ const f = details.failovers;
208
+ if (Array.isArray(f)) {
209
+ const cleaned = [];
210
+ for (const entry of f) {
211
+ if (entry && typeof entry === "object") {
212
+ const e = entry;
213
+ if (typeof e.model === "string" && typeof e.vendor === "string" && typeof e.code === "number") {
214
+ cleaned.push({ model: e.model, vendor: e.vendor, code: e.code });
215
+ }
216
+ }
217
+ }
218
+ if (cleaned.length > 0) this.failovers = cleaned;
219
+ }
220
+ }
152
221
  }
153
222
  };
154
223
  var HttpClient = class {
@@ -161,7 +230,7 @@ var HttpClient = class {
161
230
  this.baseUrl = options.baseUrl.replace(/\/$/, "");
162
231
  const fetchImpl = options.fetchFn ?? fetch;
163
232
  this.fetchFn = fetchImpl.bind(globalThis);
164
- this.defaultTimeoutMs = options.timeoutMs ?? 6e4;
233
+ this.defaultTimeoutMs = options.timeoutMs ?? 18e4;
165
234
  }
166
235
  async getJson(path, options) {
167
236
  const res = await this.fetchWithTimeout(`${this.baseUrl}${path}`, {
@@ -232,18 +301,25 @@ var HttpClient = class {
232
301
  const headerRetryAfter = parsePositiveInt(res.headers.get("retry-after"));
233
302
  try {
234
303
  const payload = await res.json();
235
- const isNested = payload !== null && typeof payload === "object" && payload.success === false && typeof payload.error === "object" && payload.error !== null;
236
- const inner = isNested ? payload.error : payload;
304
+ const errorObj = payload !== null && typeof payload === "object" && typeof payload.error === "object" && payload.error !== null ? payload.error : null;
305
+ const isWrapped = payload !== null && typeof payload === "object" && payload.success === false && errorObj !== null;
306
+ const inner = isWrapped || errorObj !== null ? errorObj : payload;
237
307
  const message = typeof inner?.message === "string" && inner.message || typeof inner?.error === "string" && inner.error || fallback;
308
+ const code = typeof inner?.code === "number" ? String(inner.code) : inner?.code;
309
+ const detailsObj = inner?.details && typeof inner.details === "object" ? inner.details : null;
310
+ const vendor = typeof inner?.vendor === "string" ? inner.vendor : typeof detailsObj?.vendor === "string" ? detailsObj.vendor : void 0;
311
+ const model = typeof inner?.model === "string" ? inner.model : typeof detailsObj?.model === "string" ? detailsObj.model : typeof detailsObj?.requestedModel === "string" ? detailsObj.requestedModel : void 0;
238
312
  return new BuilderforceApiError(
239
313
  message,
240
314
  res.status,
241
- inner?.code,
315
+ code,
242
316
  inner?.details,
243
317
  requestId,
244
318
  {
245
319
  terminal: inner?.terminal,
246
- retryAfter: headerRetryAfter ?? inner?.retryAfter
320
+ retryAfter: headerRetryAfter ?? inner?.retryAfter,
321
+ vendor,
322
+ model
247
323
  }
248
324
  );
249
325
  } catch {
@@ -286,6 +362,7 @@ function combineSignals(...signals) {
286
362
  var BuilderforceClient = class {
287
363
  chat;
288
364
  embeddings;
365
+ images;
289
366
  models;
290
367
  usage;
291
368
  constructor(options) {
@@ -307,6 +384,7 @@ var BuilderforceClient = class {
307
384
  completions: new ChatCompletionsApi(http)
308
385
  };
309
386
  this.embeddings = new EmbeddingsApi(http);
387
+ this.images = new ImagesApi(http);
310
388
  this.models = new ModelsApi(http);
311
389
  this.usage = new UsageApi(http);
312
390
  }
@@ -315,6 +393,7 @@ export {
315
393
  BuilderforceApiError,
316
394
  BuilderforceClient,
317
395
  ChatCompletionStream,
318
- EmbeddingsApi
396
+ EmbeddingsApi,
397
+ ImagesApi
319
398
  };
320
399
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/infrastructure/sse.ts","../src/application/ChatCompletionsApi.ts","../src/application/EmbeddingsApi.ts","../src/application/ModelsApi.ts","../src/application/UsageApi.ts","../src/infrastructure/httpClient.ts","../src/BuilderforceClient.ts"],"sourcesContent":["export async function* parseSseJson<T>(\n stream: ReadableStream<Uint8Array>,\n): AsyncGenerator<T, void, unknown> {\n const reader = stream.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() ?? '';\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed.startsWith('data: ')) continue;\n\n const data = trimmed.slice(6).trim();\n if (data === '[DONE]') return;\n\n try {\n yield JSON.parse(data) as T;\n } catch {\n // Skip malformed chunks instead of breaking the stream.\n }\n }\n }\n}\n","import type { ChatCompletionChunk, ChatCompletionCreateParams, ChatCompletionResponse } from '../domain/types';\nimport { HttpClient, type RequestOptions } from '../infrastructure/httpClient';\nimport { parseSseJson } from '../infrastructure/sse';\n\nexport class ChatCompletionStream implements AsyncIterable<ChatCompletionChunk> {\n private readonly stream: ReadableStream<Uint8Array>;\n\n constructor(stream: ReadableStream<Uint8Array>) {\n this.stream = stream;\n }\n\n [Symbol.asyncIterator](): AsyncIterator<ChatCompletionChunk, void, unknown> {\n return parseSseJson<ChatCompletionChunk>(this.stream);\n }\n\n async toText(): Promise<string> {\n let full = '';\n for await (const chunk of this) {\n const delta = chunk.choices?.[0]?.delta?.content;\n if (typeof delta === 'string') {\n full += delta;\n }\n }\n return full;\n }\n}\n\n/**\n * Pull SDK-level transport options (timeout, signal, idempotency key) out of\n * the params object so they don't get JSON-serialized into the request body.\n * Returns the request options AND the cleaned-up body.\n */\nfunction splitTransportOptions(params: ChatCompletionCreateParams): {\n body: Record<string, unknown>;\n request: RequestOptions;\n} {\n const { timeoutMs, signal, idempotencyKey, ...rest } = params;\n const headers: Record<string, string> = {};\n if (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey;\n return {\n body: rest as unknown as Record<string, unknown>,\n request: {\n timeoutMs,\n signal,\n ...(Object.keys(headers).length > 0 ? { headers } : {}),\n },\n };\n}\n\nexport class ChatCompletionsApi {\n private readonly http: HttpClient;\n\n constructor(http: HttpClient) {\n this.http = http;\n }\n\n async create(params: ChatCompletionCreateParams & { stream: true }): Promise<ChatCompletionStream>;\n async create(params: ChatCompletionCreateParams & { stream?: false | undefined }): Promise<ChatCompletionResponse>;\n async create(\n params: ChatCompletionCreateParams,\n ): Promise<ChatCompletionResponse | ChatCompletionStream> {\n const { body, request } = splitTransportOptions(params);\n\n if (params.stream) {\n const response = await this.http.postRaw('/llm/v1/chat/completions', body, request);\n if (!response.body) {\n throw new Error('Streaming response body is missing');\n }\n return new ChatCompletionStream(response.body);\n }\n\n return this.http.postJson<ChatCompletionResponse>('/llm/v1/chat/completions', body, request);\n }\n}\n","import type { EmbeddingsCreateParams, EmbeddingsResponse } from '../domain/types';\nimport { HttpClient, type RequestOptions } from '../infrastructure/httpClient';\n\n/**\n * Pull SDK-level transport options out of the params so they don't ride\n * along inside the JSON body. Same shape as ChatCompletionsApi (DRY pattern).\n */\nfunction splitTransportOptions(params: EmbeddingsCreateParams): {\n body: Record<string, unknown>;\n request: RequestOptions;\n} {\n const { timeoutMs, signal, idempotencyKey, ...rest } = params;\n const headers: Record<string, string> = {};\n if (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey;\n return {\n body: rest as unknown as Record<string, unknown>,\n request: {\n timeoutMs,\n signal,\n ...(Object.keys(headers).length > 0 ? { headers } : {}),\n },\n };\n}\n\nexport class EmbeddingsApi {\n private readonly http: HttpClient;\n\n constructor(http: HttpClient) {\n this.http = http;\n }\n\n /**\n * Create one or more text embeddings. Wired to OpenRouter (default model\n * `nvidia/llama-nemotron-embed-vl-1b-v2:free`). Override via `model`.\n */\n create(params: EmbeddingsCreateParams): Promise<EmbeddingsResponse> {\n const { body, request } = splitTransportOptions(params);\n return this.http.postJson<EmbeddingsResponse>('/llm/v1/embeddings', body, request);\n }\n}\n","import type { ModelsListResponse } from '../domain/types';\nimport { HttpClient } from '../infrastructure/httpClient';\n\nexport class ModelsApi {\n private readonly http: HttpClient;\n\n constructor(http: HttpClient) {\n this.http = http;\n }\n\n list(): Promise<ModelsListResponse> {\n return this.http.getJson<ModelsListResponse>('/llm/v1/models');\n }\n}\n","import type { UsageGetParams, UsageResponse } from '../domain/types';\nimport { HttpClient } from '../infrastructure/httpClient';\n\nexport class UsageApi {\n private readonly http: HttpClient;\n\n constructor(http: HttpClient) {\n this.http = http;\n }\n\n get(params: UsageGetParams = {}): Promise<UsageResponse> {\n const query = typeof params.days === 'number' ? `?days=${encodeURIComponent(String(params.days))}` : '';\n return this.http.getJson<UsageResponse>(`/llm/v1/usage${query}`);\n }\n}\n","export class BuilderforceApiError extends Error {\n public readonly status: number;\n public readonly code?: string;\n public readonly details?: unknown;\n public readonly requestId?: string;\n /**\n * `true` when the gateway has signalled this error will not resolve by\n * retrying on a different model — e.g. plan or per-claw daily token cap\n * exhausted (those caps are per-tenant, not per-model). Consumer-side\n * fallback chains should short-circuit when this is set.\n */\n public readonly terminal?: boolean;\n /** Seconds the consumer should wait before retrying — server-supplied. */\n public readonly retryAfter?: number;\n\n constructor(\n message: string,\n status: number,\n code?: string,\n details?: unknown,\n requestId?: string,\n extras?: { terminal?: boolean; retryAfter?: number },\n ) {\n super(message);\n this.name = 'BuilderforceApiError';\n this.status = status;\n this.code = code;\n this.details = details;\n this.requestId = requestId;\n this.terminal = extras?.terminal;\n this.retryAfter = extras?.retryAfter;\n }\n}\n\nexport interface HttpClientOptions {\n apiKey: string;\n baseUrl: string;\n fetchFn?: typeof fetch;\n /** Default per-request timeout in ms. Overridable per call. */\n timeoutMs?: number;\n}\n\n/** Per-request overrides — passed by the API layer, not by SDK consumers directly. */\nexport interface RequestOptions {\n /** Override the client default timeout for just this request. */\n timeoutMs?: number;\n /** Caller-provided AbortSignal. Linked together with the SDK's internal timeout\n * signal — whichever fires first aborts the request. */\n signal?: AbortSignal;\n /** Extra headers to merge in (e.g. `Idempotency-Key`). */\n headers?: Record<string, string>;\n}\n\nexport class HttpClient {\n private readonly apiKey: string;\n private readonly baseUrl: string;\n private readonly fetchFn: typeof fetch;\n private readonly defaultTimeoutMs: number;\n\n constructor(options: HttpClientOptions) {\n this.apiKey = options.apiKey;\n this.baseUrl = options.baseUrl.replace(/\\/$/, '');\n // Bind to `globalThis` so calling via `this.fetchFn(...)` doesn't trip\n // Cloudflare Workers' \"Illegal invocation\" check — the platform's fetch\n // requires the global receiver, not an instance method `this`. Affects\n // any environment that ships a strict-receiver fetch (Workers, Bun, etc.)\n // and is harmless on Node + browsers.\n const fetchImpl = options.fetchFn ?? fetch;\n this.fetchFn = fetchImpl.bind(globalThis);\n this.defaultTimeoutMs = options.timeoutMs ?? 60_000;\n }\n\n async getJson<T>(path: string, options?: RequestOptions): Promise<T> {\n const res = await this.fetchWithTimeout(`${this.baseUrl}${path}`, {\n method: 'GET',\n headers: this.mergeHeaders(options),\n }, options);\n return this.parseJsonResponse<T>(res);\n }\n\n async postJson<T>(path: string, body: unknown, options?: RequestOptions): Promise<T> {\n const res = await this.fetchWithTimeout(`${this.baseUrl}${path}`, {\n method: 'POST',\n headers: this.mergeHeaders(options, { 'Content-Type': 'application/json' }),\n body: JSON.stringify(body),\n }, options);\n return this.parseJsonResponse<T>(res);\n }\n\n async postRaw(path: string, body: unknown, options?: RequestOptions): Promise<Response> {\n const res = await this.fetchWithTimeout(`${this.baseUrl}${path}`, {\n method: 'POST',\n headers: this.mergeHeaders(options, { 'Content-Type': 'application/json' }),\n body: JSON.stringify(body),\n }, options);\n if (!res.ok) {\n throw await this.toApiError(res);\n }\n return res;\n }\n\n private mergeHeaders(options?: RequestOptions, base?: Record<string, string>): Record<string, string> {\n return {\n Authorization: `Bearer ${this.apiKey}`,\n ...(base ?? {}),\n ...(options?.headers ?? {}),\n };\n }\n\n /**\n * Wrap a fetch in a combined abort signal: an internal timeout AND any\n * caller-provided signal. Either firing aborts the request. Single source of\n * abort plumbing — every method routes through here (DRY).\n */\n private async fetchWithTimeout(\n input: RequestInfo | URL,\n init: RequestInit,\n options?: RequestOptions,\n ): Promise<Response> {\n const timeoutMs = options?.timeoutMs ?? this.defaultTimeoutMs;\n const timeoutCtl = new AbortController();\n const timer = setTimeout(() => timeoutCtl.abort(), timeoutMs);\n\n // Combine internal timeout signal + caller signal. Native AbortSignal.any\n // (Node 20+ / modern Workers) is preferred; fall back to manual linking.\n const signal = combineSignals(timeoutCtl.signal, options?.signal);\n\n try {\n return await this.fetchFn(input, { ...init, signal });\n } catch (error) {\n if (timeoutCtl.signal.aborted) {\n throw new BuilderforceApiError(`Request timed out after ${timeoutMs}ms`, 408, 'timeout');\n }\n if (options?.signal?.aborted) {\n throw new BuilderforceApiError('Request aborted by caller', 499, 'aborted');\n }\n throw error;\n } finally {\n clearTimeout(timer);\n }\n }\n\n private async parseJsonResponse<T>(res: Response): Promise<T> {\n if (!res.ok) {\n throw await this.toApiError(res);\n }\n return res.json() as Promise<T>;\n }\n\n private async toApiError(res: Response): Promise<BuilderforceApiError> {\n const fallback = `Request failed (${res.status})`;\n const requestId = res.headers.get('x-request-id') ?? undefined;\n\n // Prefer server-supplied `Retry-After` header (seconds) when present; the\n // body's `retryAfter` is a fallback for environments that strip headers.\n const headerRetryAfter = parsePositiveInt(res.headers.get('retry-after'));\n\n try {\n const payload = await res.json() as Record<string, unknown> | null;\n\n // Two envelope shapes are in the wild:\n //\n // Flat — { error: \"msg\", code, details, terminal?, retryAfter? }\n // (gateway's documented shape — plan_token_limit_exceeded etc.)\n // Nested — { success: false, error: { code, message, details } }\n // (consumer-side wrappers around the gateway, e.g. some\n // tenant proxies emit AI_RATE_LIMITED / AI_UNAVAILABLE\n // envelopes that re-wrap the upstream error)\n //\n // Detect via the `success: false` discriminator and unwrap when nested\n // so `error.code` / `error.message` / `error.details` always populate.\n const isNested =\n payload !== null\n && typeof payload === 'object'\n && payload.success === false\n && typeof payload.error === 'object'\n && payload.error !== null;\n\n const inner = (isNested ? payload.error : payload) as {\n error?: string;\n message?: string;\n code?: string;\n details?: unknown;\n terminal?: boolean;\n retryAfter?: number;\n } | null;\n\n const message =\n (typeof inner?.message === 'string' && inner.message)\n || (typeof inner?.error === 'string' && inner.error)\n || fallback;\n\n return new BuilderforceApiError(\n message,\n res.status,\n inner?.code,\n inner?.details,\n requestId,\n {\n terminal: inner?.terminal,\n retryAfter: headerRetryAfter ?? inner?.retryAfter,\n },\n );\n } catch {\n const text = await res.text().catch(() => '');\n return new BuilderforceApiError(\n text || fallback,\n res.status,\n undefined,\n undefined,\n requestId,\n headerRetryAfter !== undefined ? { retryAfter: headerRetryAfter } : undefined,\n );\n }\n }\n}\n\nfunction parsePositiveInt(s: string | null): number | undefined {\n if (s == null) return undefined;\n const n = Number(s);\n return Number.isFinite(n) && n >= 0 ? Math.floor(n) : undefined;\n}\n\n/**\n * Combine multiple AbortSignals into one. Uses native `AbortSignal.any` when\n * available (Node 20+, modern Workers); falls back to manual event linking.\n */\nfunction combineSignals(...signals: Array<AbortSignal | undefined>): AbortSignal {\n const live = signals.filter((s): s is AbortSignal => s !== undefined);\n if (live.length === 1) return live[0]!;\n\n const anyImpl = (AbortSignal as unknown as { any?: (signals: AbortSignal[]) => AbortSignal }).any;\n if (typeof anyImpl === 'function') {\n return anyImpl(live);\n }\n\n const ctl = new AbortController();\n for (const s of live) {\n if (s.aborted) { ctl.abort(s.reason); break; }\n s.addEventListener('abort', () => ctl.abort(s.reason), { once: true });\n }\n return ctl.signal;\n}\n","import { ChatCompletionsApi } from './application/ChatCompletionsApi';\nimport { EmbeddingsApi } from './application/EmbeddingsApi';\nimport { ModelsApi } from './application/ModelsApi';\nimport { UsageApi } from './application/UsageApi';\nimport { BuilderforceApiError, HttpClient } from './infrastructure/httpClient';\n\nexport interface BuilderforceClientOptions {\n apiKey: string;\n baseUrl?: string;\n fetch?: typeof fetch;\n /** Default request timeout in ms (default 60_000). Per-call override available\n * via `chat.completions.create({ timeoutMs })` and `embeddings.create({ timeoutMs })`. */\n timeoutMs?: number;\n}\n\nexport class BuilderforceClient {\n public readonly chat: {\n completions: ChatCompletionsApi;\n };\n public readonly embeddings: EmbeddingsApi;\n public readonly models: ModelsApi;\n public readonly usage: UsageApi;\n\n constructor(options: BuilderforceClientOptions) {\n const apiKey = options.apiKey?.trim();\n if (!apiKey) {\n throw new BuilderforceApiError(\n 'BuilderforceClient requires a non-empty apiKey',\n 400,\n 'missing_api_key',\n );\n }\n\n const http = new HttpClient({\n apiKey,\n baseUrl: options.baseUrl ?? 'https://api.builderforce.ai',\n fetchFn: options.fetch,\n timeoutMs: options.timeoutMs,\n });\n\n this.chat = {\n completions: new ChatCompletionsApi(http),\n };\n this.embeddings = new EmbeddingsApi(http);\n this.models = new ModelsApi(http);\n this.usage = new UsageApi(http);\n }\n}\n"],"mappings":"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","names":["splitTransportOptions"]}
1
+ {"version":3,"sources":["../src/infrastructure/sse.ts","../src/application/ChatCompletionsApi.ts","../src/application/EmbeddingsApi.ts","../src/application/ImagesApi.ts","../src/application/ModelsApi.ts","../src/application/UsageApi.ts","../src/infrastructure/httpClient.ts","../src/BuilderforceClient.ts"],"sourcesContent":["export async function* parseSseJson<T>(\n stream: ReadableStream<Uint8Array>,\n): AsyncGenerator<T, void, unknown> {\n const reader = stream.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() ?? '';\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed.startsWith('data: ')) continue;\n\n const data = trimmed.slice(6).trim();\n if (data === '[DONE]') return;\n\n try {\n yield JSON.parse(data) as T;\n } catch {\n // Skip malformed chunks instead of breaking the stream.\n }\n }\n }\n}\n","import type { ChatCompletionChunk, ChatCompletionCreateParams, ChatCompletionResponse } from '../domain/types';\nimport { HttpClient, type RequestOptions } from '../infrastructure/httpClient';\nimport { parseSseJson } from '../infrastructure/sse';\n\nexport class ChatCompletionStream implements AsyncIterable<ChatCompletionChunk> {\n private readonly stream: ReadableStream<Uint8Array>;\n\n constructor(stream: ReadableStream<Uint8Array>) {\n this.stream = stream;\n }\n\n [Symbol.asyncIterator](): AsyncIterator<ChatCompletionChunk, void, unknown> {\n return parseSseJson<ChatCompletionChunk>(this.stream);\n }\n\n async toText(): Promise<string> {\n let full = '';\n for await (const chunk of this) {\n const delta = chunk.choices?.[0]?.delta?.content;\n if (typeof delta === 'string') {\n full += delta;\n }\n }\n return full;\n }\n}\n\n/**\n * Pull SDK-level transport options (timeout, signal, idempotency key) out of\n * the params object so they don't get JSON-serialized into the request body.\n * Returns the request options AND the cleaned-up body.\n */\nfunction splitTransportOptions(params: ChatCompletionCreateParams): {\n body: Record<string, unknown>;\n request: RequestOptions;\n} {\n const { timeoutMs, signal, idempotencyKey, ...rest } = params;\n const headers: Record<string, string> = {};\n if (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey;\n return {\n body: rest as unknown as Record<string, unknown>,\n request: {\n timeoutMs,\n signal,\n ...(Object.keys(headers).length > 0 ? { headers } : {}),\n },\n };\n}\n\nexport class ChatCompletionsApi {\n private readonly http: HttpClient;\n\n constructor(http: HttpClient) {\n this.http = http;\n }\n\n async create(params: ChatCompletionCreateParams & { stream: true }): Promise<ChatCompletionStream>;\n async create(params: ChatCompletionCreateParams & { stream?: false | undefined }): Promise<ChatCompletionResponse>;\n async create(\n params: ChatCompletionCreateParams,\n ): Promise<ChatCompletionResponse | ChatCompletionStream> {\n const { body, request } = splitTransportOptions(params);\n\n if (params.stream) {\n const response = await this.http.postRaw('/llm/v1/chat/completions', body, request);\n if (!response.body) {\n throw new Error('Streaming response body is missing');\n }\n return new ChatCompletionStream(response.body);\n }\n\n return this.http.postJson<ChatCompletionResponse>('/llm/v1/chat/completions', body, request);\n }\n}\n","import type { EmbeddingsCreateParams, EmbeddingsResponse } from '../domain/types';\nimport { HttpClient, type RequestOptions } from '../infrastructure/httpClient';\n\n/**\n * Pull SDK-level transport options out of the params so they don't ride\n * along inside the JSON body. Same shape as ChatCompletionsApi (DRY pattern).\n */\nfunction splitTransportOptions(params: EmbeddingsCreateParams): {\n body: Record<string, unknown>;\n request: RequestOptions;\n} {\n const { timeoutMs, signal, idempotencyKey, ...rest } = params;\n const headers: Record<string, string> = {};\n if (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey;\n return {\n body: rest as unknown as Record<string, unknown>,\n request: {\n timeoutMs,\n signal,\n ...(Object.keys(headers).length > 0 ? { headers } : {}),\n },\n };\n}\n\nexport class EmbeddingsApi {\n private readonly http: HttpClient;\n\n constructor(http: HttpClient) {\n this.http = http;\n }\n\n /**\n * Create one or more text embeddings. Wired to OpenRouter (default model\n * `nvidia/llama-nemotron-embed-vl-1b-v2:free`). Override via `model`.\n */\n create(params: EmbeddingsCreateParams): Promise<EmbeddingsResponse> {\n const { body, request } = splitTransportOptions(params);\n return this.http.postJson<EmbeddingsResponse>('/llm/v1/embeddings', body, request);\n }\n}\n","import type { ImageGenerationCreateParams, ImageGenerationResponse } from '../domain/types';\nimport { HttpClient, type RequestOptions } from '../infrastructure/httpClient';\n\n/**\n * Pull SDK-level transport options out of the params so they don't ride\n * along inside the JSON body. Same shape as ChatCompletionsApi / EmbeddingsApi\n * (DRY pattern — every API class uses the same splitter).\n */\nfunction splitTransportOptions(params: ImageGenerationCreateParams): {\n body: Record<string, unknown>;\n request: RequestOptions;\n} {\n const { timeoutMs, signal, idempotencyKey, ...rest } = params;\n const headers: Record<string, string> = {};\n if (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey;\n return {\n body: rest as unknown as Record<string, unknown>,\n request: {\n timeoutMs,\n signal,\n ...(Object.keys(headers).length > 0 ? { headers } : {}),\n },\n };\n}\n\n/**\n * `client.images.generate({ prompt, ... })` — OpenAI-compatible image generation\n * routed through the Builderforce gateway. The gateway cascades free Together\n * vendors → premium FluxAPI fallback so callers always see a successful\n * response unless every upstream is saturated. Read\n * `_builderforce.resolvedModel` / `resolvedVendor` to detect which vendor\n * served the request.\n *\n * Image generations are billed against the tenant's daily token budget at a\n * flat per-image rate (currently ~1000 tokens/image — deliberately conservative).\n * Hitting the cap returns the same `429 plan_token_limit_exceeded` envelope\n * as chat — caller code that already handles that path needs no changes.\n */\nexport class ImagesApi {\n private readonly http: HttpClient;\n\n constructor(http: HttpClient) {\n this.http = http;\n }\n\n generate(params: ImageGenerationCreateParams): Promise<ImageGenerationResponse> {\n const { body, request } = splitTransportOptions(params);\n return this.http.postJson<ImageGenerationResponse>('/llm/v1/images/generations', body, request);\n }\n}\n","import type { ModelsListResponse } from '../domain/types';\nimport { HttpClient } from '../infrastructure/httpClient';\n\nexport class ModelsApi {\n private readonly http: HttpClient;\n\n constructor(http: HttpClient) {\n this.http = http;\n }\n\n list(): Promise<ModelsListResponse> {\n return this.http.getJson<ModelsListResponse>('/llm/v1/models');\n }\n}\n","import type { UsageGetParams, UsageResponse } from '../domain/types';\nimport { HttpClient } from '../infrastructure/httpClient';\n\nexport class UsageApi {\n private readonly http: HttpClient;\n\n constructor(http: HttpClient) {\n this.http = http;\n }\n\n get(params: UsageGetParams = {}): Promise<UsageResponse> {\n const query = typeof params.days === 'number' ? `?days=${encodeURIComponent(String(params.days))}` : '';\n return this.http.getJson<UsageResponse>(`/llm/v1/usage${query}`);\n }\n}\n","import type { FailoverEvent } from '../domain/types';\n\nexport class BuilderforceApiError extends Error {\n public readonly status: number;\n public readonly code?: string;\n public readonly details?: unknown;\n public readonly requestId?: string;\n /**\n * `true` when the gateway has signalled this error will not resolve by\n * retrying on a different model — e.g. plan or per-claw daily token cap\n * exhausted (those caps are per-tenant, not per-model). Consumer-side\n * fallback chains should short-circuit when this is set.\n */\n public readonly terminal?: boolean;\n /** Seconds the consumer should wait before retrying — server-supplied. */\n public readonly retryAfter?: number;\n /**\n * Cascade attempts that failed before this error was returned — populated\n * when the gateway returns `429 cascade_exhausted` with a `details.failovers`\n * array. Each entry includes the vendor that owns the model so callers can\n * detect single-vendor saturation (e.g. all attempts on `openrouter`).\n */\n public readonly failovers?: FailoverEvent[];\n /**\n * Upstream vendor the gateway dispatched against (`'openrouter' | 'cerebras'\n * | 'nvidia' | 'ollama' | 'googleai' | …`). Set on every error where the\n * gateway selected an upstream — including single-attempt failures that\n * never ran a cascade (timeouts, single-vendor 429s, `model_unavailable`).\n *\n * Unset only for pre-dispatch errors where no vendor was ever selected:\n * `401`/`403` auth failures, `400` validation failures, `409` idempotent\n * replay, and tenant-cap 429s (`plan_token_limit_exceeded`,\n * `claw_token_limit_exceeded`) — those caps are per-tenant, not per-model.\n *\n * Sourced from the gateway's catalog lookup so consumers never have to\n * parse the model id to recover vendor identity.\n */\n public readonly vendor?: string;\n /**\n * Model id the gateway dispatched against — set whenever `vendor` is set.\n * Pair with `vendor` for per-attempt observability without prefix parsing.\n */\n public readonly model?: string;\n\n constructor(\n message: string,\n status: number,\n code?: string,\n details?: unknown,\n requestId?: string,\n extras?: { terminal?: boolean; retryAfter?: number; vendor?: string; model?: string },\n ) {\n super(message);\n this.name = 'BuilderforceApiError';\n this.status = status;\n this.code = code;\n this.details = details;\n this.requestId = requestId;\n this.terminal = extras?.terminal;\n this.retryAfter = extras?.retryAfter;\n this.vendor = extras?.vendor;\n this.model = extras?.model;\n // Pull typed failovers out of `details.failovers` when the gateway\n // supplied them. Validation is light — drop entries missing required\n // fields so consumers never get a partially-populated row.\n if (details && typeof details === 'object') {\n const f = (details as { failovers?: unknown }).failovers;\n if (Array.isArray(f)) {\n const cleaned: FailoverEvent[] = [];\n for (const entry of f) {\n if (entry && typeof entry === 'object') {\n const e = entry as { model?: unknown; vendor?: unknown; code?: unknown };\n if (typeof e.model === 'string' && typeof e.vendor === 'string' && typeof e.code === 'number') {\n cleaned.push({ model: e.model, vendor: e.vendor, code: e.code });\n }\n }\n }\n if (cleaned.length > 0) this.failovers = cleaned;\n }\n }\n }\n}\n\nexport interface HttpClientOptions {\n apiKey: string;\n baseUrl: string;\n fetchFn?: typeof fetch;\n /** Default per-request timeout in ms. Overridable per call. */\n timeoutMs?: number;\n}\n\n/** Per-request overrides — passed by the API layer, not by SDK consumers directly. */\nexport interface RequestOptions {\n /** Override the client default timeout for just this request. */\n timeoutMs?: number;\n /** Caller-provided AbortSignal. Linked together with the SDK's internal timeout\n * signal — whichever fires first aborts the request. */\n signal?: AbortSignal;\n /** Extra headers to merge in (e.g. `Idempotency-Key`). */\n headers?: Record<string, string>;\n}\n\nexport class HttpClient {\n private readonly apiKey: string;\n private readonly baseUrl: string;\n private readonly fetchFn: typeof fetch;\n private readonly defaultTimeoutMs: number;\n\n constructor(options: HttpClientOptions) {\n this.apiKey = options.apiKey;\n this.baseUrl = options.baseUrl.replace(/\\/$/, '');\n // Bind to `globalThis` so calling via `this.fetchFn(...)` doesn't trip\n // Cloudflare Workers' \"Illegal invocation\" check — the platform's fetch\n // requires the global receiver, not an instance method `this`. Affects\n // any environment that ships a strict-receiver fetch (Workers, Bun, etc.)\n // and is harmless on Node + browsers.\n const fetchImpl = options.fetchFn ?? fetch;\n this.fetchFn = fetchImpl.bind(globalThis);\n // 180s aligns the outer SDK budget with the premium routing path on the\n // gateway: per-vendor 60s × up to 3 PREMIUM-tier attempts = ~180s. Customers\n // running tailor / job-extract style long-context calls were hitting the\n // previous 60s cap before the gateway could finish its premium cascade.\n // Per-call `timeoutMs` still overrides for callers that want a tighter UX.\n this.defaultTimeoutMs = options.timeoutMs ?? 180_000;\n }\n\n async getJson<T>(path: string, options?: RequestOptions): Promise<T> {\n const res = await this.fetchWithTimeout(`${this.baseUrl}${path}`, {\n method: 'GET',\n headers: this.mergeHeaders(options),\n }, options);\n return this.parseJsonResponse<T>(res);\n }\n\n async postJson<T>(path: string, body: unknown, options?: RequestOptions): Promise<T> {\n const res = await this.fetchWithTimeout(`${this.baseUrl}${path}`, {\n method: 'POST',\n headers: this.mergeHeaders(options, { 'Content-Type': 'application/json' }),\n body: JSON.stringify(body),\n }, options);\n return this.parseJsonResponse<T>(res);\n }\n\n async postRaw(path: string, body: unknown, options?: RequestOptions): Promise<Response> {\n const res = await this.fetchWithTimeout(`${this.baseUrl}${path}`, {\n method: 'POST',\n headers: this.mergeHeaders(options, { 'Content-Type': 'application/json' }),\n body: JSON.stringify(body),\n }, options);\n if (!res.ok) {\n throw await this.toApiError(res);\n }\n return res;\n }\n\n private mergeHeaders(options?: RequestOptions, base?: Record<string, string>): Record<string, string> {\n return {\n Authorization: `Bearer ${this.apiKey}`,\n ...(base ?? {}),\n ...(options?.headers ?? {}),\n };\n }\n\n /**\n * Wrap a fetch in a combined abort signal: an internal timeout AND any\n * caller-provided signal. Either firing aborts the request. Single source of\n * abort plumbing — every method routes through here (DRY).\n */\n private async fetchWithTimeout(\n input: RequestInfo | URL,\n init: RequestInit,\n options?: RequestOptions,\n ): Promise<Response> {\n const timeoutMs = options?.timeoutMs ?? this.defaultTimeoutMs;\n const timeoutCtl = new AbortController();\n const timer = setTimeout(() => timeoutCtl.abort(), timeoutMs);\n\n // Combine internal timeout signal + caller signal. Native AbortSignal.any\n // (Node 20+ / modern Workers) is preferred; fall back to manual linking.\n const signal = combineSignals(timeoutCtl.signal, options?.signal);\n\n try {\n return await this.fetchFn(input, { ...init, signal });\n } catch (error) {\n if (timeoutCtl.signal.aborted) {\n throw new BuilderforceApiError(`Request timed out after ${timeoutMs}ms`, 408, 'timeout');\n }\n if (options?.signal?.aborted) {\n throw new BuilderforceApiError('Request aborted by caller', 499, 'aborted');\n }\n throw error;\n } finally {\n clearTimeout(timer);\n }\n }\n\n private async parseJsonResponse<T>(res: Response): Promise<T> {\n if (!res.ok) {\n throw await this.toApiError(res);\n }\n return res.json() as Promise<T>;\n }\n\n private async toApiError(res: Response): Promise<BuilderforceApiError> {\n const fallback = `Request failed (${res.status})`;\n const requestId = res.headers.get('x-request-id') ?? undefined;\n\n // Prefer server-supplied `Retry-After` header (seconds) when present; the\n // body's `retryAfter` is a fallback for environments that strip headers.\n const headerRetryAfter = parsePositiveInt(res.headers.get('retry-after'));\n\n try {\n const payload = await res.json() as Record<string, unknown> | null;\n\n // Three envelope shapes are in the wild:\n //\n // Flat — { error: \"msg\", code, details, terminal?, retryAfter? }\n // (gateway's documented shape — plan_token_limit_exceeded etc.)\n // OpenAI — { error: { message, code, type, details } }\n // (cascade-exhausted 429 on both chat and image surfaces —\n // matches OpenAI's error envelope convention)\n // Wrapped — { success: false, error: { code, message, details } }\n // (consumer-side wrappers around the gateway, e.g. some\n // tenant proxies emit AI_RATE_LIMITED / AI_UNAVAILABLE\n // envelopes that re-wrap the upstream error)\n //\n // Unwrap to a single `inner` shape so `details.failovers` etc. always\n // populate on `BuilderforceApiError` regardless of which envelope the\n // gateway picked. Single parsing site for every surface (DRY).\n const errorObj =\n payload !== null\n && typeof payload === 'object'\n && typeof payload.error === 'object'\n && payload.error !== null\n ? payload.error as Record<string, unknown>\n : null;\n\n const isWrapped =\n payload !== null\n && typeof payload === 'object'\n && payload.success === false\n && errorObj !== null;\n\n const inner = (isWrapped || errorObj !== null ? errorObj : payload) as {\n error?: string;\n message?: string;\n code?: string | number;\n details?: unknown;\n terminal?: boolean;\n retryAfter?: number;\n vendor?: string;\n model?: string;\n } | null;\n\n const message =\n (typeof inner?.message === 'string' && inner.message)\n || (typeof inner?.error === 'string' && inner.error)\n || fallback;\n\n // Coerce numeric error codes (e.g. cascade-exhausted emits `code: 429`)\n // to string so `BuilderforceApiError.code` stays a stable type for\n // consumer-side switches. String codes pass through unchanged.\n const code = typeof inner?.code === 'number' ? String(inner.code) : inner?.code;\n\n // Vendor/model may travel at the top of the error envelope (gateway\n // dispatched against an upstream) OR be embedded in details (e.g.\n // `model_unavailable` carries `details.requestedModel`). Prefer the\n // top-level fields; fall back to details so we still extract a model\n // from the strict-pin 503 envelope without a gateway change.\n const detailsObj = (inner?.details && typeof inner.details === 'object')\n ? inner.details as { vendor?: unknown; model?: unknown; requestedModel?: unknown }\n : null;\n const vendor = typeof inner?.vendor === 'string'\n ? inner.vendor\n : (typeof detailsObj?.vendor === 'string' ? detailsObj.vendor : undefined);\n const model = typeof inner?.model === 'string'\n ? inner.model\n : (typeof detailsObj?.model === 'string'\n ? detailsObj.model\n : (typeof detailsObj?.requestedModel === 'string' ? detailsObj.requestedModel : undefined));\n\n return new BuilderforceApiError(\n message,\n res.status,\n code,\n inner?.details,\n requestId,\n {\n terminal: inner?.terminal,\n retryAfter: headerRetryAfter ?? inner?.retryAfter,\n vendor,\n model,\n },\n );\n } catch {\n const text = await res.text().catch(() => '');\n return new BuilderforceApiError(\n text || fallback,\n res.status,\n undefined,\n undefined,\n requestId,\n headerRetryAfter !== undefined ? { retryAfter: headerRetryAfter } : undefined,\n );\n }\n }\n}\n\nfunction parsePositiveInt(s: string | null): number | undefined {\n if (s == null) return undefined;\n const n = Number(s);\n return Number.isFinite(n) && n >= 0 ? Math.floor(n) : undefined;\n}\n\n/**\n * Combine multiple AbortSignals into one. Uses native `AbortSignal.any` when\n * available (Node 20+, modern Workers); falls back to manual event linking.\n */\nfunction combineSignals(...signals: Array<AbortSignal | undefined>): AbortSignal {\n const live = signals.filter((s): s is AbortSignal => s !== undefined);\n if (live.length === 1) return live[0]!;\n\n const anyImpl = (AbortSignal as unknown as { any?: (signals: AbortSignal[]) => AbortSignal }).any;\n if (typeof anyImpl === 'function') {\n return anyImpl(live);\n }\n\n const ctl = new AbortController();\n for (const s of live) {\n if (s.aborted) { ctl.abort(s.reason); break; }\n s.addEventListener('abort', () => ctl.abort(s.reason), { once: true });\n }\n return ctl.signal;\n}\n","import { ChatCompletionsApi } from './application/ChatCompletionsApi';\nimport { EmbeddingsApi } from './application/EmbeddingsApi';\nimport { ImagesApi } from './application/ImagesApi';\nimport { ModelsApi } from './application/ModelsApi';\nimport { UsageApi } from './application/UsageApi';\nimport { BuilderforceApiError, HttpClient } from './infrastructure/httpClient';\n\nexport interface BuilderforceClientOptions {\n apiKey: string;\n baseUrl?: string;\n fetch?: typeof fetch;\n /** Default request timeout in ms (default 60_000). Per-call override available\n * via `chat.completions.create({ timeoutMs })` and `embeddings.create({ timeoutMs })`. */\n timeoutMs?: number;\n}\n\nexport class BuilderforceClient {\n public readonly chat: {\n completions: ChatCompletionsApi;\n };\n public readonly embeddings: EmbeddingsApi;\n public readonly images: ImagesApi;\n public readonly models: ModelsApi;\n public readonly usage: UsageApi;\n\n constructor(options: BuilderforceClientOptions) {\n const apiKey = options.apiKey?.trim();\n if (!apiKey) {\n throw new BuilderforceApiError(\n 'BuilderforceClient requires a non-empty apiKey',\n 400,\n 'missing_api_key',\n );\n }\n\n const http = new HttpClient({\n apiKey,\n baseUrl: options.baseUrl ?? 'https://api.builderforce.ai',\n fetchFn: options.fetch,\n timeoutMs: options.timeoutMs,\n });\n\n this.chat = {\n completions: new ChatCompletionsApi(http),\n };\n this.embeddings = new EmbeddingsApi(http);\n this.images = new ImagesApi(http);\n this.models = new ModelsApi(http);\n this.usage = new UsageApi(http);\n }\n}\n"],"mappings":";AAAA,gBAAuB,aACrB,QACkC;AAClC,QAAM,SAAS,OAAO,UAAU;AAChC,QAAM,UAAU,IAAI,YAAY;AAChC,MAAI,SAAS;AAEb,SAAO,MAAM;AACX,UAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK;AAC1C,QAAI,KAAM;AAEV,cAAU,QAAQ,OAAO,OAAO,EAAE,QAAQ,KAAK,CAAC;AAChD,UAAM,QAAQ,OAAO,MAAM,IAAI;AAC/B,aAAS,MAAM,IAAI,KAAK;AAExB,eAAW,QAAQ,OAAO;AACxB,YAAM,UAAU,KAAK,KAAK;AAC1B,UAAI,CAAC,QAAQ,WAAW,QAAQ,EAAG;AAEnC,YAAM,OAAO,QAAQ,MAAM,CAAC,EAAE,KAAK;AACnC,UAAI,SAAS,SAAU;AAEvB,UAAI;AACF,cAAM,KAAK,MAAM,IAAI;AAAA,MACvB,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AACF;;;ACzBO,IAAM,uBAAN,MAAyE;AAAA,EAC7D;AAAA,EAEjB,YAAY,QAAoC;AAC9C,SAAK,SAAS;AAAA,EAChB;AAAA,EAEA,CAAC,OAAO,aAAa,IAAuD;AAC1E,WAAO,aAAkC,KAAK,MAAM;AAAA,EACtD;AAAA,EAEA,MAAM,SAA0B;AAC9B,QAAI,OAAO;AACX,qBAAiB,SAAS,MAAM;AAC9B,YAAM,QAAQ,MAAM,UAAU,CAAC,GAAG,OAAO;AACzC,UAAI,OAAO,UAAU,UAAU;AAC7B,gBAAQ;AAAA,MACV;AAAA,IACF;AACA,WAAO;AAAA,EACT;AACF;AAOA,SAAS,sBAAsB,QAG7B;AACA,QAAM,EAAE,WAAW,QAAQ,gBAAgB,GAAG,KAAK,IAAI;AACvD,QAAM,UAAkC,CAAC;AACzC,MAAI,eAAgB,SAAQ,iBAAiB,IAAI;AACjD,SAAO;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,MACP;AAAA,MACA;AAAA,MACA,GAAI,OAAO,KAAK,OAAO,EAAE,SAAS,IAAI,EAAE,QAAQ,IAAI,CAAC;AAAA,IACvD;AAAA,EACF;AACF;AAEO,IAAM,qBAAN,MAAyB;AAAA,EACb;AAAA,EAEjB,YAAY,MAAkB;AAC5B,SAAK,OAAO;AAAA,EACd;AAAA,EAIA,MAAM,OACJ,QACwD;AACxD,UAAM,EAAE,MAAM,QAAQ,IAAI,sBAAsB,MAAM;AAEtD,QAAI,OAAO,QAAQ;AACjB,YAAM,WAAW,MAAM,KAAK,KAAK,QAAQ,4BAA4B,MAAM,OAAO;AAClF,UAAI,CAAC,SAAS,MAAM;AAClB,cAAM,IAAI,MAAM,oCAAoC;AAAA,MACtD;AACA,aAAO,IAAI,qBAAqB,SAAS,IAAI;AAAA,IAC/C;AAEA,WAAO,KAAK,KAAK,SAAiC,4BAA4B,MAAM,OAAO;AAAA,EAC7F;AACF;;;AClEA,SAASA,uBAAsB,QAG7B;AACA,QAAM,EAAE,WAAW,QAAQ,gBAAgB,GAAG,KAAK,IAAI;AACvD,QAAM,UAAkC,CAAC;AACzC,MAAI,eAAgB,SAAQ,iBAAiB,IAAI;AACjD,SAAO;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,MACP;AAAA,MACA;AAAA,MACA,GAAI,OAAO,KAAK,OAAO,EAAE,SAAS,IAAI,EAAE,QAAQ,IAAI,CAAC;AAAA,IACvD;AAAA,EACF;AACF;AAEO,IAAM,gBAAN,MAAoB;AAAA,EACR;AAAA,EAEjB,YAAY,MAAkB;AAC5B,SAAK,OAAO;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAO,QAA6D;AAClE,UAAM,EAAE,MAAM,QAAQ,IAAIA,uBAAsB,MAAM;AACtD,WAAO,KAAK,KAAK,SAA6B,sBAAsB,MAAM,OAAO;AAAA,EACnF;AACF;;;AC/BA,SAASC,uBAAsB,QAG7B;AACA,QAAM,EAAE,WAAW,QAAQ,gBAAgB,GAAG,KAAK,IAAI;AACvD,QAAM,UAAkC,CAAC;AACzC,MAAI,eAAgB,SAAQ,iBAAiB,IAAI;AACjD,SAAO;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,MACP;AAAA,MACA;AAAA,MACA,GAAI,OAAO,KAAK,OAAO,EAAE,SAAS,IAAI,EAAE,QAAQ,IAAI,CAAC;AAAA,IACvD;AAAA,EACF;AACF;AAeO,IAAM,YAAN,MAAgB;AAAA,EACJ;AAAA,EAEjB,YAAY,MAAkB;AAC5B,SAAK,OAAO;AAAA,EACd;AAAA,EAEA,SAAS,QAAuE;AAC9E,UAAM,EAAE,MAAM,QAAQ,IAAIA,uBAAsB,MAAM;AACtD,WAAO,KAAK,KAAK,SAAkC,8BAA8B,MAAM,OAAO;AAAA,EAChG;AACF;;;AC9CO,IAAM,YAAN,MAAgB;AAAA,EACJ;AAAA,EAEjB,YAAY,MAAkB;AAC5B,SAAK,OAAO;AAAA,EACd;AAAA,EAEA,OAAoC;AAClC,WAAO,KAAK,KAAK,QAA4B,gBAAgB;AAAA,EAC/D;AACF;;;ACVO,IAAM,WAAN,MAAe;AAAA,EACH;AAAA,EAEjB,YAAY,MAAkB;AAC5B,SAAK,OAAO;AAAA,EACd;AAAA,EAEA,IAAI,SAAyB,CAAC,GAA2B;AACvD,UAAM,QAAQ,OAAO,OAAO,SAAS,WAAW,SAAS,mBAAmB,OAAO,OAAO,IAAI,CAAC,CAAC,KAAK;AACrG,WAAO,KAAK,KAAK,QAAuB,gBAAgB,KAAK,EAAE;AAAA,EACjE;AACF;;;ACZO,IAAM,uBAAN,cAAmC,MAAM;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA;AAAA;AAAA,EAEA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeA;AAAA;AAAA;AAAA;AAAA;AAAA,EAKA;AAAA,EAEhB,YACE,SACA,QACA,MACA,SACA,WACA,QACA;AACA,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,SAAS;AACd,SAAK,OAAO;AACZ,SAAK,UAAU;AACf,SAAK,YAAY;AACjB,SAAK,WAAW,QAAQ;AACxB,SAAK,aAAa,QAAQ;AAC1B,SAAK,SAAS,QAAQ;AACtB,SAAK,QAAQ,QAAQ;AAIrB,QAAI,WAAW,OAAO,YAAY,UAAU;AAC1C,YAAM,IAAK,QAAoC;AAC/C,UAAI,MAAM,QAAQ,CAAC,GAAG;AACpB,cAAM,UAA2B,CAAC;AAClC,mBAAW,SAAS,GAAG;AACrB,cAAI,SAAS,OAAO,UAAU,UAAU;AACtC,kBAAM,IAAI;AACV,gBAAI,OAAO,EAAE,UAAU,YAAY,OAAO,EAAE,WAAW,YAAY,OAAO,EAAE,SAAS,UAAU;AAC7F,sBAAQ,KAAK,EAAE,OAAO,EAAE,OAAO,QAAQ,EAAE,QAAQ,MAAM,EAAE,KAAK,CAAC;AAAA,YACjE;AAAA,UACF;AAAA,QACF;AACA,YAAI,QAAQ,SAAS,EAAG,MAAK,YAAY;AAAA,MAC3C;AAAA,IACF;AAAA,EACF;AACF;AAqBO,IAAM,aAAN,MAAiB;AAAA,EACL;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAY,SAA4B;AACtC,SAAK,SAAS,QAAQ;AACtB,SAAK,UAAU,QAAQ,QAAQ,QAAQ,OAAO,EAAE;AAMhD,UAAM,YAAY,QAAQ,WAAW;AACrC,SAAK,UAAU,UAAU,KAAK,UAAU;AAMxC,SAAK,mBAAmB,QAAQ,aAAa;AAAA,EAC/C;AAAA,EAEA,MAAM,QAAW,MAAc,SAAsC;AACnE,UAAM,MAAM,MAAM,KAAK,iBAAiB,GAAG,KAAK,OAAO,GAAG,IAAI,IAAI;AAAA,MAChE,QAAQ;AAAA,MACR,SAAS,KAAK,aAAa,OAAO;AAAA,IACpC,GAAG,OAAO;AACV,WAAO,KAAK,kBAAqB,GAAG;AAAA,EACtC;AAAA,EAEA,MAAM,SAAY,MAAc,MAAe,SAAsC;AACnF,UAAM,MAAM,MAAM,KAAK,iBAAiB,GAAG,KAAK,OAAO,GAAG,IAAI,IAAI;AAAA,MAChE,QAAQ;AAAA,MACR,SAAS,KAAK,aAAa,SAAS,EAAE,gBAAgB,mBAAmB,CAAC;AAAA,MAC1E,MAAM,KAAK,UAAU,IAAI;AAAA,IAC3B,GAAG,OAAO;AACV,WAAO,KAAK,kBAAqB,GAAG;AAAA,EACtC;AAAA,EAEA,MAAM,QAAQ,MAAc,MAAe,SAA6C;AACtF,UAAM,MAAM,MAAM,KAAK,iBAAiB,GAAG,KAAK,OAAO,GAAG,IAAI,IAAI;AAAA,MAChE,QAAQ;AAAA,MACR,SAAS,KAAK,aAAa,SAAS,EAAE,gBAAgB,mBAAmB,CAAC;AAAA,MAC1E,MAAM,KAAK,UAAU,IAAI;AAAA,IAC3B,GAAG,OAAO;AACV,QAAI,CAAC,IAAI,IAAI;AACX,YAAM,MAAM,KAAK,WAAW,GAAG;AAAA,IACjC;AACA,WAAO;AAAA,EACT;AAAA,EAEQ,aAAa,SAA0B,MAAuD;AACpG,WAAO;AAAA,MACL,eAAe,UAAU,KAAK,MAAM;AAAA,MACpC,GAAI,QAAQ,CAAC;AAAA,MACb,GAAI,SAAS,WAAW,CAAC;AAAA,IAC3B;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAc,iBACZ,OACA,MACA,SACmB;AACnB,UAAM,YAAY,SAAS,aAAa,KAAK;AAC7C,UAAM,aAAa,IAAI,gBAAgB;AACvC,UAAM,QAAQ,WAAW,MAAM,WAAW,MAAM,GAAG,SAAS;AAI5D,UAAM,SAAS,eAAe,WAAW,QAAQ,SAAS,MAAM;AAEhE,QAAI;AACF,aAAO,MAAM,KAAK,QAAQ,OAAO,EAAE,GAAG,MAAM,OAAO,CAAC;AAAA,IACtD,SAAS,OAAO;AACd,UAAI,WAAW,OAAO,SAAS;AAC7B,cAAM,IAAI,qBAAqB,2BAA2B,SAAS,MAAM,KAAK,SAAS;AAAA,MACzF;AACA,UAAI,SAAS,QAAQ,SAAS;AAC5B,cAAM,IAAI,qBAAqB,6BAA6B,KAAK,SAAS;AAAA,MAC5E;AACA,YAAM;AAAA,IACR,UAAE;AACA,mBAAa,KAAK;AAAA,IACpB;AAAA,EACF;AAAA,EAEA,MAAc,kBAAqB,KAA2B;AAC5D,QAAI,CAAC,IAAI,IAAI;AACX,YAAM,MAAM,KAAK,WAAW,GAAG;AAAA,IACjC;AACA,WAAO,IAAI,KAAK;AAAA,EAClB;AAAA,EAEA,MAAc,WAAW,KAA8C;AACrE,UAAM,WAAW,mBAAmB,IAAI,MAAM;AAC9C,UAAM,YAAY,IAAI,QAAQ,IAAI,cAAc,KAAK;AAIrD,UAAM,mBAAmB,iBAAiB,IAAI,QAAQ,IAAI,aAAa,CAAC;AAExE,QAAI;AACF,YAAM,UAAU,MAAM,IAAI,KAAK;AAiB/B,YAAM,WACJ,YAAY,QACT,OAAO,YAAY,YACnB,OAAO,QAAQ,UAAU,YACzB,QAAQ,UAAU,OACjB,QAAQ,QACR;AAEN,YAAM,YACJ,YAAY,QACT,OAAO,YAAY,YACnB,QAAQ,YAAY,SACpB,aAAa;AAElB,YAAM,QAAS,aAAa,aAAa,OAAO,WAAW;AAW3D,YAAM,UACH,OAAO,OAAO,YAAY,YAAY,MAAM,WACzC,OAAO,OAAO,UAAU,YAAY,MAAM,SAC3C;AAKL,YAAM,OAAO,OAAO,OAAO,SAAS,WAAW,OAAO,MAAM,IAAI,IAAI,OAAO;AAO3E,YAAM,aAAc,OAAO,WAAW,OAAO,MAAM,YAAY,WAC3D,MAAM,UACN;AACJ,YAAM,SAAS,OAAO,OAAO,WAAW,WACpC,MAAM,SACL,OAAO,YAAY,WAAW,WAAW,WAAW,SAAS;AAClE,YAAM,QAAQ,OAAO,OAAO,UAAU,WAClC,MAAM,QACL,OAAO,YAAY,UAAU,WAC1B,WAAW,QACV,OAAO,YAAY,mBAAmB,WAAW,WAAW,iBAAiB;AAEtF,aAAO,IAAI;AAAA,QACT;AAAA,QACA,IAAI;AAAA,QACJ;AAAA,QACA,OAAO;AAAA,QACP;AAAA,QACA;AAAA,UACE,UAAY,OAAO;AAAA,UACnB,YAAY,oBAAoB,OAAO;AAAA,UACvC;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,IACF,QAAQ;AACN,YAAM,OAAO,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,EAAE;AAC5C,aAAO,IAAI;AAAA,QACT,QAAQ;AAAA,QACR,IAAI;AAAA,QACJ;AAAA,QACA;AAAA,QACA;AAAA,QACA,qBAAqB,SAAY,EAAE,YAAY,iBAAiB,IAAI;AAAA,MACtE;AAAA,IACF;AAAA,EACF;AACF;AAEA,SAAS,iBAAiB,GAAsC;AAC9D,MAAI,KAAK,KAAM,QAAO;AACtB,QAAM,IAAI,OAAO,CAAC;AAClB,SAAO,OAAO,SAAS,CAAC,KAAK,KAAK,IAAI,KAAK,MAAM,CAAC,IAAI;AACxD;AAMA,SAAS,kBAAkB,SAAsD;AAC/E,QAAM,OAAO,QAAQ,OAAO,CAAC,MAAwB,MAAM,MAAS;AACpE,MAAI,KAAK,WAAW,EAAG,QAAO,KAAK,CAAC;AAEpC,QAAM,UAAW,YAA6E;AAC9F,MAAI,OAAO,YAAY,YAAY;AACjC,WAAO,QAAQ,IAAI;AAAA,EACrB;AAEA,QAAM,MAAM,IAAI,gBAAgB;AAChC,aAAW,KAAK,MAAM;AACpB,QAAI,EAAE,SAAS;AAAE,UAAI,MAAM,EAAE,MAAM;AAAG;AAAA,IAAO;AAC7C,MAAE,iBAAiB,SAAS,MAAM,IAAI,MAAM,EAAE,MAAM,GAAG,EAAE,MAAM,KAAK,CAAC;AAAA,EACvE;AACA,SAAO,IAAI;AACb;;;AC7TO,IAAM,qBAAN,MAAyB;AAAA,EACd;AAAA,EAGA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEhB,YAAY,SAAoC;AAC9C,UAAM,SAAS,QAAQ,QAAQ,KAAK;AACpC,QAAI,CAAC,QAAQ;AACX,YAAM,IAAI;AAAA,QACR;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAEA,UAAM,OAAO,IAAI,WAAW;AAAA,MAC1B;AAAA,MACA,SAAS,QAAQ,WAAW;AAAA,MAC5B,SAAS,QAAQ;AAAA,MACjB,WAAW,QAAQ;AAAA,IACrB,CAAC;AAED,SAAK,OAAO;AAAA,MACV,aAAa,IAAI,mBAAmB,IAAI;AAAA,IAC1C;AACA,SAAK,aAAa,IAAI,cAAc,IAAI;AACxC,SAAK,SAAS,IAAI,UAAU,IAAI;AAChC,SAAK,SAAS,IAAI,UAAU,IAAI;AAChC,SAAK,QAAQ,IAAI,SAAS,IAAI;AAAA,EAChC;AACF;","names":["splitTransportOptions","splitTransportOptions"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@seanhogg/builderforce-sdk",
3
- "version": "0.6.0",
4
- "description": "Typed SDK for the Builderforce.ai LLM gateway — chat completions with tool-calling and structured output, embeddings, models, and usage analytics over an OpenAI-compatible surface.",
3
+ "version": "0.8.0",
4
+ "description": "Typed SDK for the Builderforce.ai LLM gateway — chat completions with tool-calling and structured output, embeddings, image generation, models, and usage analytics over an OpenAI-compatible surface.",
5
5
  "license": "MIT",
6
6
  "author": "Sean Hogg",
7
7
  "homepage": "https://github.com/SeanHogg/Builderforce.ai/tree/main/sdk#readme",
@@ -19,8 +19,11 @@
19
19
  "openai-compatible",
20
20
  "ai-gateway",
21
21
  "chat-completions",
22
+ "image-generation",
23
+ "flux",
22
24
  "openrouter",
23
25
  "cerebras",
26
+ "together",
24
27
  "ollama",
25
28
  "sdk",
26
29
  "typescript"
@@ -46,8 +49,7 @@
46
49
  "build": "tsup --config tsup.config.ts",
47
50
  "type-check": "tsc --noEmit",
48
51
  "test": "vitest run",
49
- "prepublishOnly": "npm run build",
50
- "publish:if-new": "node scripts/publish-if-new.mjs"
52
+ "prepublishOnly": "npm run build"
51
53
  },
52
54
  "engines": {
53
55
  "node": ">=18"