@getjack/jack 0.1.24 → 0.1.26

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.
@@ -0,0 +1,181 @@
1
+ # AI and Vectorize Bindings in Jack Cloud
2
+
3
+ ## Quick Reference for Coding Agents
4
+
5
+ **DO NOT add `[ai]` or `[[vectorize]]` bindings directly for jack cloud projects.**
6
+
7
+ Jack cloud uses metered proxies for AI and Vectorize calls. Use the patterns below:
8
+
9
+ ### AI Pattern
10
+
11
+ ```typescript
12
+ import { createJackAI } from "./jack-ai";
13
+
14
+ function getAI(env: Env) {
15
+ if (env.__AI_PROXY && env.__JACK_PROJECT_ID && env.__JACK_ORG_ID) {
16
+ return createJackAI(env);
17
+ }
18
+ if (env.AI) {
19
+ return env.AI;
20
+ }
21
+ throw new Error("No AI binding available");
22
+ }
23
+
24
+ // Usage:
25
+ const ai = getAI(env);
26
+ const result = await ai.run("@cf/meta/llama-3.2-1b-instruct", { messages });
27
+ ```
28
+
29
+ ### Vectorize Pattern
30
+
31
+ ```typescript
32
+ import { createJackVectorize } from "./jack-vectorize";
33
+
34
+ const INDEX_NAME = "my-vectors";
35
+
36
+ function getVectorize(env: Env) {
37
+ if (env.__VECTORIZE_PROXY && env.__JACK_PROJECT_ID && env.__JACK_ORG_ID) {
38
+ return createJackVectorize(env, INDEX_NAME);
39
+ }
40
+ if (env.VECTORS) {
41
+ return env.VECTORS;
42
+ }
43
+ throw new Error("No Vectorize binding available");
44
+ }
45
+
46
+ // Usage:
47
+ const vectors = getVectorize(env);
48
+ const results = await vectors.query(embedding, { topK: 10 });
49
+ await vectors.insert([{ id: "doc1", values: embedding, metadata: {} }]);
50
+ ```
51
+
52
+ ## Why This Pattern?
53
+
54
+ Jack cloud is multi-tenant. All users share Cloudflare's AI quota (10k neurons/day). Without metering:
55
+ - One user could exhaust the daily quota for everyone
56
+ - No visibility into per-project usage
57
+ - No ability to bill for AI usage
58
+
59
+ The proxy:
60
+ 1. Checks per-project quota
61
+ 2. Logs usage to Analytics Engine
62
+ 3. Forwards to real AI binding
63
+ 4. Returns response unchanged
64
+
65
+ ## Environment Bindings
66
+
67
+ ### Jack Cloud (Managed Deploy)
68
+
69
+ Control plane injects these bindings:
70
+ - `__AI_PROXY` - Service binding to jack-binding-proxy for AI
71
+ - `__VECTORIZE_PROXY` - Service binding to jack-binding-proxy for Vectorize
72
+ - `__JACK_PROJECT_ID` - Project ID for metering
73
+ - `__JACK_ORG_ID` - Organization ID for billing
74
+
75
+ `env.AI` and `env.VECTORS` are **NOT available** in jack cloud. Direct calls will fail.
76
+
77
+ ### Local Development
78
+
79
+ wrangler.jsonc provides:
80
+ - `AI` - Direct Cloudflare AI binding for local testing
81
+ - `VECTORS` - Direct Vectorize binding for local testing
82
+
83
+ The helper functions automatically use the right binding based on environment.
84
+
85
+ ## Template Pattern
86
+
87
+ ### AI Templates
88
+
89
+ 1. **src/jack-ai.ts** - Client wrapper (copy from ai-chat or semantic-search template)
90
+
91
+ 2. **Env interface** with optional bindings:
92
+ ```typescript
93
+ interface Env {
94
+ AI?: Ai; // Local dev
95
+ __AI_PROXY?: Fetcher; // Jack cloud
96
+ __JACK_PROJECT_ID?: string; // Jack cloud
97
+ __JACK_ORG_ID?: string; // Jack cloud
98
+ }
99
+ ```
100
+
101
+ 3. **getAI() helper** that handles both environments
102
+
103
+ 4. **wrangler.jsonc** with AI binding for local dev only:
104
+ ```jsonc
105
+ {
106
+ "ai": { "binding": "AI" }
107
+ }
108
+ ```
109
+
110
+ ### Vectorize Templates
111
+
112
+ 1. **src/jack-vectorize.ts** - Client wrapper (copy from semantic-search template)
113
+
114
+ 2. **Env interface** with optional bindings:
115
+ ```typescript
116
+ interface Env {
117
+ VECTORS?: VectorizeIndex; // Local dev
118
+ __VECTORIZE_PROXY?: Fetcher; // Jack cloud
119
+ __JACK_PROJECT_ID?: string; // Jack cloud
120
+ __JACK_ORG_ID?: string; // Jack cloud
121
+ }
122
+ ```
123
+
124
+ 3. **getVectorize() helper** that handles both environments
125
+
126
+ 4. **wrangler.jsonc** with Vectorize binding for local dev only:
127
+ ```jsonc
128
+ {
129
+ "vectorize": [{
130
+ "binding": "VECTORS",
131
+ "index_name": "my-vectors",
132
+ "preset": "cloudflare"
133
+ }]
134
+ }
135
+ ```
136
+
137
+ ## Error Handling
138
+
139
+ Quota exceeded returns 429:
140
+
141
+ ### AI Quota
142
+ ```typescript
143
+ try {
144
+ const result = await ai.run(model, inputs);
145
+ } catch (error) {
146
+ if (error.code === "AI_QUOTA_EXCEEDED") {
147
+ // Daily limit (1000 requests) reached, resets at midnight UTC
148
+ console.log(`Retry in ${error.resetIn} seconds`);
149
+ }
150
+ }
151
+ ```
152
+
153
+ ### Vectorize Quota
154
+ ```typescript
155
+ try {
156
+ const results = await vectors.query(embedding, { topK: 10 });
157
+ } catch (error) {
158
+ if (error.code === "VECTORIZE_QUERY_QUOTA_EXCEEDED") {
159
+ // Query limit (33,000/day) reached
160
+ console.log(`Retry in ${error.resetIn} seconds`);
161
+ }
162
+ if (error.code === "VECTORIZE_MUTATION_QUOTA_EXCEEDED") {
163
+ // Mutation limit (10,000/day) reached
164
+ console.log(`Retry in ${error.resetIn} seconds`);
165
+ }
166
+ }
167
+ ```
168
+
169
+ ## BYOC Mode
170
+
171
+ For Bring Your Own Cloud deployments:
172
+ - User configures their own Cloudflare account
173
+ - Direct AI binding is used (no proxy)
174
+ - No metering (it's their account)
175
+ - Standard Cloudflare docs apply
176
+
177
+ ## See Also
178
+
179
+ - `/docs/internal/specs/binding-proxy-worker.md` - Full architecture spec
180
+ - `apps/binding-proxy-worker/` - Proxy implementation
181
+ - `apps/control-plane/src/deployment-service.ts` - Binding injection logic
@@ -6,6 +6,36 @@
6
6
  2. **Templates are for user code, not jack's tools** - wrangler is jack's responsibility (installed globally), not the template's
7
7
  3. **Ship a lockfile** - `bun.lock` provides 70% faster installs on cold cache
8
8
 
9
+ ## AI Bindings (IMPORTANT)
10
+
11
+ **For jack cloud projects, DO NOT use direct AI bindings.**
12
+
13
+ See [AI-BINDINGS.md](./AI-BINDINGS.md) for the full pattern.
14
+
15
+ Quick summary:
16
+ - Jack cloud uses a metered proxy (`__AI_PROXY`) instead of direct `env.AI`
17
+ - Templates must include `src/jack-ai.ts` wrapper
18
+ - Use `getAI(env)` helper instead of `env.AI` directly
19
+ - wrangler.jsonc still declares `ai: { binding: "AI" }` for local dev only
20
+
21
+ ```typescript
22
+ // WRONG for jack cloud:
23
+ const result = await env.AI.run(model, inputs);
24
+
25
+ // CORRECT (works for both local dev and jack cloud):
26
+ import { createJackAI } from "./jack-ai";
27
+
28
+ function getAI(env: Env) {
29
+ if (env.__AI_PROXY && env.__JACK_PROJECT_ID && env.__JACK_ORG_ID) {
30
+ return createJackAI(env);
31
+ }
32
+ return env.AI;
33
+ }
34
+
35
+ const ai = getAI(env);
36
+ const result = await ai.run(model, inputs);
37
+ ```
38
+
9
39
  ## Dependency Rules
10
40
 
11
41
  ### DO include in templates:
@@ -4,7 +4,7 @@
4
4
  "secrets": [],
5
5
  "capabilities": ["ai"],
6
6
  "intent": {
7
- "keywords": ["ai", "chat", "llm", "mistral", "completion", "chatbot"],
7
+ "keywords": ["ai", "chat", "llm", "llama", "completion", "chatbot"],
8
8
  "examples": ["AI chatbot", "chat interface", "LLM chat app"]
9
9
  },
10
10
  "hooks": {
@@ -16,12 +16,11 @@
16
16
  },
17
17
  {
18
18
  "action": "box",
19
- "title": "AI Chat: {{name}}",
19
+ "title": "{{name}}",
20
20
  "lines": [
21
21
  "{{url}}",
22
22
  "",
23
- "Open in browser to start chatting!",
24
- "Rate limit: 10 requests/minute"
23
+ "jack open to view in browser"
25
24
  ]
26
25
  }
27
26
  ]
