@hraness/slopcamera 3.4.0 → 3.6.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.
Files changed (45) hide show
  1. package/README.md +32 -19
  2. package/apps/desktop/application/operation.ts +1 -1
  3. package/apps/desktop/cli/args.ts +3 -1
  4. package/apps/desktop/cli/commands.ts +22 -1
  5. package/apps/desktop/cli/help.ts +37 -13
  6. package/apps/desktop/cli/menubar-status.ts +155 -0
  7. package/apps/desktop/cli/menubar.ts +129 -101
  8. package/apps/desktop/dist/cli/main.js +334 -312
  9. package/apps/desktop/html-overlay/catalog.ts +1 -0
  10. package/apps/desktop/html-overlay/spatial.ts +10 -6
  11. package/apps/desktop/workflows/index.ts +1 -0
  12. package/dist/cli.js +7 -8
  13. package/dist/generate.js +1 -1
  14. package/dist/{index-qtn3s421.js → index-4z9y2wa2.js} +1 -1
  15. package/dist/{index-sy1n4zfy.js → index-fevf4ppk.js} +1 -1
  16. package/dist/{index-qry58nj2.js → index-mnp0bftq.js} +2 -2
  17. package/dist/index-nxtmy18t.js +3 -0
  18. package/dist/{index-abnk4h9c.js → index-qxfexvj3.js} +2 -2
  19. package/dist/index.js +1 -1
  20. package/dist/operations.js +1 -1
  21. package/dist/vectorize/worker.js +1 -1
  22. package/dist/workflow.js +1 -1
  23. package/docs/README.md +3 -3
  24. package/examples/studio/vgpu/README.md +1 -1
  25. package/package.json +5 -6
  26. package/skills/slopcamera/SKILL.md +4 -3
  27. package/skills/slopcamera/references/brand-illustrations.md +237 -0
  28. package/skills/slopcamera/references/gateway-media.md +1 -1
  29. package/skills/slopcamera/references/install.md +10 -1
  30. package/skills/slopcamera/references/patent-drawings.md +4 -4
  31. package/skills/slopcamera/references/support.md +1 -1
  32. package/skills/slopcamera/references/vectorization.md +3 -1
  33. package/src/cli.ts +13 -10
  34. package/src/code/public-operations.ts +9 -0
  35. package/src/generate.ts +44 -1
  36. package/src/generation-pricing.ts +217 -0
  37. package/src/mcp/tools.ts +4 -0
  38. package/src/operations.ts +4 -0
  39. package/src/support.ts +7 -1
  40. package/src/vectorize/command.ts +6 -1
  41. package/src/vectorize/supervisor.ts +23 -3
  42. package/src/vectorize/tool.ts +8 -4
  43. package/src/version.ts +1 -1
  44. package/src/visual-style.ts +4 -1
  45. package/dist/index-77fjfg9f.js +0 -3
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Provider list prices for the image models Slopcamera can bill, and the
3
+ * cost of one Vercel AI Gateway image call derived from them.
4
+ *
5
+ * The AI Gateway charges the provider's list price with no markup
6
+ * (https://vercel.com/docs/ai-gateway/pricing, checked 2026-09-26), so the
7
+ * OpenAI and Recraft list prices below are what one call costs. Every price
8
+ * is integer micro-USD.
9
+ *
10
+ * A request never sets size or quality, so OpenAI chooses them ("auto"). The
11
+ * worst case is therefore the highest quality at the largest size, plus the
12
+ * prompt's text input tokens. Slopcamera never sends reference or edit
13
+ * images, so every input token is a text token.
14
+ */
15
+
16
+ export type ProviderCostBasis = "reported" | "contractual" | "estimated" | "unknown"
17
+
18
+ /** One provider cost line, in the shape Hraness Credits accepts. */
19
+ export interface SlopcameraProviderCost {
20
+ readonly provider: string
21
+ readonly operation: string
22
+ readonly microUsd: number
23
+ readonly basis: ProviderCostBasis
24
+ }
25
+
26
+ /** Token counts a Gateway image call returned, as the AI SDK reports them. */
27
+ export interface SlopcameraImageUsage {
28
+ readonly inputTokens?: number | undefined
29
+ readonly outputTokens?: number | undefined
30
+ }
31
+
32
+ interface TokenPricedImageModel {
33
+ readonly kind: "tokens"
34
+ /** Text input tokens, micro-USD per million. */
35
+ readonly textInputPerMillion: number
36
+ /**
37
+ * Output tokens, micro-USD per million. This is the image output rate,
38
+ * which is the highest output rate the model has, so it also bounds any
39
+ * text output tokens.
40
+ */
41
+ readonly outputPerMillion: number
42
+ /** OpenAI's published price for one high-quality 1024x1536 image. */
43
+ readonly worstImage: number
44
+ readonly source: string
45
+ }
46
+
47
+ interface ImagePricedModel {
48
+ readonly kind: "image"
49
+ /** Flat price for one image in the style Slopcamera requests (none). */
50
+ readonly perImage: number
51
+ readonly source: string
52
+ }
53
+
54
+ export type SlopcameraImageModelPrice = TokenPricedImageModel | ImagePricedModel
55
+
56
+ export const slopcameraProviderPricesCheckedAt = "2026-09-26"
57
+
58
+ /**
59
+ * Cited list prices. A model without an entry here cannot be billed: the
60
+ * hosted service refuses it before any hold.
61
+ */
62
+ export const slopcameraImageModelPrices: Readonly<
63
+ Record<string, SlopcameraImageModelPrice>
64
+ > = Object.freeze({
65
+ "openai/gpt-image-1": Object.freeze({
66
+ kind: "tokens",
67
+ textInputPerMillion: 5_000_000,
68
+ outputPerMillion: 40_000_000,
69
+ worstImage: 250_000,
70
+ source: "https://developers.openai.com/api/docs/models/gpt-image-1",
71
+ }),
72
+ "openai/gpt-image-1-mini": Object.freeze({
73
+ kind: "tokens",
74
+ textInputPerMillion: 2_000_000,
75
+ outputPerMillion: 8_000_000,
76
+ worstImage: 52_000,
77
+ source: "https://developers.openai.com/api/docs/models/gpt-image-1-mini",
78
+ }),
79
+ "openai/gpt-image-1.5": Object.freeze({
80
+ kind: "tokens",
81
+ textInputPerMillion: 5_000_000,
82
+ outputPerMillion: 32_000_000,
83
+ worstImage: 200_000,
84
+ source: "https://developers.openai.com/api/docs/models/gpt-image-1.5",
85
+ }),
86
+ "recraft/recraft-v4.1-utility": Object.freeze({
87
+ kind: "image",
88
+ perImage: 35_000,
89
+ source: "https://vercel.com/ai-gateway/models/recraft-v4.1-utility",
90
+ }),
91
+ })
92
+
93
+ /**
94
+ * Tokens allowed on top of the prompt's UTF-8 byte count when no usage came
95
+ * back. A byte-level BPE token covers at least one byte, so the byte count
96
+ * bounds the prompt's own tokens; the allowance covers request framing.
97
+ */
98
+ export const slopcameraPromptTokenAllowance = 64
99
+
100
+ export const slopcameraCostProvider = "vercel-ai-gateway"
101
+
102
+ export function slopcameraImageModelPrice(
103
+ model: string,
104
+ ): SlopcameraImageModelPrice | undefined {
105
+ return Object.hasOwn(slopcameraImageModelPrices, model)
106
+ ? slopcameraImageModelPrices[model]
107
+ : undefined
108
+ }
109
+
110
+ function tokenCost(tokens: number, perMillion: number): number {
111
+ // Round up so a partial micro-dollar is never dropped.
112
+ return Math.ceil((tokens * perMillion) / 1_000_000)
113
+ }
114
+
115
+ function promptBytes(prompt: string): number {
116
+ return Buffer.byteLength(prompt, "utf8")
117
+ }
118
+
119
+ function validTokenCount(value: unknown): value is number {
120
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0
121
+ }
122
+
123
+ /**
124
+ * The most one image call for this model and prompt can cost: highest
125
+ * quality at the largest size, and every prompt byte counted as a token.
126
+ */
127
+ export function worstCaseImageCostMicroUsd(
128
+ model: string,
129
+ prompt: string,
130
+ ): number | undefined {
131
+ const price = slopcameraImageModelPrice(model)
132
+ if (price === undefined) return undefined
133
+ if (price.kind === "image") return price.perImage
134
+ return (
135
+ price.worstImage +
136
+ tokenCost(
137
+ promptBytes(prompt) + slopcameraPromptTokenAllowance,
138
+ price.textInputPerMillion,
139
+ )
140
+ )
141
+ }
142
+
143
+ function estimatedLines(
144
+ model: string,
145
+ prompt: string,
146
+ price: SlopcameraImageModelPrice,
147
+ ): SlopcameraProviderCost[] {
148
+ if (price.kind === "image") {
149
+ return [{
150
+ provider: slopcameraCostProvider,
151
+ operation: `${model} image`,
152
+ microUsd: price.perImage,
153
+ basis: "estimated",
154
+ }]
155
+ }
156
+ return [
157
+ {
158
+ provider: slopcameraCostProvider,
159
+ operation: `${model} image output`,
160
+ microUsd: price.worstImage,
161
+ basis: "estimated",
162
+ },
163
+ {
164
+ provider: slopcameraCostProvider,
165
+ operation: `${model} text input`,
166
+ microUsd: tokenCost(
167
+ promptBytes(prompt) + slopcameraPromptTokenAllowance,
168
+ price.textInputPerMillion,
169
+ ),
170
+ basis: "estimated",
171
+ },
172
+ ]
173
+ }
174
+
175
+ /**
176
+ * Cost lines for one image call that reached the provider. Returned usage
177
+ * prices the call at the official per-token rates (basis "reported");
178
+ * without usage, the worst case stands in (basis "estimated").
179
+ */
180
+ export function imageCallCosts(
181
+ model: string,
182
+ prompt: string,
183
+ usage: SlopcameraImageUsage | undefined,
184
+ ): SlopcameraProviderCost[] {
185
+ const price = slopcameraImageModelPrice(model)
186
+ if (price === undefined) {
187
+ return [{
188
+ provider: slopcameraCostProvider,
189
+ operation: `${model} image`,
190
+ microUsd: 0,
191
+ basis: "unknown",
192
+ }]
193
+ }
194
+ if (
195
+ price.kind === "tokens" &&
196
+ usage !== undefined &&
197
+ validTokenCount(usage.inputTokens) &&
198
+ validTokenCount(usage.outputTokens) &&
199
+ usage.outputTokens > 0
200
+ ) {
201
+ return [
202
+ {
203
+ provider: slopcameraCostProvider,
204
+ operation: `${model} image output`,
205
+ microUsd: tokenCost(usage.outputTokens, price.outputPerMillion),
206
+ basis: "reported",
207
+ },
208
+ {
209
+ provider: slopcameraCostProvider,
210
+ operation: `${model} text input`,
211
+ microUsd: tokenCost(usage.inputTokens, price.textInputPerMillion),
212
+ basis: "reported",
213
+ },
214
+ ]
215
+ }
216
+ return estimatedLines(model, prompt, price)
217
+ }
package/src/mcp/tools.ts CHANGED
@@ -158,6 +158,10 @@ function deepFreeze<T>(value: T): T {
158
158
  return Object.freeze(value)
159
159
  }
160
160
 
161
+ /**
162
+ * The complete MCP tool registry. Public copy cites its size, its scene-tool
163
+ * count, and its names; `scripts/check-copy.ts` fails when they disagree.
164
+ */
161
165
  export const slopcameraMcpTools: readonly McpToolDefinition[] = deepFreeze([
162
166
  {
163
167
  name: "check_diagram",
package/src/operations.ts CHANGED
@@ -38,6 +38,10 @@ import {
38
38
  type HostResourceLease,
39
39
  } from "./host-resources.js"
40
40
 
41
+ /**
42
+ * The fixed semantic operation registry. Public copy cites its size and names;
43
+ * `scripts/check-copy.ts` fails when README, docs, or skills disagree with it.
44
+ */
41
45
  export const slopcameraOperationCodes = [
42
46
  "slopcamera.diagram.check",
43
47
  "slopcamera.diagram.render",
package/src/support.ts CHANGED
@@ -1,13 +1,19 @@
1
1
  import {
2
2
  maybeShowSupportInvitation,
3
3
  runSupportCommand,
4
+ supportAdvancedHelp,
5
+ supportHelpLine,
4
6
  type SupportCommandOptions,
5
7
  } from "@hraness/support-foundation/node"
6
8
 
9
+ /** The one support line for root help, and the agent verbs for `help advanced`. */
10
+ export const slopcameraSupportHelpLine = () => supportHelpLine({ command: ["slopcamera"] })
11
+ export const slopcameraSupportAdvancedHelp = () => supportAdvancedHelp({ command: ["slopcamera"] })
12
+
7
13
  export const supportProfile = {
8
14
  id: "slopcamera",
9
15
  name: "Slopcamera",
10
- valueProposition: "Support ongoing development of local visual tools for agents.",
16
+ valueProposition: "Support ongoing development of local media tools for agents.",
11
17
  updates: false,
12
18
  } as const
13
19
 
@@ -16,7 +16,12 @@ const MAX_COMMAND_OUTPUT_BYTES = 64 * 1_024
16
16
  const PRIMARY_OUTPUT_READ_BYTES = 64 * 1_024
17
17
  const PRIMARY_OUTPUT_POLL_MS = 2
18
18
  const PRIMARY_OUTPUT_EXIT_EMPTY_POLLS = 2
19
- const TERMINATION_GRACE_MS = 50
19
+ /**
20
+ * Grace between SIGTERM and SIGKILL when a bounded command overruns. Callers
21
+ * that layer their own wall-clock budget above a bounded command must reserve
22
+ * at least this much, plus scheduling latency, so cleanup finishes inside it.
23
+ */
24
+ export const TERMINATION_GRACE_MS = 50
20
25
  const HARD_KILL_WAIT_MS = 500
21
26
  const MAX_INHERITED_FILE_DESCRIPTORS = 16
22
27
 
@@ -2,7 +2,10 @@ import { fileURLToPath } from "node:url"
2
2
  import { mkdtemp, rm } from "node:fs/promises"
3
3
  import { tmpdir } from "node:os"
4
4
  import { dirname, join } from "node:path"
5
- import { runBoundedCommand } from "./command.js"
5
+ import {
6
+ TERMINATION_GRACE_MS,
7
+ runBoundedCommand,
8
+ } from "./command.js"
6
9
  import { resolveVectorizeLimits } from "./limits.js"
7
10
  import {
8
11
  MAX_VECTORIZE_REQUEST_BYTES,
@@ -33,8 +36,25 @@ const vectorizeErrorCodes = new Set<VectorizeErrorCode>([
33
36
  "trace_failed",
34
37
  "unsafe_svg",
35
38
  ])
36
- const WORKER_SHUTDOWN_RESERVE_MS = 250
37
- const WORKER_RESPONSE_RESERVE_MS = 100
39
+ /**
40
+ * Wall-clock the supervisor keeps back from the worker's budget so the whole
41
+ * boundary (worker timeout, layered process-group termination, stream
42
+ * settling, temporary-root removal, response decoding) still rejects inside
43
+ * `limits.maxDurationMs`.
44
+ *
45
+ * When the worker overruns, two termination layers run back to back: the
46
+ * worker terminates its own VTracer group, and this supervisor terminates the
47
+ * worker group. Each layer waits `TERMINATION_GRACE_MS` before SIGKILL and then
48
+ * needs the scheduler to reap the process and settle its pipes. A loaded CI
49
+ * runner has been measured spending ~400 ms in that chain against the earlier
50
+ * 350 ms reserve (hraness/slopcamera#235), so the reserve now budgets each
51
+ * layer explicitly instead of guessing one round number.
52
+ */
53
+ const PROCESS_TEARDOWN_ALLOWANCE_MS = 250
54
+ const TERMINATION_LAYERS = 2
55
+ const WORKER_SHUTDOWN_RESERVE_MS =
56
+ TERMINATION_LAYERS * (TERMINATION_GRACE_MS + PROCESS_TEARDOWN_ALLOWANCE_MS)
57
+ const WORKER_RESPONSE_RESERVE_MS = 150
38
58
 
39
59
  export async function runVectorizeWorker(
40
60
  input: VectorizeInput,
@@ -383,11 +383,13 @@ async function hashRegularFile(
383
383
  deadline.assert("VTracer hash")
384
384
  handle = await open(resolvedPath, boundedReadFlags())
385
385
  const metadata = await handle.stat()
386
- if (!metadata.isFile() || metadata.size < 1 || metadata.size > maximumBytes) {
386
+ if (
387
+ !metadata?.isFile() || metadata.size < 1 || metadata.size > maximumBytes
388
+ ) {
387
389
  throw new VectorizeError(
388
390
  failureCode,
389
391
  "VTracer must be a non-empty regular file within its size limit.",
390
- { bytes: metadata.size, maximumBytes },
392
+ { bytes: metadata?.size ?? 0, maximumBytes },
391
393
  )
392
394
  }
393
395
  const hash = createHash("sha256")
@@ -470,11 +472,13 @@ function assertBoundedRegularTool(
470
472
  metadata: Awaited<ReturnType<FileHandle["stat"]>>,
471
473
  path: string,
472
474
  ): void {
473
- if (!metadata.isFile() || metadata.size < 1 || metadata.size > MAX_TOOL_BYTES) {
475
+ if (
476
+ !metadata?.isFile() || metadata.size < 1 || metadata.size > MAX_TOOL_BYTES
477
+ ) {
474
478
  throw new VectorizeError(
475
479
  "tool_version",
476
480
  `VTracer must be a non-empty regular file: ${path}`,
477
- { bytes: metadata.size, maximumBytes: MAX_TOOL_BYTES },
481
+ { bytes: metadata?.size ?? 0, maximumBytes: MAX_TOOL_BYTES },
478
482
  )
479
483
  }
480
484
  }
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const SLOPCAMERA_VERSION = "3.4.0" as const
1
+ export const SLOPCAMERA_VERSION = "3.6.0" as const
@@ -1,6 +1,9 @@
1
1
  import { z } from "zod"
2
2
 
3
- /** Art direction is independent of the renderer, its library locks, and model choice. */
3
+ /**
4
+ * Art direction is independent of the renderer, its library locks, and model choice.
5
+ * Public copy cites this count; `scripts/check-copy.ts` fails on drift.
6
+ */
4
7
  export const VISUAL_STYLE_IDS = Object.freeze([
5
8
  "silent-actuality", "noir-35mm", "documentary-16mm", "super8-color",
6
9
  "theatrical-cel", "watercolor-storybook", "ink-sketch", "rubber-hose",
@@ -1,3 +0,0 @@
1
- // @bun
2
- import{__require}from"./index-80a36bc0.js";import{createHash,randomUUID}from"crypto";import{link,rm,writeFile}from"fs/promises";import{dirname,extname,resolve}from"path";class SlopcameraCloudError extends Error{code;constructor(code,message,options){super(`[${code}] ${message}`,options);this.name="SlopcameraCloudError";this.code=code}}var slopcameraGatewayApiBaseUrl="https://ai-gateway.vercel.sh/v4/ai";var slopcameraImageModels=Object.freeze(["openai/gpt-image-1.5","recraft/recraft-v4.1-utility"]);var slopcameraResponseMediaTypes=Object.freeze(["image/png","image/jpeg","image/webp"]);var slopcameraMaximumPromptBytes=32*1024;var slopcameraMaximumRawImageBytes=64*1024*1024;var defaultGenerationTimeoutMs=5*60000;var maximumGenerationTimeoutMs=30*60000;var defaultMaximumGatewayResponseBytes=96*1024*1024;function invalidArgument(message){throw new SlopcameraCloudError("INVALID_ARGUMENT",message)}function credentialValue(value){if(value.length<16||value.length>16*1024||value.trim()!==value||/[^\x21-\x7e]/u.test(value)){throw new SlopcameraCloudError("AUTHENTICATION_REQUIRED","The selected Vercel AI Gateway credential is invalid.")}return value}function environment(injected){return injected??process.env}function resolveSlopcameraGatewayCredential(injected){const values=environment(injected);if(values.AI_GATEWAY_API_KEY!==undefined){return{source:"AI_GATEWAY_API_KEY",token:credentialValue(values.AI_GATEWAY_API_KEY)}}if(values.VERCEL_OIDC_TOKEN!==undefined){return{source:"VERCEL_OIDC_TOKEN",token:credentialValue(values.VERCEL_OIDC_TOKEN)}}throw new SlopcameraCloudError("AUTHENTICATION_REQUIRED","Set AI_GATEWAY_API_KEY or run Slopcamera through `vercel env run -- \u2026` with VERCEL_OIDC_TOKEN available.")}function slopcameraGatewayCredentialStatus(injected){const values=environment(injected);if(values.AI_GATEWAY_API_KEY!==undefined){credentialValue(values.AI_GATEWAY_API_KEY);return{available:true,source:"AI_GATEWAY_API_KEY"}}if(values.VERCEL_OIDC_TOKEN!==undefined){credentialValue(values.VERCEL_OIDC_TOKEN);return{available:true,source:"VERCEL_OIDC_TOKEN"}}return{available:false,source:null}}function validateModel(value){if(typeof value!=="string"||value.length<3||value.length>256||!/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._:-]*$/iu.test(value)){invalidArgument("Model must be a bounded Vercel AI Gateway provider/model id.")}return value}function validatePrompt(value){if(typeof value!=="string"||value.trim().length===0||/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u.test(value)||Buffer.byteLength(value,"utf8")>slopcameraMaximumPromptBytes){invalidArgument(`Prompt must be non-empty and no more than ${slopcameraMaximumPromptBytes} UTF-8 bytes.`)}return value}function validateTimeout(value){const timeout=value??defaultGenerationTimeoutMs;if(!Number.isSafeInteger(timeout)||timeout<1000||timeout>maximumGenerationTimeoutMs){invalidArgument(`timeoutMs must be an integer from 1000 through ${maximumGenerationTimeoutMs}.`)}return timeout}function isObject(value){return typeof value==="object"&&value!==null||typeof value==="function"}function parseFunction(value){if(typeof value!=="function"){throw new SlopcameraCloudError("GENERATION_FAILED","The Vercel AI Gateway runtime is unavailable.")}return value}function disableAiSdkWarningLogging(){globalThis.AI_SDK_LOG_WARNINGS=false}function assertGenerationActive(signal){if(signal.aborted){throw new SlopcameraCloudError("GENERATION_FAILED","Vercel AI Gateway image generation was cancelled or exceeded its deadline.")}}async function loadDefaultGatewayRuntime(){let aiModule;let gatewayModule;try{[aiModule,gatewayModule]=await Promise.all([import("ai-v7"),import("@ai-sdk/gateway-v4")])}catch{throw new SlopcameraCloudError("GENERATION_FAILED","The Vercel AI Gateway runtime is unavailable.")}if(!isObject(aiModule)||!isObject(gatewayModule)){throw new SlopcameraCloudError("GENERATION_FAILED","The Vercel AI Gateway runtime is unavailable.")}const generateImage=parseFunction(aiModule.generateImage);if(typeof gatewayModule.createGateway!=="function"){throw new SlopcameraCloudError("GENERATION_FAILED","The Vercel AI Gateway runtime is unavailable.")}const createGateway=gatewayModule.createGateway;return{createGateway:(settings)=>{const provider=createGateway(settings);if(!isObject(provider)||typeof provider.imageModel!=="function"){throw new SlopcameraCloudError("GENERATION_FAILED","The Vercel AI Gateway runtime is unavailable.")}const imageModel=provider.imageModel;return{imageModel:(modelId)=>imageModel.call(provider,modelId)}},generateImage}}function gatewayUrl(input){try{if(input instanceof Request)return new URL(input.url);if(input instanceof URL)return new URL(input.href);return new URL(input,slopcameraGatewayApiBaseUrl)}catch{throw new SlopcameraCloudError("GENERATION_FAILED","The Vercel AI Gateway request was rejected.")}}function canonicalGatewayInput(input,url){if(!(input instanceof Request))return url;const snapshot=new Request(input);const body=snapshot.method==="GET"||snapshot.method==="HEAD"?undefined:snapshot.body??undefined;const requestInit={headers:snapshot.headers,method:snapshot.method,signal:snapshot.signal,...body===undefined?{}:{body,duplex:"half"}};return new Request(url.href,requestInit)}function boundedResponse(response,maximumBytes){const declared=response.headers.get("content-length");if(declared!==null){const value=Number(declared);if(!Number.isSafeInteger(value)||value<0||value>maximumBytes){response.body?.cancel().catch(()=>{return});throw new SlopcameraCloudError("GENERATION_INVALID_RESPONSE","Vercel AI Gateway returned an invalid bounded response.")}}if(response.body===null)return response;const reader=response.body.getReader();let bytes=0;const body=new ReadableStream({cancel:async(reason)=>await reader.cancel(reason),pull:async(controller)=>{try{const next=await reader.read();if(next.done){reader.releaseLock();controller.close();return}bytes+=next.value.byteLength;if(bytes>maximumBytes){await reader.cancel().catch(()=>{return});controller.error(new SlopcameraCloudError("GENERATION_INVALID_RESPONSE","Vercel AI Gateway returned an invalid bounded response."));return}controller.enqueue(next.value)}catch{controller.error(new SlopcameraCloudError("GENERATION_INVALID_RESPONSE","Vercel AI Gateway returned an invalid bounded response."))}}});return new Response(body,{headers:response.headers,status:response.status,statusText:response.statusText})}function createFixedGatewayFetch(options={}){const fetchImplementation=options.fetch??globalThis.fetch;const maximumResponseBytes=options.maximumResponseBytes??defaultMaximumGatewayResponseBytes;if(!Number.isSafeInteger(maximumResponseBytes)||maximumResponseBytes<1||maximumResponseBytes>1024*1024*1024){invalidArgument("maximumResponseBytes is outside the supported range.")}const fixed=new URL(slopcameraGatewayApiBaseUrl);return async(input,init)=>{const url=gatewayUrl(input);if(url.origin!==fixed.origin||url.pathname!==fixed.pathname&&!url.pathname.startsWith(`${fixed.pathname}/`)||url.username!==""||url.password!==""){throw new SlopcameraCloudError("GENERATION_FAILED","The Vercel AI Gateway request was rejected.")}const canonicalInput=canonicalGatewayInput(input,url);let response;try{response=await fetchImplementation(canonicalInput,{...init,redirect:"error"})}catch(error){if(error instanceof SlopcameraCloudError)throw error;throw new SlopcameraCloudError("GENERATION_FAILED","Vercel AI Gateway image generation failed; the request was not retried.")}if(response.redirected||response.status>=300&&response.status<400){await response.body?.cancel().catch(()=>{return});throw new SlopcameraCloudError("GENERATION_FAILED","The Vercel AI Gateway request was rejected.")}return boundedResponse(response,maximumResponseBytes)}}function combineSignals(caller,timeoutMs){const controller=new AbortController;const abort=()=>controller.abort(caller?.reason);let rejectInterruption;const interruption=new Promise((_resolve,reject)=>{rejectInterruption=reject});const rejectOnAbort=()=>{rejectInterruption?.(new SlopcameraCloudError("GENERATION_FAILED","Vercel AI Gateway image generation was cancelled or exceeded its deadline."))};controller.signal.addEventListener("abort",rejectOnAbort,{once:true});if(caller?.aborted===true)abort();caller?.addEventListener("abort",abort,{once:true});const timer=setTimeout(()=>controller.abort(),timeoutMs);return{dispose:()=>{clearTimeout(timer);caller?.removeEventListener("abort",abort);controller.signal.removeEventListener("abort",rejectOnAbort);rejectInterruption=undefined},interruption,signal:controller.signal}}function mediaType(value){if(typeof value!=="string"||!slopcameraResponseMediaTypes.includes(value)){throw new SlopcameraCloudError("GENERATION_INVALID_RESPONSE","Vercel AI Gateway returned an unsupported image type.")}return value}function validImageBytes(bytes,type){if(bytes.byteLength<12||bytes.byteLength>slopcameraMaximumRawImageBytes){return false}if(type==="image/png"){return Buffer.from(bytes.subarray(0,8)).equals(Buffer.from([137,80,78,71,13,10,26,10]))}if(type==="image/jpeg"){return bytes[0]===255&&bytes[1]===216&&bytes.at(-2)===255&&bytes.at(-1)===217}return bytes.byteLength>=16&&Buffer.from(bytes.subarray(0,4)).toString("ascii")==="RIFF"&&Buffer.from(bytes).readUInt32LE(4)===bytes.byteLength-8&&Buffer.from(bytes.subarray(8,12)).toString("ascii")==="WEBP"&&["VP8 ","VP8L","VP8X"].includes(Buffer.from(bytes.subarray(12,16)).toString("ascii"))}function warningReceipt(value){let type="provider-warning";let detail="Provider warning";if(typeof value==="string"){detail=value}else if(isObject(value)){if(typeof value.type==="string"&&value.type.length<=64){type=value.type.replace(/[^a-z0-9._-]/giu,"-")||type}if(typeof value.message==="string")detail=value.message;else if(typeof value.details==="string")detail=value.details}return`${type} sha256:${createHash("sha256").update(detail).digest("hex")}`}function parseResult(value,model){if(!isObject(value)||!Array.isArray(value.images)||value.images.length!==1){throw new SlopcameraCloudError("GENERATION_INVALID_RESPONSE","Vercel AI Gateway did not return exactly one image.")}const image=value.images[0];if(!isObject(image)||!(image.uint8Array instanceof Uint8Array)){throw new SlopcameraCloudError("GENERATION_INVALID_RESPONSE","Vercel AI Gateway returned an invalid bounded image.")}const type=mediaType(image.mediaType);if(!validImageBytes(image.uint8Array,type)){throw new SlopcameraCloudError("GENERATION_INVALID_RESPONSE","Vercel AI Gateway returned an invalid bounded image.")}const gateway=isObject(value.providerMetadata)&&isObject(value.providerMetadata.gateway)?value.providerMetadata.gateway:undefined;const foreignGenerationId=gateway!==undefined&&typeof gateway.generationId==="string"&&gateway.generationId.length>0&&gateway.generationId.length<=256&&!/[\u0000-\u001f\u007f]/u.test(gateway.generationId)?gateway.generationId:randomUUID();const requestId=`sha256:${createHash("sha256").update("slopcamera.gateway-generation-id/v1\x00").update(foreignGenerationId).digest("hex")}`;const warnings=Array.isArray(value.warnings)?value.warnings.slice(0,100).map(warningReceipt):[];return{bytes:image.uint8Array,response:{image:{base64:Buffer.from(image.uint8Array).toString("base64"),mediaType:type},model,provider:"vercel-ai-gateway",requestId,warnings}}}async function performGeneration(input,dependencies){const model=validateModel(input.model);const prompt=validatePrompt(input.prompt);const credential=resolveSlopcameraGatewayCredential(dependencies.environment);const timeout=combineSignals(input.signal,validateTimeout(input.timeoutMs));try{const generation=(async()=>{assertGenerationActive(timeout.signal);disableAiSdkWarningLogging();const runtime=await(dependencies.loadRuntime??loadDefaultGatewayRuntime)();assertGenerationActive(timeout.signal);const provider=runtime.createGateway({apiKey:credential.token,baseURL:slopcameraGatewayApiBaseUrl,fetch:createFixedGatewayFetch({...dependencies.fetch===undefined?{}:{fetch:dependencies.fetch},...dependencies.maximumResponseBytes===undefined?{}:{maximumResponseBytes:dependencies.maximumResponseBytes}})});assertGenerationActive(timeout.signal);const generated=await runtime.generateImage({abortSignal:timeout.signal,maxRetries:0,model:provider.imageModel(model),n:1,prompt});return parseResult(generated,model)})();return await Promise.race([generation,timeout.interruption])}catch(error){if(error instanceof SlopcameraCloudError)throw error;throw new SlopcameraCloudError("GENERATION_FAILED","Vercel AI Gateway image generation failed; the request was not retried.")}finally{timeout.dispose()}}async function generateSlopcameraImage(input,dependencies={}){return(await performGeneration(input,dependencies)).response}function expectedMediaType(outputPath){const extension=extname(outputPath).toLocaleLowerCase("en-US");if(extension===".png")return"image/png";if(extension===".jpg"||extension===".jpeg")return"image/jpeg";if(extension===".webp")return"image/webp";invalidArgument("Output path must end in .png, .jpg, .jpeg, or .webp.")}async function atomicImageWrite(outputPath,bytes){const absolutePath=resolve(outputPath);const temporaryPath=resolve(dirname(absolutePath),`.${randomUUID()}.slopcamera-generate.tmp`);try{await writeFile(temporaryPath,bytes,{flag:"wx"});await link(temporaryPath,absolutePath);return absolutePath}catch{throw new SlopcameraCloudError("OUTPUT_WRITE_FAILED","Slopcamera could not atomically write the generated image.")}finally{await rm(temporaryPath,{force:true}).catch(()=>{return})}}async function generateSlopcameraImageFile(input,dependencies={}){if(typeof input.outputPath!=="string"||input.outputPath.length<1||input.outputPath.length>4096||input.outputPath.includes("\x00")){invalidArgument("Output path must be a non-empty local path.")}const expected=expectedMediaType(input.outputPath);const generated=await performGeneration(input,dependencies);if(generated.response.image.mediaType!==expected){throw new SlopcameraCloudError("GENERATION_INVALID_RESPONSE",`Generated ${generated.response.image.mediaType} does not match the requested ${expected} output path.`)}const outputPath=await atomicImageWrite(input.outputPath,generated.bytes);return{bytes:generated.bytes.byteLength,mediaType:generated.response.image.mediaType,model:generated.response.model,outputPath,provider:generated.response.provider,requestId:generated.response.requestId,sha256:createHash("sha256").update(generated.bytes).digest("hex"),warnings:generated.response.warnings}}
3
- export{SlopcameraCloudError,slopcameraGatewayApiBaseUrl,slopcameraImageModels,slopcameraResponseMediaTypes,slopcameraMaximumPromptBytes,slopcameraMaximumRawImageBytes,resolveSlopcameraGatewayCredential,slopcameraGatewayCredentialStatus,createFixedGatewayFetch,generateSlopcameraImage,generateSlopcameraImageFile};