@kood/claude-code 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +129 -5
- package/package.json +2 -2
- package/templates/hono/CLAUDE.md +20 -2
- package/templates/hono/docs/architecture/architecture.md +909 -0
- package/templates/hono/docs/commands/git.md +275 -0
- package/templates/hono/docs/deployment/cloudflare.md +527 -190
- package/templates/hono/docs/deployment/docker.md +514 -0
- package/templates/hono/docs/deployment/index.md +179 -214
- package/templates/hono/docs/deployment/railway.md +416 -0
- package/templates/hono/docs/deployment/vercel.md +567 -0
- package/templates/hono/docs/library/ai-sdk/index.md +427 -0
- package/templates/hono/docs/library/ai-sdk/openrouter.md +479 -0
- package/templates/hono/docs/library/ai-sdk/providers.md +468 -0
- package/templates/hono/docs/library/ai-sdk/streaming.md +447 -0
- package/templates/hono/docs/library/ai-sdk/structured-output.md +493 -0
- package/templates/hono/docs/library/ai-sdk/tools.md +513 -0
- package/templates/hono/docs/library/hono/env-setup.md +458 -0
- package/templates/hono/docs/library/hono/index.md +1 -3
- package/templates/hono/docs/library/pino/index.md +437 -0
- package/templates/hono/docs/library/prisma/cloudflare-d1.md +503 -0
- package/templates/hono/docs/library/prisma/config.md +362 -0
- package/templates/hono/docs/library/prisma/index.md +86 -13
- package/templates/hono/docs/skills/gemini-review/SKILL.md +116 -116
- package/templates/hono/docs/skills/gemini-review/references/checklists.md +125 -125
- package/templates/hono/docs/skills/gemini-review/references/prompt-templates.md +191 -191
- package/templates/npx/CLAUDE.md +309 -0
- package/templates/npx/docs/commands/git.md +275 -0
- package/templates/npx/docs/library/commander/index.md +164 -0
- package/templates/npx/docs/library/fs-extra/index.md +171 -0
- package/templates/npx/docs/library/prompts/index.md +253 -0
- package/templates/npx/docs/mcp/index.md +60 -0
- package/templates/npx/docs/skills/gemini-review/SKILL.md +220 -0
- package/templates/npx/docs/skills/gemini-review/references/checklists.md +134 -0
- package/templates/npx/docs/skills/gemini-review/references/prompt-templates.md +301 -0
- package/templates/tanstack-start/CLAUDE.md +43 -5
- package/templates/tanstack-start/docs/architecture/architecture.md +134 -4
- package/templates/tanstack-start/docs/commands/git.md +275 -0
- package/templates/tanstack-start/docs/deployment/cloudflare.md +223 -50
- package/templates/tanstack-start/docs/deployment/index.md +320 -30
- package/templates/tanstack-start/docs/deployment/nitro.md +195 -14
- package/templates/tanstack-start/docs/deployment/railway.md +302 -150
- package/templates/tanstack-start/docs/deployment/vercel.md +345 -75
- package/templates/tanstack-start/docs/guides/best-practices.md +203 -1
- package/templates/tanstack-start/docs/guides/env-setup.md +450 -0
- package/templates/tanstack-start/docs/library/ai-sdk/hooks.md +472 -0
- package/templates/tanstack-start/docs/library/ai-sdk/index.md +264 -0
- package/templates/tanstack-start/docs/library/ai-sdk/openrouter.md +371 -0
- package/templates/tanstack-start/docs/library/ai-sdk/providers.md +403 -0
- package/templates/tanstack-start/docs/library/ai-sdk/streaming.md +320 -0
- package/templates/tanstack-start/docs/library/ai-sdk/structured-output.md +454 -0
- package/templates/tanstack-start/docs/library/ai-sdk/tools.md +473 -0
- package/templates/tanstack-start/docs/library/pino/index.md +320 -0
- package/templates/tanstack-start/docs/library/prisma/cloudflare-d1.md +404 -0
- package/templates/tanstack-start/docs/library/prisma/config.md +377 -0
- package/templates/tanstack-start/docs/library/prisma/index.md +3 -5
- package/templates/tanstack-start/docs/library/prisma/schema.md +123 -25
- package/templates/tanstack-start/docs/library/prisma/setup.md +0 -7
- package/templates/tanstack-start/docs/library/tanstack-start/server-functions.md +80 -2
- package/templates/tanstack-start/docs/skills/gemini-review/SKILL.md +116 -116
- package/templates/tanstack-start/docs/skills/gemini-review/references/checklists.md +138 -144
- package/templates/tanstack-start/docs/skills/gemini-review/references/prompt-templates.md +186 -187
- package/templates/hono/docs/git/index.md +0 -180
- package/templates/tanstack-start/docs/git/index.md +0 -203
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
# AI SDK - OpenRouter (Hono)
|
|
2
|
+
|
|
3
|
+
> **상위 문서**: [AI SDK](./index.md) | [프로바이더](./providers.md)
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 개요
|
|
8
|
+
|
|
9
|
+
[OpenRouter](https://openrouter.ai/)는 Anthropic, Google, Meta, Mistral 등 주요 AI 프로바이더의 수백 개 모델에 단일 API로 접근할 수 있는 통합 게이트웨이입니다.
|
|
10
|
+
|
|
11
|
+
### 주요 장점
|
|
12
|
+
|
|
13
|
+
| 장점 | 설명 |
|
|
14
|
+
|------|------|
|
|
15
|
+
| **통합 API** | 하나의 API 키로 수백 개 모델 접근 |
|
|
16
|
+
| **비용 효율** | 월정액 없이 사용량 기반 과금 |
|
|
17
|
+
| **투명한 가격** | 모델별 토큰당 비용 명확히 표시 |
|
|
18
|
+
| **고가용성** | 엔터프라이즈급 인프라와 자동 장애 조치 |
|
|
19
|
+
| **최신 모델** | 새 모델 출시 즉시 사용 가능 |
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 설치
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install @openrouter/ai-sdk-provider
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 기본 설정
|
|
32
|
+
|
|
33
|
+
### Provider 인스턴스 생성
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import { createOpenRouter } from '@openrouter/ai-sdk-provider'
|
|
37
|
+
|
|
38
|
+
const openrouter = createOpenRouter({
|
|
39
|
+
apiKey: process.env.OPENROUTER_API_KEY,
|
|
40
|
+
})
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
API 키는 [OpenRouter Dashboard](https://openrouter.ai/keys)에서 발급받을 수 있습니다.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Hono 통합
|
|
48
|
+
|
|
49
|
+
### 기본 채팅 API
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import { Hono } from 'hono'
|
|
53
|
+
import { streamText, convertToModelMessages } from 'ai'
|
|
54
|
+
import { createOpenRouter } from '@openrouter/ai-sdk-provider'
|
|
55
|
+
|
|
56
|
+
const app = new Hono()
|
|
57
|
+
|
|
58
|
+
const openrouter = createOpenRouter({
|
|
59
|
+
apiKey: process.env.OPENROUTER_API_KEY,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
app.post('/api/chat', async (c) => {
|
|
63
|
+
const { messages } = await c.req.json()
|
|
64
|
+
|
|
65
|
+
const result = streamText({
|
|
66
|
+
model: openrouter.chat('anthropic/claude-3.5-sonnet'),
|
|
67
|
+
messages: convertToModelMessages(messages),
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
return result.toUIMessageStreamResponse()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
export default app
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 동적 모델 선택
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
import { Hono } from 'hono'
|
|
80
|
+
import { streamText, convertToModelMessages } from 'ai'
|
|
81
|
+
import { createOpenRouter } from '@openrouter/ai-sdk-provider'
|
|
82
|
+
import { HTTPException } from 'hono/http-exception'
|
|
83
|
+
|
|
84
|
+
const app = new Hono()
|
|
85
|
+
|
|
86
|
+
const openrouter = createOpenRouter({
|
|
87
|
+
apiKey: process.env.OPENROUTER_API_KEY,
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// 허용된 모델 목록
|
|
91
|
+
const ALLOWED_MODELS = [
|
|
92
|
+
'anthropic/claude-3.5-sonnet',
|
|
93
|
+
'anthropic/claude-3-opus',
|
|
94
|
+
'openai/gpt-4o',
|
|
95
|
+
'openai/gpt-4-turbo',
|
|
96
|
+
'google/gemini-pro-1.5',
|
|
97
|
+
'meta-llama/llama-3.1-70b-instruct',
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
app.post('/api/chat', async (c) => {
|
|
101
|
+
const { messages, model = 'anthropic/claude-3.5-sonnet' } = await c.req.json()
|
|
102
|
+
|
|
103
|
+
// 모델 검증
|
|
104
|
+
if (!ALLOWED_MODELS.includes(model)) {
|
|
105
|
+
throw new HTTPException(400, { message: 'Invalid model' })
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const result = streamText({
|
|
109
|
+
model: openrouter.chat(model),
|
|
110
|
+
messages: convertToModelMessages(messages),
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
return result.toUIMessageStreamResponse()
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
export default app
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Cloudflare Workers 배포
|
|
122
|
+
|
|
123
|
+
### 기본 설정
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
// src/index.ts
|
|
127
|
+
import { Hono } from 'hono'
|
|
128
|
+
import { streamText, convertToModelMessages } from 'ai'
|
|
129
|
+
import { createOpenRouter } from '@openrouter/ai-sdk-provider'
|
|
130
|
+
|
|
131
|
+
type Bindings = {
|
|
132
|
+
OPENROUTER_API_KEY: string
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const app = new Hono<{ Bindings: Bindings }>()
|
|
136
|
+
|
|
137
|
+
app.post('/api/chat', async (c) => {
|
|
138
|
+
const openrouter = createOpenRouter({
|
|
139
|
+
apiKey: c.env.OPENROUTER_API_KEY,
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
const { messages, model } = await c.req.json()
|
|
143
|
+
|
|
144
|
+
const result = streamText({
|
|
145
|
+
model: openrouter.chat(model ?? 'anthropic/claude-3.5-sonnet'),
|
|
146
|
+
messages: convertToModelMessages(messages),
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
return result.toUIMessageStreamResponse()
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
export default app
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### wrangler.toml
|
|
156
|
+
|
|
157
|
+
```toml
|
|
158
|
+
name = "ai-api"
|
|
159
|
+
main = "src/index.ts"
|
|
160
|
+
compatibility_date = "2024-01-01"
|
|
161
|
+
|
|
162
|
+
# API 키는 Cloudflare 대시보드에서 설정
|
|
163
|
+
# Settings > Variables > Environment Variables
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## 인기 모델
|
|
169
|
+
|
|
170
|
+
| 모델 ID | 설명 |
|
|
171
|
+
|---------|------|
|
|
172
|
+
| `anthropic/claude-3.5-sonnet` | Claude 3.5 Sonnet |
|
|
173
|
+
| `anthropic/claude-3-opus` | Claude 3 Opus |
|
|
174
|
+
| `openai/gpt-4o` | GPT-4o |
|
|
175
|
+
| `openai/gpt-4-turbo` | GPT-4 Turbo |
|
|
176
|
+
| `google/gemini-pro-1.5` | Gemini Pro 1.5 |
|
|
177
|
+
| `meta-llama/llama-3.1-405b-instruct` | Llama 3.1 405B |
|
|
178
|
+
| `meta-llama/llama-3.1-70b-instruct` | Llama 3.1 70B |
|
|
179
|
+
| `mistralai/mistral-large` | Mistral Large |
|
|
180
|
+
|
|
181
|
+
전체 모델 목록: [OpenRouter Models](https://openrouter.ai/docs#models)
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## 도구 사용
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
import { Hono } from 'hono'
|
|
189
|
+
import { streamText, tool, convertToModelMessages } from 'ai'
|
|
190
|
+
import { createOpenRouter } from '@openrouter/ai-sdk-provider'
|
|
191
|
+
import { z } from 'zod'
|
|
192
|
+
|
|
193
|
+
type Bindings = {
|
|
194
|
+
OPENROUTER_API_KEY: string
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const app = new Hono<{ Bindings: Bindings }>()
|
|
198
|
+
|
|
199
|
+
app.post('/api/assistant', async (c) => {
|
|
200
|
+
const openrouter = createOpenRouter({
|
|
201
|
+
apiKey: c.env.OPENROUTER_API_KEY,
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
const { messages } = await c.req.json()
|
|
205
|
+
|
|
206
|
+
const result = streamText({
|
|
207
|
+
model: openrouter.chat('anthropic/claude-3.5-sonnet'),
|
|
208
|
+
messages: convertToModelMessages(messages),
|
|
209
|
+
tools: {
|
|
210
|
+
getWeather: tool({
|
|
211
|
+
description: 'Get weather for a location',
|
|
212
|
+
inputSchema: z.object({
|
|
213
|
+
location: z.string().describe('City name'),
|
|
214
|
+
}),
|
|
215
|
+
execute: async ({ location }) => {
|
|
216
|
+
// 실제 날씨 API 호출
|
|
217
|
+
return { location, temperature: 22, condition: 'Sunny' }
|
|
218
|
+
},
|
|
219
|
+
}),
|
|
220
|
+
searchProducts: tool({
|
|
221
|
+
description: 'Search products in database',
|
|
222
|
+
inputSchema: z.object({
|
|
223
|
+
query: z.string(),
|
|
224
|
+
limit: z.number().optional().default(10),
|
|
225
|
+
}),
|
|
226
|
+
execute: async ({ query, limit }) => {
|
|
227
|
+
// 데이터베이스 검색
|
|
228
|
+
return { products: [], total: 0 }
|
|
229
|
+
},
|
|
230
|
+
}),
|
|
231
|
+
},
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
return result.toUIMessageStreamResponse()
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
export default app
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## 구조화된 출력
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
import { Hono } from 'hono'
|
|
246
|
+
import { generateObject } from 'ai'
|
|
247
|
+
import { createOpenRouter } from '@openrouter/ai-sdk-provider'
|
|
248
|
+
import { z } from 'zod'
|
|
249
|
+
|
|
250
|
+
type Bindings = {
|
|
251
|
+
OPENROUTER_API_KEY: string
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const app = new Hono<{ Bindings: Bindings }>()
|
|
255
|
+
|
|
256
|
+
const userSchema = z.object({
|
|
257
|
+
name: z.string(),
|
|
258
|
+
age: z.number(),
|
|
259
|
+
skills: z.array(z.string()),
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
app.post('/api/generate-user', async (c) => {
|
|
263
|
+
const openrouter = createOpenRouter({
|
|
264
|
+
apiKey: c.env.OPENROUTER_API_KEY,
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
const { prompt } = await c.req.json()
|
|
268
|
+
|
|
269
|
+
const { object } = await generateObject({
|
|
270
|
+
model: openrouter.chat('anthropic/claude-3.5-sonnet'),
|
|
271
|
+
schema: userSchema,
|
|
272
|
+
prompt,
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
return c.json(object)
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
export default app
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## 미들웨어 패턴
|
|
284
|
+
|
|
285
|
+
### OpenRouter 미들웨어
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
import { Hono } from 'hono'
|
|
289
|
+
import { createMiddleware } from 'hono/factory'
|
|
290
|
+
import { createOpenRouter } from '@openrouter/ai-sdk-provider'
|
|
291
|
+
|
|
292
|
+
type Bindings = {
|
|
293
|
+
OPENROUTER_API_KEY: string
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
type Variables = {
|
|
297
|
+
openrouter: ReturnType<typeof createOpenRouter>
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const openrouterMiddleware = createMiddleware<{
|
|
301
|
+
Bindings: Bindings
|
|
302
|
+
Variables: Variables
|
|
303
|
+
}>(async (c, next) => {
|
|
304
|
+
const openrouter = createOpenRouter({
|
|
305
|
+
apiKey: c.env.OPENROUTER_API_KEY,
|
|
306
|
+
})
|
|
307
|
+
c.set('openrouter', openrouter)
|
|
308
|
+
await next()
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
const app = new Hono<{ Bindings: Bindings; Variables: Variables }>()
|
|
312
|
+
|
|
313
|
+
app.use('/api/*', openrouterMiddleware)
|
|
314
|
+
|
|
315
|
+
app.post('/api/chat', async (c) => {
|
|
316
|
+
const openrouter = c.get('openrouter')
|
|
317
|
+
const { messages } = await c.req.json()
|
|
318
|
+
|
|
319
|
+
const result = streamText({
|
|
320
|
+
model: openrouter.chat('anthropic/claude-3.5-sonnet'),
|
|
321
|
+
messages,
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
return result.toUIMessageStreamResponse()
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
export default app
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Rate Limiting
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
import { Hono } from 'hono'
|
|
334
|
+
import { rateLimiter } from 'hono-rate-limiter'
|
|
335
|
+
import { streamText, convertToModelMessages } from 'ai'
|
|
336
|
+
import { createOpenRouter } from '@openrouter/ai-sdk-provider'
|
|
337
|
+
|
|
338
|
+
type Bindings = {
|
|
339
|
+
OPENROUTER_API_KEY: string
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const app = new Hono<{ Bindings: Bindings }>()
|
|
343
|
+
|
|
344
|
+
// Rate limiting 적용
|
|
345
|
+
app.use(
|
|
346
|
+
'/api/*',
|
|
347
|
+
rateLimiter({
|
|
348
|
+
windowMs: 60 * 1000, // 1분
|
|
349
|
+
limit: 20, // 최대 20 요청
|
|
350
|
+
standardHeaders: 'draft-6',
|
|
351
|
+
keyGenerator: (c) => c.req.header('x-forwarded-for') ?? 'anonymous',
|
|
352
|
+
})
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
app.post('/api/chat', async (c) => {
|
|
356
|
+
const openrouter = createOpenRouter({
|
|
357
|
+
apiKey: c.env.OPENROUTER_API_KEY,
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
const { messages } = await c.req.json()
|
|
361
|
+
|
|
362
|
+
const result = streamText({
|
|
363
|
+
model: openrouter.chat('anthropic/claude-3.5-sonnet'),
|
|
364
|
+
messages: convertToModelMessages(messages),
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
return result.toUIMessageStreamResponse()
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
export default app
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## 에러 처리
|
|
376
|
+
|
|
377
|
+
```typescript
|
|
378
|
+
import { Hono } from 'hono'
|
|
379
|
+
import { HTTPException } from 'hono/http-exception'
|
|
380
|
+
import { streamText, convertToModelMessages } from 'ai'
|
|
381
|
+
import { createOpenRouter } from '@openrouter/ai-sdk-provider'
|
|
382
|
+
|
|
383
|
+
type Bindings = {
|
|
384
|
+
OPENROUTER_API_KEY: string
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const app = new Hono<{ Bindings: Bindings }>()
|
|
388
|
+
|
|
389
|
+
app.post('/api/chat', async (c) => {
|
|
390
|
+
try {
|
|
391
|
+
const openrouter = createOpenRouter({
|
|
392
|
+
apiKey: c.env.OPENROUTER_API_KEY,
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
const body = await c.req.json()
|
|
396
|
+
|
|
397
|
+
if (!body.messages || !Array.isArray(body.messages)) {
|
|
398
|
+
throw new HTTPException(400, { message: 'Invalid messages format' })
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const result = streamText({
|
|
402
|
+
model: openrouter.chat(body.model ?? 'anthropic/claude-3.5-sonnet'),
|
|
403
|
+
messages: convertToModelMessages(body.messages),
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
return result.toUIMessageStreamResponse()
|
|
407
|
+
} catch (error) {
|
|
408
|
+
if (error instanceof HTTPException) {
|
|
409
|
+
throw error
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
console.error('OpenRouter Error:', error)
|
|
413
|
+
throw new HTTPException(500, { message: 'AI processing failed' })
|
|
414
|
+
}
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
// 글로벌 에러 핸들러
|
|
418
|
+
app.onError((err, c) => {
|
|
419
|
+
if (err instanceof HTTPException) {
|
|
420
|
+
return c.json({ error: err.message }, err.status)
|
|
421
|
+
}
|
|
422
|
+
return c.json({ error: 'Internal server error' }, 500)
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
export default app
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
## 비용 관리
|
|
431
|
+
|
|
432
|
+
### 사용량 추적
|
|
433
|
+
|
|
434
|
+
```typescript
|
|
435
|
+
app.post('/api/generate', async (c) => {
|
|
436
|
+
const openrouter = createOpenRouter({
|
|
437
|
+
apiKey: c.env.OPENROUTER_API_KEY,
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
const { prompt } = await c.req.json()
|
|
441
|
+
|
|
442
|
+
const result = await generateText({
|
|
443
|
+
model: openrouter.chat('anthropic/claude-3.5-sonnet'),
|
|
444
|
+
prompt,
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
return c.json({
|
|
448
|
+
text: result.text,
|
|
449
|
+
usage: result.usage,
|
|
450
|
+
// { promptTokens: 10, completionTokens: 50, totalTokens: 60 }
|
|
451
|
+
})
|
|
452
|
+
})
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
## 환경 변수
|
|
458
|
+
|
|
459
|
+
```bash
|
|
460
|
+
# .env (로컬 개발)
|
|
461
|
+
OPENROUTER_API_KEY=sk-or-v1-...
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
```toml
|
|
465
|
+
# wrangler.toml (Cloudflare Workers)
|
|
466
|
+
# API 키는 Cloudflare 대시보드에서 설정:
|
|
467
|
+
# Settings > Variables > Environment Variables
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
---
|
|
471
|
+
|
|
472
|
+
## 리소스
|
|
473
|
+
|
|
474
|
+
- [OpenRouter 공식 문서](https://openrouter.ai/docs)
|
|
475
|
+
- [OpenRouter 대시보드](https://openrouter.ai/dashboard)
|
|
476
|
+
- [API 키 발급](https://openrouter.ai/keys)
|
|
477
|
+
- [모델 목록](https://openrouter.ai/docs#models)
|
|
478
|
+
- [GitHub 저장소](https://github.com/OpenRouterTeam/ai-sdk-provider)
|
|
479
|
+
- [상태 페이지](https://status.openrouter.ai)
|