@@ -1,13 +1,46 @@
1
+ import { createJackAI } from "./jack-ai";
2
+
1
3
  interface Env {
2
- AI: Ai;
4
+ // Direct AI binding (for local dev with wrangler)
5
+ AI?: Ai;
6
+ // Jack proxy bindings (injected in jack cloud)
7
+ __AI_PROXY?: Fetcher;
8
+ __JACK_PROJECT_ID?: string;
9
+ __JACK_ORG_ID?: string;
10
+ // Assets binding
3
11
  ASSETS: Fetcher;
4
12
  }
5
13
 
14
+ function getAI(env: Env) {
15
+ // Prefer jack cloud proxy if available (for metering)
16
+ if (env.__AI_PROXY && env.__JACK_PROJECT_ID && env.__JACK_ORG_ID) {
17
+ return createJackAI(env as Required<Pick<Env, "__AI_PROXY" | "__JACK_PROJECT_ID" | "__JACK_ORG_ID">>);
18
+ }
19
+ // Fallback to direct binding for local dev
20
+ if (env.AI) {
21
+ return env.AI;
22
+ }
23
+ throw new Error("No AI binding available");
24
+ }
25
+
6
26
  interface ChatMessage {
7
27
  role: "user" | "assistant" | "system";
8
28
  content: string;
9
29
  }
