@netlify/agent-runner-cli 1.29.1 → 1.30.1-broken.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 +6 -5
- package/dist/bin-local.d.ts +1 -0
- package/dist/bin-local.js +301 -0
- package/dist/bin.js +225 -78
- package/dist/index.d.ts +64 -12
- package/dist/index.js +211 -77
- package/dist/scripts/scaffold.d.ts +2 -0
- package/dist/scripts/scaffold.js +13 -0
- package/dist/skills/frontend-design/SKILL.md +86 -0
- package/dist/skills/general-database/SKILL.md +45 -0
- package/dist/skills/netlify-ai-gateway/SKILL.md +411 -0
- package/dist/skills/netlify-blobs/SKILL.md +421 -0
- package/dist/skills/netlify-database/SKILL.md +423 -0
- package/dist/skills/netlify-edge-functions/SKILL.md +478 -0
- package/dist/skills/netlify-forms/SKILL.md +358 -0
- package/dist/skills/netlify-forms/scripts/enable.cjs +12 -0
- package/dist/skills/netlify-functions/SKILL.md +478 -0
- package/dist/skills/netlify-identity/SKILL.md +632 -0
- package/dist/skills/netlify-identity/scripts/enable.cjs +12 -0
- package/dist/skills/netlify-image-cdn/SKILL.md +244 -0
- package/package.json +40 -28
- package/scripts/scenarios/install-skills.mjs +57 -0
- package/scripts/scenarios/netlify-deploy.mjs +311 -0
- package/scripts/scenarios/prewarm-template-mirror.mjs +69 -0
- package/patches/@google+gemini-cli+0.1.17.patch +0 -87
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import{spawnSync as w}from"child_process";import{existsSync as I,readdirSync as A,readFileSync as N,statSync as E,writeFileSync as x,mkdirSync as F}from"fs";import{createRequire as v}from"module";import i from"path";import e from"process";var L=".axis-scaffold-skip-install",S=v(import.meta.url);function C(){let t=S("@netlify/ts-cli/package.json"),n=i.dirname(S.resolve("@netlify/ts-cli/package.json"));return i.join(n,t.bin)}function $(t){let n=t.slice(2),s=n[0];(!s||s.startsWith("--"))&&(console.error("Usage: scaffold.js <template-id> [--package-manager npm|pnpm|yarn]"),e.exit(1));let o=n.indexOf("--package-manager"),r=o!==-1?n[o+1]??"":"";return{templateId:s,packageManager:r}}var b=new Set(["node_modules",".git",".netlify",".claude",".next","dist","build",".cache"]),T=["package.json","app.config.ts","app.config.js","vite.config.ts","vite.config.js","next.config.ts","next.config.js","next.config.mjs","remix.config.js","nuxt.config.ts","app/routes/__root.tsx","app/routes/__root.jsx","app/root.tsx","app/root.jsx","src/main.tsx","src/main.ts","src/App.tsx","app/layout.tsx","pages/_app.tsx"],D=4,j=1e4;function M(t){let n=[];for(let s of T){if(n.length>=D)break;let o=i.join(t,s);try{if(!I(o))continue;let r=N(o,"utf8");n.push({filePath:s,content:r.length>j?r.slice(0,j)+`
|
|
2
|
+
\u2026 (truncated)`:r})}catch{}}return n}function O(t,n=4){let s=[];function o(r,a){if(a>n)return;let f;try{f=A(r).sort()}catch{return}for(let m of f){if(b.has(m))continue;let p=i.join(r,m),y=i.relative(t,p),h=!1;try{h=E(p).isDirectory()}catch{continue}h?(s.push(y+"/"),o(p,a+1)):s.push(y)}}return o(t,0),s.join(`
|
|
3
|
+
`)}function P(t){let n=O(t),s=M(t),o=a=>i.extname(a).slice(1)||"text";return["## File tree\n\n```\n"+n+"\n```",...s.map(({filePath:a,content:f})=>`## ${a}
|
|
4
|
+
|
|
5
|
+
\`\`\`${o(a)}
|
|
6
|
+
${f}
|
|
7
|
+
\`\`\``)].join(`
|
|
8
|
+
|
|
9
|
+
`)}var{templateId:l,packageManager:g}=$(e.argv),K=C(),B=e.env.NVM_BIN?`${e.env.NVM_BIN}/node`:"node",d=[K,"--target-dir","./","--no-git","--json","--add-ons",l];g&&d.push("--package-manager",g);var k=e.env.AXIS_WORKSPACE,R=k!==void 0&&I(i.join(k,L));(e.env.NETLIFY_SCAFFOLD_NO_INSTALL==="1"||R)&&d.push("--no-install");var c=w(B,d,{stdio:["inherit","pipe","inherit"],encoding:"utf8"});c.status!==0&&(c.stdout&&e.stdout.write(c.stdout),e.exit(c.status??1));var u;try{u=JSON.parse(c.stdout)?.starter?.framework}catch{c.stdout&&e.stdout.write(c.stdout)}var Y={template:l,packageManager:g},_=u?` (${u})`:"";try{F(i.join(e.cwd(),".netlify"),{recursive:!0}),x(i.join(e.cwd(),".netlify","scaffold-result.json"),JSON.stringify(Y));let t=P(e.cwd());x(i.join(e.cwd(),".netlify","scaffold-tree.md"),t),console.log(`\u2713 Scaffolded "${l}" template${_}.
|
|
10
|
+
Bootstrap context written to .netlify/scaffold-tree.md \u2014 read that file now.
|
|
11
|
+
It contains the complete file tree and the contents of key project files.
|
|
12
|
+
Do not run ls, find, or read any other files before starting \u2014 everything you need is in that file.`)}catch{console.log(`\u2713 Scaffolded "${l}" template${_}.`)}
|
|
13
|
+
//# sourceMappingURL=scaffold.js.map
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: frontend-design
|
|
3
|
+
description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
|
|
7
|
+
|
|
8
|
+
The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.
|
|
9
|
+
|
|
10
|
+
## Design Thinking
|
|
11
|
+
|
|
12
|
+
Before coding, understand the context and commit to a BOLD aesthetic direction:
|
|
13
|
+
- **Purpose**: What problem does this interface solve? Who uses it?
|
|
14
|
+
- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
|
|
15
|
+
- **Constraints**: Technical requirements (framework, performance, accessibility).
|
|
16
|
+
- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
|
|
17
|
+
|
|
18
|
+
**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
|
|
19
|
+
|
|
20
|
+
Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
|
|
21
|
+
- Production-grade and functional
|
|
22
|
+
- Visually striking and memorable
|
|
23
|
+
- Cohesive with a clear aesthetic point-of-view
|
|
24
|
+
- Meticulously refined in every detail
|
|
25
|
+
|
|
26
|
+
## Design Variance
|
|
27
|
+
|
|
28
|
+
Use these baseline values to drive design decisions. Adapt them dynamically based on what the user requests:
|
|
29
|
+
|
|
30
|
+
* **DESIGN_VARIANCE: 8** (1=Perfect Symmetry, 10=Artsy Chaos) - Controls layout asymmetry, grid-breaking, and spatial experimentation.
|
|
31
|
+
* **MOTION_INTENSITY: 6** (1=Static, 10=Cinematic) - Controls animation complexity, from hover states only to full choreographed sequences.
|
|
32
|
+
* **VISUAL_DENSITY: 4** (1=Art Gallery/Airy, 10=Cockpit/Packed Data) - Controls spacing, whitespace, and information density.
|
|
33
|
+
|
|
34
|
+
These are defaults, not fixed rules. A luxury brand landing page might use (6, 4, 2) while a data dashboard could use (3, 5, 9). Let the context drive the values.
|
|
35
|
+
|
|
36
|
+
## Frontend Aesthetics Guidelines
|
|
37
|
+
|
|
38
|
+
Focus on:
|
|
39
|
+
- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
|
|
40
|
+
- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes. Stick to one palette for the entire output, do not fluctuate between warm and cool tones within the same project.
|
|
41
|
+
- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
|
|
42
|
+
- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density. Prefer CSS Grid over complex flexbox percentage math for reliable multi-column structures.
|
|
43
|
+
- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, and grain overlays.
|
|
44
|
+
|
|
45
|
+
## Interactive States
|
|
46
|
+
|
|
47
|
+
Do not generate only the "happy path" static state. Implement full interaction cycles:
|
|
48
|
+
- **Loading**: Skeleton loaders matching the layout shape, not generic spinners.
|
|
49
|
+
- **Empty States**: Beautifully composed empty states indicating how to populate data.
|
|
50
|
+
- **Error States**: Clear, inline error reporting (e.g., form validation).
|
|
51
|
+
- **Tactile Feedback**: Hover, active, and focus states that feel physical and responsive.
|
|
52
|
+
|
|
53
|
+
## Performance Guardrails
|
|
54
|
+
|
|
55
|
+
- Animate `transform` and `opacity`, never `top`, `left`, `width`, or `height`.
|
|
56
|
+
- Apply grain/noise filters to fixed, non-interactive overlays, never to scrolling containers.
|
|
57
|
+
- Use z-index intentionally for systemic layers (navbars, modals, overlays), not scattered arbitrarily.
|
|
58
|
+
- Ensure mobile layout gracefully collapses to single-column with adequate spacing.
|
|
59
|
+
|
|
60
|
+
## Forbidden Patterns (AI Tells)
|
|
61
|
+
|
|
62
|
+
NEVER use generic AI-generated aesthetics. Actively avoid these common signatures:
|
|
63
|
+
|
|
64
|
+
**Visual**:
|
|
65
|
+
- Overused font families (Inter, Roboto, Arial, system fonts). Choose distinctive, characterful fonts.
|
|
66
|
+
- Cliched color schemes, particularly purple/blue gradients on white. No neon outer glows.
|
|
67
|
+
- Pure black (#000000). Use off-black, dark gray, or tinted darks.
|
|
68
|
+
- Oversaturated accent colors. Desaturate to blend elegantly with neutrals.
|
|
69
|
+
- Predictable layouts, especially the generic "3 equal cards in a row" feature section. Use asymmetric grids, zig-zag layouts, or horizontal scrolling instead.
|
|
70
|
+
- Cookie-cutter centered hero sections. Try split-screen, left-aligned, or asymmetric compositions.
|
|
71
|
+
|
|
72
|
+
**Content (the "Jane Doe" effect)**:
|
|
73
|
+
- Generic placeholder names ("John Doe", "Jane Smith"). Invent creative, realistic names.
|
|
74
|
+
- Fake round numbers ("99.99%", "$50.00"). Use organic, messy data ("47.2%", "$38.74").
|
|
75
|
+
- Marketing filler words ("Elevate", "Seamless", "Unleash", "Next-Gen"). Use concrete, specific language.
|
|
76
|
+
- Generic brand names ("Acme", "Nexus", "SmartFlow"). Invent contextually appropriate names.
|
|
77
|
+
|
|
78
|
+
**Technical**:
|
|
79
|
+
- Emojis in UI text or alt text. Use proper icons or SVG primitives instead.
|
|
80
|
+
- Broken or unreliable image URLs. Use reliable placeholder services or inline SVGs.
|
|
81
|
+
|
|
82
|
+
Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.
|
|
83
|
+
|
|
84
|
+
**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.
|
|
85
|
+
|
|
86
|
+
Remember: extraordinary creative work is achievable. Push boundaries, think outside the box, and commit fully to a distinctive vision.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: general-database
|
|
3
|
+
description: Use this skill whenever the task involves storing, persisting, querying, or managing data — including CRUD operations, collections, records, user data, or any application state that needs to survive between requests. Covers both relational databases (Netlify Database) and object storage (Netlify Blobs).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Data Storage on Netlify
|
|
7
|
+
|
|
8
|
+
Prefer Netlify Database over Netlify Blobs for any application data — records, collections, lists, user-generated content,
|
|
9
|
+
or anything you would store as rows. Use Blobs only for files, binary assets (images, video), or cached responses.
|
|
10
|
+
|
|
11
|
+
## Application data: Netlify Database
|
|
12
|
+
|
|
13
|
+
**Netlify Database** is the recommended solution for storing application data. It provides a fully managed Postgres database
|
|
14
|
+
with zero configuration — install the `@netlify/database` package, and a database is automatically provisioned for your site.
|
|
15
|
+
|
|
16
|
+
Use Netlify Database when you need to store:
|
|
17
|
+
|
|
18
|
+
- Records, collections, or lists
|
|
19
|
+
- User-generated content
|
|
20
|
+
- Any data you would naturally model as rows in a table
|
|
21
|
+
- Data requiring queries, filtering, or transactions
|
|
22
|
+
|
|
23
|
+
There are two recommended approaches:
|
|
24
|
+
|
|
25
|
+
- **Drizzle ORM** (recommended): Use Drizzle ORM with the native Netlify Database adapter for type-safe schema
|
|
26
|
+
definitions and a query builder. Install `@netlify/database`, `drizzle-orm@beta`, and `drizzle-kit@beta`. The
|
|
27
|
+
`@beta` dist-tag is required — the Netlify Database adapter only exists on the beta release line.
|
|
28
|
+
- **Native `@netlify/database` driver**: Use the `sql` tagged template literal for direct SQL queries. Install `@netlify/database`.
|
|
29
|
+
|
|
30
|
+
IMPORTANT: Any code that assumes a different database schema requires a corresponding migration file in
|
|
31
|
+
`netlify/database/migrations/`. Without a migration, the database schema will not change and the application will break.
|
|
32
|
+
|
|
33
|
+
See the **netlify-database** skill for complete setup instructions and usage examples.
|
|
34
|
+
|
|
35
|
+
## Files and binary assets: Netlify Blobs
|
|
36
|
+
|
|
37
|
+
**Netlify Blobs** is the recommended solution for file and binary storage.
|
|
38
|
+
|
|
39
|
+
Use Netlify Blobs when you need:
|
|
40
|
+
|
|
41
|
+
- File and image storage
|
|
42
|
+
- Binary assets (images, video, PDFs)
|
|
43
|
+
- Cached responses
|
|
44
|
+
|
|
45
|
+
See the **netlify-blobs** skill for complete setup instructions and usage examples.
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: netlify-ai-gateway
|
|
3
|
+
description: Use Netlify AI Gateway for AI inference in Netlify Functions and server-side code. Use when adding AI/LLM features with OpenAI, Anthropic, or Google Gemini without managing API keys. Must be read before selecting or changing any AI model. Contains the list of supported models.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Netlify AI Gateway
|
|
7
|
+
|
|
8
|
+
> **IMPORTANT:** Only use models listed in the "Available Models" section below. AI Gateway does not support every model a provider offers. Using an unsupported model will cause runtime errors.
|
|
9
|
+
|
|
10
|
+
Zero-config AI inference for Netlify projects. Netlify automatically injects environment variables so official
|
|
11
|
+
provider SDKs work without API keys or configuration.
|
|
12
|
+
|
|
13
|
+
## How It Works
|
|
14
|
+
|
|
15
|
+
Netlify sets provider-specific environment variables in all compute contexts (Functions, Edge Functions, server-side
|
|
16
|
+
framework code). Official SDKs auto-detect these variables, so a default constructor like `new OpenAI()` works
|
|
17
|
+
out of the box. Requests are proxied through AI Gateway and billed to your Netlify account credits.
|
|
18
|
+
|
|
19
|
+
Netlify **never overrides** environment variables you have already set. The check is per-provider: if you set your own
|
|
20
|
+
`OPENAI_API_KEY`, Netlify will not set `OPENAI_API_KEY` or `OPENAI_BASE_URL`, but will still inject Anthropic and
|
|
21
|
+
Gemini variables independently.
|
|
22
|
+
|
|
23
|
+
AI Gateway requires a **credit-based plan** (Free, Personal, or Pro). It is not available on legacy pricing plans.
|
|
24
|
+
|
|
25
|
+
### Environment Variables
|
|
26
|
+
|
|
27
|
+
| Provider | Variables |
|
|
28
|
+
|----------|-----------|
|
|
29
|
+
| **Anthropic** | `ANTHROPIC_API_KEY`, `ANTHROPIC_BASE_URL` |
|
|
30
|
+
| **OpenAI** | `OPENAI_API_KEY`, `OPENAI_BASE_URL` |
|
|
31
|
+
| **Google Gemini** | `GEMINI_API_KEY`, `GOOGLE_GEMINI_BASE_URL` |
|
|
32
|
+
| **Gateway (always set)** | `NETLIFY_AI_GATEWAY_KEY`, `NETLIFY_AI_GATEWAY_BASE_URL` |
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
Install the SDK for your provider and use a zero-config constructor. No API key or base URL needed.
|
|
37
|
+
|
|
38
|
+
### Anthropic
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install @anthropic-ai/sdk
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import Anthropic from '@anthropic-ai/sdk'
|
|
46
|
+
|
|
47
|
+
const anthropic = new Anthropic()
|
|
48
|
+
|
|
49
|
+
const message = await anthropic.messages.create({
|
|
50
|
+
model: 'claude-opus-4-8',
|
|
51
|
+
max_tokens: 1024,
|
|
52
|
+
messages: [{ role: 'user', content: 'Hello!' }],
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
console.log(message.content[0].text)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### OpenAI
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npm install openai
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
import OpenAI from 'openai'
|
|
66
|
+
|
|
67
|
+
const openai = new OpenAI()
|
|
68
|
+
|
|
69
|
+
const completion = await openai.chat.completions.create({
|
|
70
|
+
model: 'gpt-5.2',
|
|
71
|
+
messages: [{ role: 'user', content: 'Hello!' }],
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
console.log(completion.choices[0].message.content)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
OpenAI also supports the newer Responses API:
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import OpenAI from 'openai'
|
|
81
|
+
|
|
82
|
+
const openai = new OpenAI()
|
|
83
|
+
|
|
84
|
+
const response = await openai.responses.create({
|
|
85
|
+
model: 'gpt-5.2',
|
|
86
|
+
input: [{ role: 'user', content: 'Hello!' }],
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
console.log(response.output_text)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Gemini
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npm install @google/genai
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
import { GoogleGenAI } from '@google/genai'
|
|
100
|
+
|
|
101
|
+
const ai = new GoogleGenAI({})
|
|
102
|
+
|
|
103
|
+
const response = await ai.models.generateContent({
|
|
104
|
+
model: 'gemini-3-flash-preview',
|
|
105
|
+
contents: 'Hello!',
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
console.log(response.text)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Streaming
|
|
112
|
+
|
|
113
|
+
### Anthropic Streaming
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import Anthropic from '@anthropic-ai/sdk'
|
|
117
|
+
|
|
118
|
+
const anthropic = new Anthropic()
|
|
119
|
+
|
|
120
|
+
const stream = await anthropic.messages.create({
|
|
121
|
+
model: 'claude-opus-4-8',
|
|
122
|
+
max_tokens: 1024,
|
|
123
|
+
messages: [{ role: 'user', content: 'Tell me a story.' }],
|
|
124
|
+
stream: true,
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
for await (const event of stream) {
|
|
128
|
+
if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') {
|
|
129
|
+
process.stdout.write(event.delta.text)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### OpenAI Streaming
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
import OpenAI from 'openai'
|
|
138
|
+
|
|
139
|
+
const openai = new OpenAI()
|
|
140
|
+
|
|
141
|
+
const stream = await openai.chat.completions.create({
|
|
142
|
+
model: 'gpt-5.2',
|
|
143
|
+
messages: [{ role: 'user', content: 'Tell me a story.' }],
|
|
144
|
+
stream: true,
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
for await (const chunk of stream) {
|
|
148
|
+
if (chunk.choices[0]?.delta?.content) {
|
|
149
|
+
process.stdout.write(chunk.choices[0].delta.content)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Gemini Streaming
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
import { GoogleGenAI } from '@google/genai'
|
|
158
|
+
|
|
159
|
+
const ai = new GoogleGenAI({})
|
|
160
|
+
|
|
161
|
+
const stream = await ai.models.generateContentStream({
|
|
162
|
+
model: 'gemini-3-flash-preview',
|
|
163
|
+
contents: 'Tell me a story.',
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
for await (const chunk of stream) {
|
|
167
|
+
if (chunk.text) {
|
|
168
|
+
process.stdout.write(chunk.text)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Image Generation
|
|
174
|
+
|
|
175
|
+
### OpenAI (gpt-image-1)
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
import OpenAI from 'openai'
|
|
179
|
+
|
|
180
|
+
const openai = new OpenAI()
|
|
181
|
+
|
|
182
|
+
const result = await openai.images.generate({
|
|
183
|
+
model: 'gpt-image-1',
|
|
184
|
+
prompt: 'A cute otter in a river',
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
const imageBase64 = result.data[0].b64_json
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Image generation is also available via the Responses API with the `image_generation` tool:
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
const response = await openai.responses.create({
|
|
194
|
+
model: 'gpt-4o',
|
|
195
|
+
input: 'Create a simple logo',
|
|
196
|
+
tools: [{ type: 'image_generation' }],
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
for (const item of response.output) {
|
|
200
|
+
if (item.type === 'image_generation_call') {
|
|
201
|
+
const imageBase64 = item.result.data
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Netlify Functions
|
|
207
|
+
|
|
208
|
+
Full example of a Netlify Function using AI Gateway:
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
// netlify/functions/chat.mts
|
|
212
|
+
import type { Context } from '@netlify/functions'
|
|
213
|
+
import Anthropic from '@anthropic-ai/sdk'
|
|
214
|
+
|
|
215
|
+
const anthropic = new Anthropic()
|
|
216
|
+
|
|
217
|
+
export default async (req: Request, context: Context) => {
|
|
218
|
+
const { prompt } = await req.json()
|
|
219
|
+
|
|
220
|
+
const message = await anthropic.messages.create({
|
|
221
|
+
model: 'claude-opus-4-8',
|
|
222
|
+
max_tokens: 1024,
|
|
223
|
+
messages: [{ role: 'user', content: prompt }],
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
return Response.json({ response: message.content[0].text })
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export const config = {
|
|
230
|
+
path: '/api/chat',
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
For streaming responses from a function:
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
// netlify/functions/stream.mts
|
|
238
|
+
import type { Context } from '@netlify/functions'
|
|
239
|
+
import OpenAI from 'openai'
|
|
240
|
+
|
|
241
|
+
const openai = new OpenAI()
|
|
242
|
+
|
|
243
|
+
export default async (req: Request, context: Context) => {
|
|
244
|
+
const { prompt } = await req.json()
|
|
245
|
+
|
|
246
|
+
const stream = await openai.chat.completions.create({
|
|
247
|
+
model: 'gpt-5.2',
|
|
248
|
+
messages: [{ role: 'user', content: prompt }],
|
|
249
|
+
stream: true,
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
return new Response(
|
|
253
|
+
new ReadableStream({
|
|
254
|
+
async start(controller) {
|
|
255
|
+
for await (const chunk of stream) {
|
|
256
|
+
const text = chunk.choices[0]?.delta?.content
|
|
257
|
+
if (text) {
|
|
258
|
+
controller.enqueue(new TextEncoder().encode(text))
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
controller.close()
|
|
262
|
+
},
|
|
263
|
+
}),
|
|
264
|
+
{ headers: { 'Content-Type': 'text/plain; charset=utf-8' } },
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export const config = {
|
|
269
|
+
path: '/api/stream',
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## Framework Server-Side Code
|
|
274
|
+
|
|
275
|
+
AI Gateway env vars are available in any server-side context, not just Netlify Functions.
|
|
276
|
+
|
|
277
|
+
### Next.js Route Handler
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
// app/api/chat/route.ts
|
|
281
|
+
import OpenAI from 'openai'
|
|
282
|
+
|
|
283
|
+
const openai = new OpenAI()
|
|
284
|
+
|
|
285
|
+
export async function POST(request: Request) {
|
|
286
|
+
const { prompt } = await request.json()
|
|
287
|
+
|
|
288
|
+
const completion = await openai.chat.completions.create({
|
|
289
|
+
model: 'gpt-5.2',
|
|
290
|
+
messages: [{ role: 'user', content: prompt }],
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
return Response.json({ response: completion.choices[0].message.content })
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
This also works in server components, API routes in Astro, Remix loaders/actions, and SvelteKit server routes.
|
|
298
|
+
|
|
299
|
+
## REST API / Direct Fetch
|
|
300
|
+
|
|
301
|
+
For HTTP-level control, use the gateway variables directly:
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
const response = await fetch(`${process.env.NETLIFY_AI_GATEWAY_BASE_URL}/anthropic/v1/messages`, {
|
|
305
|
+
method: 'POST',
|
|
306
|
+
headers: {
|
|
307
|
+
'Content-Type': 'application/json',
|
|
308
|
+
'Authorization': `Bearer ${process.env.NETLIFY_AI_GATEWAY_KEY}`,
|
|
309
|
+
'anthropic-version': '2023-06-01',
|
|
310
|
+
},
|
|
311
|
+
body: JSON.stringify({
|
|
312
|
+
model: 'claude-opus-4-8',
|
|
313
|
+
max_tokens: 1024,
|
|
314
|
+
messages: [{ role: 'user', content: 'Hello!' }],
|
|
315
|
+
}),
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
const data = await response.json()
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Or using the provider-specific variables with `fetch`:
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
const response = await fetch(`${process.env.ANTHROPIC_BASE_URL}/v1/messages`, {
|
|
325
|
+
method: 'POST',
|
|
326
|
+
headers: {
|
|
327
|
+
'Content-Type': 'application/json',
|
|
328
|
+
'x-api-key': process.env.ANTHROPIC_API_KEY,
|
|
329
|
+
'anthropic-version': '2023-06-01',
|
|
330
|
+
},
|
|
331
|
+
body: JSON.stringify({
|
|
332
|
+
model: 'claude-opus-4-8',
|
|
333
|
+
max_tokens: 1024,
|
|
334
|
+
messages: [{ role: 'user', content: 'Hello!' }],
|
|
335
|
+
}),
|
|
336
|
+
})
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
## Available Models
|
|
340
|
+
|
|
341
|
+
For the latest list, see https://docs.netlify.com/build/ai-gateway/overview/.
|
|
342
|
+
|
|
343
|
+
<!-- AVAILABLE_MODELS -->
|
|
344
|
+
|
|
345
|
+
## Rate Limits
|
|
346
|
+
|
|
347
|
+
Tokens per minute (TPM) are scoped per **account** across all projects. For current limits per model and plan tier, see https://docs.netlify.com/build/ai-gateway/overview/.
|
|
348
|
+
|
|
349
|
+
## Limitations
|
|
350
|
+
|
|
351
|
+
- **Context window:** Max 200k tokens per request
|
|
352
|
+
- **Batch inference:** Not supported
|
|
353
|
+
- **Custom headers:** Cannot send custom request headers to providers
|
|
354
|
+
- **Prompt caching:** Anthropic supports 5-minute ephemeral caching only. Gemini caching is not supported. OpenAI sets `prompt_cache_key` per-account automatically.
|
|
355
|
+
- **Priority processing (OpenAI):** Not supported
|
|
356
|
+
- **Production deploy required:** At least one production deploy must exist before AI Gateway activates
|
|
357
|
+
|
|
358
|
+
## Disabling AI Gateway
|
|
359
|
+
|
|
360
|
+
To prevent Netlify from injecting any AI-related environment variables, disable AI features in the Netlify UI under
|
|
361
|
+
**Project configuration > Build & deploy > Build with AI > Manage AI features**.
|
|
362
|
+
|
|
363
|
+
## Monitoring Usage
|
|
364
|
+
|
|
365
|
+
View AI Gateway token usage and costs in the Netlify UI under **Team settings > Billing > Usage**. Each request's
|
|
366
|
+
token consumption is tracked per model and converted to Netlify credits.
|
|
367
|
+
|
|
368
|
+
## Common Errors & Solutions
|
|
369
|
+
|
|
370
|
+
### "API key not found" or env var is undefined
|
|
371
|
+
|
|
372
|
+
**Cause:** AI Gateway env vars are not injected.
|
|
373
|
+
|
|
374
|
+
**Fix:**
|
|
375
|
+
|
|
376
|
+
1. Ensure the site has at least one production deploy
|
|
377
|
+
2. Deploy to Netlify
|
|
378
|
+
3. Check that you are reading the env var in server-side code, not client-side browser code
|
|
379
|
+
|
|
380
|
+
### Rate limit exceeded (429)
|
|
381
|
+
|
|
382
|
+
**Cause:** Account hit the tokens-per-minute limit for that model.
|
|
383
|
+
|
|
384
|
+
**Fix:**
|
|
385
|
+
|
|
386
|
+
1. Wait briefly and retry — TPM limits reset each minute
|
|
387
|
+
2. Switch to a smaller/cheaper model (e.g. `gpt-4.1-mini` instead of `gpt-4.1`)
|
|
388
|
+
3. Reduce prompt length or `max_tokens`
|
|
389
|
+
4. Upgrade plan tier for higher limits
|
|
390
|
+
|
|
391
|
+
### Model not available
|
|
392
|
+
|
|
393
|
+
**Cause:** The requested model is not supported through AI Gateway.
|
|
394
|
+
|
|
395
|
+
**Fix:**
|
|
396
|
+
|
|
397
|
+
1. Check the available models list above
|
|
398
|
+
2. Use the exact model ID string (e.g. `claude-sonnet-4-5-20250929`, not `claude-sonnet`)
|
|
399
|
+
|
|
400
|
+
## Packages
|
|
401
|
+
|
|
402
|
+
```bash
|
|
403
|
+
# Anthropic
|
|
404
|
+
npm install @anthropic-ai/sdk
|
|
405
|
+
|
|
406
|
+
# OpenAI
|
|
407
|
+
npm install openai
|
|
408
|
+
|
|
409
|
+
# Google Gemini
|
|
410
|
+
npm install @google/genai
|
|
411
|
+
```
|