10
30
 
31
+ // System prompt - customize this to change the AI's personality
32
+ const SYSTEM_PROMPT = `You are a helpful AI assistant built with jack (getjack.sh).
33
+
34
+ jack helps developers ship ideas fast - from "what if" to a live URL in seconds. You're running on Cloudflare's edge network, close to users worldwide.
35
+
36
+ Be concise, friendly, and helpful. If asked about jack:
37
+ - jack new creates projects from templates
38
+ - jack ship deploys to production
39
+ - jack open opens your app in browser
40
+ - Docs: https://docs.getjack.sh
41
+
42
+ Focus on being useful. Keep responses short unless detail is needed.`;
43
+
11
44
  // Rate limiting: 10 requests per minute per IP
12
45
  const RATE_LIMIT = 10;
13
46
  const RATE_WINDOW_MS = 60_000;
@@ -65,7 +98,7 @@ export default {
65
98
 
66
99
  try {
67
100
  const body = (await request.json()) as { messages?: ChatMessage[] };
68
- const messages = body.messages;
101
+ let messages = body.messages;
69
102
 
70
103
  if (!messages || !Array.isArray(messages)) {
71
104
  return Response.json(
@@ -74,9 +107,16 @@ export default {
74
107
  );
75
108
  }
76
109
 
77
- // Stream response using SSE
78
- const stream = await env.AI.run(
79
- "@cf/mistral/mistral-7b-instruct-v0.1",
110
+ // Prepend system prompt if not already present
111
+ if (messages.length === 0 || messages[0].role !== "system") {
112
+ messages = [{ role: "system", content: SYSTEM_PROMPT }, ...messages];
113
+ }
114
+
115
+ // Stream response using Llama 3.2 1B - cheapest model with good quality
116
+ // See: https://developers.cloudflare.com/workers-ai/models/
117
+ const ai = getAI(env);
118
+ const stream = await ai.run(
119
+ "@cf/meta/llama-3.2-1b-instruct",
80
120
  {
81
121
  messages,
82
122
  stream: true,
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Jack AI Client - Drop-in replacement for Cloudflare AI binding.
3
+ *
4
+ * This wrapper provides the same interface as env.AI but routes calls
5
+ * through jack's binding proxy for metering and quota enforcement.
6
+ *
7
+ * Usage in templates:
8
+ * ```typescript
9
+ * import { createJackAI } from "./jack-ai";
10
+ *
11
+ * interface Env {
12
+ * __AI_PROXY: Fetcher; // Service binding to binding-proxy worker
13
+ * __JACK_PROJECT_ID: string; // Injected by control plane
14
+ * __JACK_ORG_ID: string; // Injected by control plane
15
+ * }
16
+ *
17
+ * export default {
18
+ * async fetch(request: Request, env: Env) {
19
+ * const AI = createJackAI(env);
20
+ * const result = await AI.run("@cf/meta/llama-3.2-1b-instruct", { messages });
21
+ * // Works exactly like env.AI.run()
22
+ * }
23
+ * };
24
+ * ```
25
+ *
26
+ * The wrapper is transparent - it accepts the same parameters as env.AI.run()
27
+ * and returns the same response types, including streaming.
28
+ */
29
+
30
+ interface JackAIEnv {
31
+ __AI_PROXY: Fetcher;
32
+ __JACK_PROJECT_ID: string;
33
+ __JACK_ORG_ID: string;
34
+ }
35
+
36
+ /**
37
+ * Creates a Jack AI client that mirrors the Cloudflare AI binding interface.
38
+ *
39
+ * @param env - Worker environment with jack proxy bindings
40
+ * @returns AI-compatible object with run() method
41
+ */
42
+ export function createJackAI(env: JackAIEnv): {
43
+ run: <T = unknown>(
44
+ model: string,
45
+ inputs: unknown,
46
+ options?: unknown,
47
+ ) => Promise<T | ReadableStream>;
48
+ } {
49
+ return {
50
+ async run<T = unknown>(
51
+ model: string,
52
+ inputs: unknown,
53
+ options?: unknown,
54
+ ): Promise<T | ReadableStream> {
55
+ const response = await env.__AI_PROXY.fetch("http://internal/ai/run", {
56
+ method: "POST",
57
+ headers: {
58
+ "Content-Type": "application/json",
59
+ "X-Jack-Project-ID": env.__JACK_PROJECT_ID,
60
+ "X-Jack-Org-ID": env.__JACK_ORG_ID,
61
+ },
62
+ body: JSON.stringify({ model, inputs, options }),
63
+ });
64
+
65
+ // Handle quota exceeded
66
+ if (response.status === 429) {
67
+ const error = await response.json();
68
+ const quotaError = new Error((error as { message?: string }).message || "AI quota exceeded");
69
+ (quotaError as Error & { code: string }).code = "AI_QUOTA_EXCEEDED";
70
+ (quotaError as Error & { resetIn?: number }).resetIn = (error as { resetIn?: number }).resetIn;
71
+ throw quotaError;
72
+ }
73
+
74
+ // Handle other errors
75
+ if (!response.ok) {
76
+ const error = await response.json();
77
+ throw new Error((error as { error?: string }).error || "AI request failed");
78
+ }
79
+
80
+ // Handle streaming response
81
+ const contentType = response.headers.get("Content-Type");
82
+ if (contentType?.includes("text/event-stream")) {
83
+ return response.body as ReadableStream;
84
+ }
85
+
86
+ // Handle JSON response
87
+ return response.json() as Promise<T>;
88
+ },
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Type-safe wrapper that infers return types based on model.
94
+ * For advanced users who want full type safety.
95
+ */
96
+ export type JackAI = ReturnType<typeof createJackAI>;
@@ -7,18 +7,17 @@
7
7
  "keywords": ["rag", "semantic", "search", "embeddings", "vectorize", "vector", "similarity"],
8
8
  "examples": ["RAG app", "document search", "semantic search", "knowledge base"]
9
9
  },
10
+ "agentContext": "Local development requires 'wrangler dev --remote' for AI and Vectorize bindings.",
10
11
  "hooks": {
11
12
  "postDeploy": [
12
13
  { "action": "clipboard", "text": "{{url}}", "message": "URL copied" },
13
14
  {
14
15
  "action": "box",
15
- "title": "Semantic Search: {{name}}",
16
+ "title": "{{name}}",
16
17
  "lines": [
17
18
  "{{url}}",
18
19
  "",
19
- "Open in browser to index documents and search!",
20
- "",
21
- "Note: Local dev requires 'wrangler dev --remote'"
20
+ "jack open to view in browser"
22
21
  ]
23
22
  }
24
23
  ]
@@ -1,10 +1,64 @@
1
+ import { createJackAI, type JackAI } from "./jack-ai";
2
+ import { createJackVectorize, type JackVectorize } from "./jack-vectorize";
3
+
1
4
  interface Env {
2
- AI: Ai;
3
- VECTORS: VectorizeIndex;
5
+ // Direct bindings (for local dev with wrangler)
6
+ AI?: Ai;
7
+ VECTORS?: VectorizeIndex;
8
+ // Jack proxy bindings (injected in jack cloud)
9
+ __AI_PROXY?: Fetcher;
10
+ __VECTORIZE_PROXY?: Fetcher;
11
+ __JACK_PROJECT_ID?: string;
12
+ __JACK_ORG_ID?: string;
13
+ // Other bindings
4
14
  DB: D1Database;
5
15
  ASSETS: Fetcher;
6
16
  }
7
17
 
18
+ // Index name must match wrangler.jsonc vectorize config
19
+ const VECTORIZE_INDEX_NAME = "jack-template-vectors";
20
+
21
+ // Minimal AI interface for embedding generation
22
+ type AIClient = {
23
+ run: (model: string, inputs: { text: string }) => Promise<{ data: number[][] } | unknown>;
24
+ };
25
+
26
+ function getAI(env: Env): AIClient {
27
+ // Prefer jack cloud proxy if available (for metering)
28
+ if (env.__AI_PROXY && env.__JACK_PROJECT_ID && env.__JACK_ORG_ID) {
29
+ return createJackAI(env as Required<Pick<Env, "__AI_PROXY" | "__JACK_PROJECT_ID" | "__JACK_ORG_ID">>) as AIClient;
30
+ }
31
+ // Fallback to direct binding for local dev
32
+ if (env.AI) {
33
+ return env.AI as unknown as AIClient;
34
+ }
35
+ throw new Error("No AI binding available");
36
+ }
37
+
38
+ // Minimal Vectorize interface
39
+ type VectorizeClient = {
40
+ insert: (vectors: { id: string; values: number[]; metadata?: Record<string, unknown> }[]) => Promise<unknown>;
41
+ query: (
42
+ vector: number[],
43
+ options?: { topK?: number; returnMetadata?: "none" | "indexed" | "all" },
44
+ ) => Promise<{ matches: { id: string; score: number; metadata?: Record<string, unknown> }[] }>;
45
+ };
46
+
47
+ function getVectorize(env: Env): VectorizeClient {
48
+ // Prefer jack cloud proxy if available (for metering)
49
+ if (env.__VECTORIZE_PROXY && env.__JACK_PROJECT_ID && env.__JACK_ORG_ID) {
50
+ return createJackVectorize(
51
+ env as Required<Pick<Env, "__VECTORIZE_PROXY" | "__JACK_PROJECT_ID" | "__JACK_ORG_ID">>,
52
+ VECTORIZE_INDEX_NAME,
53
+ ) as VectorizeClient;
54
+ }
55
+ // Fallback to direct binding for local dev
56
+ if (env.VECTORS) {
57
+ return env.VECTORS as unknown as VectorizeClient;
58
+ }
59
+ throw new Error("No Vectorize binding available");
60
+ }
61
+
8
62
  // Rate limiting: 10 requests per minute per IP
9
63
  const RATE_LIMIT = 10;
10
64
  const RATE_WINDOW_MS = 60_000;
@@ -29,17 +83,17 @@ function checkRateLimit(ip: string): boolean {
29
83
 
30
84
  /**
31
85
  * Extract embedding vector from AI response
32
- * Handles union type from @cf/baai/bge-base-en-v1.5
86
+ * Handles response from @cf/baai/bge-base-en-v1.5
33
87
  */
34
- function getEmbeddingVector(response: Awaited<ReturnType<Ai["run"]>>): number[] | null {
88
+ function getEmbeddingVector(response: unknown): number[] | null {
35
89
  if (
36
90
  response &&
37
91
  typeof response === "object" &&
38
92
  "data" in response &&
39
- Array.isArray(response.data) &&
40
- response.data.length > 0
93
+ Array.isArray((response as { data: unknown }).data) &&
94
+ (response as { data: unknown[] }).data.length > 0
41
95
  ) {
42
- return response.data[0] as number[];
96
+ return (response as { data: number[][] }).data[0];
43
97
  }
44
98
  return null;
45
99
  }
@@ -72,8 +126,9 @@ export default {
72
126
  return Response.json({ error: "Missing id or content" }, { status: 400 });
73
127
  }
74
128
 
75
- // Generate embedding using free Cloudflare AI
76
- const embedding = await env.AI.run("@cf/baai/bge-base-en-v1.5", {
129
+ // Generate embedding using Cloudflare AI
130
+ const ai = getAI(env);
131
+ const embedding = await ai.run("@cf/baai/bge-base-en-v1.5", {
77
132
  text: content,
78
133
  });
79
134
 
@@ -83,7 +138,8 @@ export default {
83
138
  }
84
139
 
85
140
  // Store in Vectorize
86
- await env.VECTORS.insert([
141
+ const vectors = getVectorize(env);
142
+ await vectors.insert([
87
143
  {
88
144
  id,
89
145
  values: embeddingVector,
@@ -117,7 +173,8 @@ export default {
117
173
  }
118
174
 
119
175
  // Generate query embedding
120
- const embedding = await env.AI.run("@cf/baai/bge-base-en-v1.5", {
176
+ const ai = getAI(env);
177
+ const embedding = await ai.run("@cf/baai/bge-base-en-v1.5", {
121
178
  text: query,
122
179
  });
123
180
 
@@ -127,7 +184,8 @@ export default {
127
184
  }
128
185
 
129
186
  // Search Vectorize
130
- const results = await env.VECTORS.query(embeddingVector, {
187
+ const vectors = getVectorize(env);
188
+ const results = await vectors.query(embeddingVector, {
131
189
  topK: limit,
132
190
  returnMetadata: "all",
133
191
  });
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Jack AI Client - Drop-in replacement for Cloudflare AI binding.
3
+ *
4
+ * This wrapper provides the same interface as env.AI but routes calls
5
+ * through jack's binding proxy for metering and quota enforcement.
6
+ *
7
+ * Usage in templates:
8
+ * ```typescript
9
+ * import { createJackAI } from "./jack-ai";
10
+ *
11
+ * interface Env {
12
+ * __AI_PROXY: Fetcher; // Service binding to binding-proxy worker
13
+ * __JACK_PROJECT_ID: string; // Injected by control plane
14
+ * __JACK_ORG_ID: string; // Injected by control plane
15
+ * }
16
+ *
17
+ * export default {
18
+ * async fetch(request: Request, env: Env) {
19
+ * const AI = createJackAI(env);
20
+ * const result = await AI.run("@cf/meta/llama-3.2-1b-instruct", { messages });
21
+ * // Works exactly like env.AI.run()
22
+ * }
23
+ * };
24
+ * ```
25
+ *
26
+ * The wrapper is transparent - it accepts the same parameters as env.AI.run()
27
+ * and returns the same response types, including streaming.
28
+ */
29
+
30
+ interface JackAIEnv {
31
+ __AI_PROXY: Fetcher;
32
+ __JACK_PROJECT_ID: string;
33
+ __JACK_ORG_ID: string;
34
+ }
35
+
36
+ /**
37
+ * Creates a Jack AI client that mirrors the Cloudflare AI binding interface.
38
+ *
39
+ * @param env - Worker environment with jack proxy bindings
40
+ * @returns AI-compatible object with run() method
41
+ */
42
+ export function createJackAI(env: JackAIEnv): {
43
+ run: <T = unknown>(
44
+ model: string,
45
+ inputs: unknown,
46
+ options?: unknown,
47
+ ) => Promise<T | ReadableStream>;
48
+ } {
49
+ return {
50
+ async run<T = unknown>(
51
+ model: string,
52
+ inputs: unknown,
53
+ options?: unknown,
54
+ ): Promise<T | ReadableStream> {
55
+ const response = await env.__AI_PROXY.fetch("http://internal/ai/run", {
56
+ method: "POST",
57
+ headers: {
58
+ "Content-Type": "application/json",
59
+ "X-Jack-Project-ID": env.__JACK_PROJECT_ID,
60
+ "X-Jack-Org-ID": env.__JACK_ORG_ID,
61
+ },
62
+ body: JSON.stringify({ model, inputs, options }),
63
+ });
64
+
65
+ // Handle quota exceeded
66
+ if (response.status === 429) {
67
+ const error = await response.json();
68
+ const quotaError = new Error((error as { message?: string }).message || "AI quota exceeded");
69
+ (quotaError as Error & { code: string }).code = "AI_QUOTA_EXCEEDED";
70
+ (quotaError as Error & { resetIn?: number }).resetIn = (error as { resetIn?: number }).resetIn;
71
+ throw quotaError;
72
+ }
73
+
74
+ // Handle other errors
75
+ if (!response.ok) {
76
+ const error = await response.json();
77
+ throw new Error((error as { error?: string }).error || "AI request failed");
78
+ }
79
+
80
+ // Handle streaming response
81
+ const contentType = response.headers.get("Content-Type");
82
+ if (contentType?.includes("text/event-stream")) {
83
+ return response.body as ReadableStream;
84
+ }
85
+
86
+ // Handle JSON response
87
+ return response.json() as Promise<T>;
88
+ },
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Type-safe wrapper that infers return types based on model.
94
+ * For advanced users who want full type safety.
95
+ */
96
+ export type JackAI = ReturnType<typeof createJackAI>;