gencow 0.1.109 → 0.1.110

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.
@@ -296,16 +296,20 @@ export function buildAiPrompt(apiObj, namespaces) {
296
296
  md += `- 반드시 useQuery와 useMutation을 사용해서 데이터와 연결해줘.\n`;
297
297
  md += `- fetch()를 직접 호출하거나 apiPost() 같은 래퍼를 만들지 마.\n`;
298
298
  md += `- gencow/api.ts는 자동 생성된 파일이야. 수동으로 만들지 마.\n`;
299
+ md += `- ⚠️ gencow/ai.ts는 자동 생성 파일이야. 절대 수정하지 마. 에러 발생 시 환경변수(.env)를 확인하거나 \`npx gencow add AI\`로 재설치해.\n`;
299
300
  md += `- gencow/index.ts의 re-export는 export * as moduleName from "./moduleName" 패턴을 써.\n`;
300
301
  md += ` Module, Mod 같은 접미사를 붙이지 마.\n`;
301
302
  md += `\n`;
302
303
  md += `⚠️ mutation 제한:\n`;
303
304
  md += `- mutation은 10초 이내에 완료되어야 해. 외부 API나 LLM 호출이 길면 단계별로 분리해.\n`;
304
- md += `- 작업은 ctx.scheduler.runAfter(0, "module.nextStep", { sessionId }) 다음 단계를 예약.\n`;
305
- md += `- 예: 크롤링(Step1) → 필터링(Step2) → 요약(Step3) 각각 별도 mutation으로 분리.\n`;
305
+ md += `- 1순위: 단계를 별도 mutation으로 분리하고, 프론트엔드에서 순차 호출해.\n`;
306
+ md += ` 예: 크롤링(Step1) → 필터링(Step2) → 요약(Step3) 각각 별도 mutation으로 분리.\n`;
307
+ md += ` 프론트엔드: await crawl(); await filter(); await summarize();\n`;
308
+ md += `- ⚠️ 보조: ctx.scheduler.runAfter(0, "module.nextStep", { sessionId }) — 로컬 dev에서만 안정적. 클라우드에서는 앱 sleep 시 타이머가 소멸할 수 있어.\n`;
306
309
  md += `- 같은 서버의 다른 모듈 함수를 호출할 때는 fetch()가 아닌 직접 import해서 호출해.\n`;
307
310
  md += ` 예: import { fetchNews } from "./naverApi"; → const result = await fetchNews.handler(ctx, { keyword });\n`;
308
311
  md += ` HTTP self-fetch (fetch("/api/mutation")) 패턴은 불필요한 네트워크 우회이므로 사용하지 마.\n`;
312
+
309
313
  md += `\n`;
310
314
  md += `크론 잡 (예약 작업):\n`;
311
315
  md += ` gencow/crons.ts에서 cronJobs()로 선언\n`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gencow",
3
- "version": "0.1.109",
3
+ "version": "0.1.110",
4
4
  "description": "Gencow — AI Backend Engine",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,3 +1,7 @@
1
+ // ⚠️ 이 파일은 Gencow AI 엔진이 자동 생성합니다. 직접 수정하지 마세요.
2
+ // ⚠️ 에러 발생 시 코드를 수정하지 말고 환경변수(.env)를 확인하거나 `npx gencow add AI`로 재설치하세요.
3
+ // ⚠️ PROXY_URL/API_KEY 관련 코드를 삭제하면 클라우드 배포 시 AI가 작동하지 않습니다.
4
+
1
5
  import { generateText, streamText, embed, embedMany, generateObject } from "ai";
2
6
  import { createOpenAI } from "@ai-sdk/openai";
3
7
 
@@ -40,6 +44,42 @@ function resolveModel(model?: Parameters<typeof generateText>[0]["model"]) {
40
44
  return model ?? defaultModel;
41
45
  }
42
46
 
47
+ /**
48
+ * AI 프록시 에러 진단 — 모든 AI 메서드에서 공통 사용.
49
+ * 에러 발생 시 구체적인 진단 메시지를 출력하고 원본 에러를 다시 throw.
50
+ * ⚠️ 이 함수는 ai.ts 수정 금지를 바이브코더에게 전달하는 핵심 방어 메커니즘.
51
+ */
52
+ function wrapProxyError(err: unknown): never {
53
+ // S2: HTTP status 코드 우선 확인 (AI SDK 에러 객체 구조)
54
+ const status = (err as { status?: number; statusCode?: number }).status
55
+ ?? (err as { status?: number; statusCode?: number }).statusCode;
56
+ const msg = err instanceof Error ? err.message : String(err);
57
+
58
+ const isNotFound = status === 404 || (!status && msg.includes("Not Found"));
59
+ const isAuthError = status === 401 || status === 403
60
+ || (!status && (msg.includes("Unauthenticated") || msg.includes("Unauthorized")));
61
+
62
+ if (isNotFound) {
63
+ const proxyUrl = process.env.GENCOW_AI_PROXY_URL;
64
+ console.error(
65
+ `\n🔴 AI Proxy 연결 실패 (404 Not Found)\n` +
66
+ ` GENCOW_AI_PROXY_URL: ${proxyUrl || "(미설정)"}\n` +
67
+ ` 가능한 원인:\n` +
68
+ ` 1. 플랫폼 서버가 최신 버전으로 배포되지 않았을 수 있습니다.\n` +
69
+ ` 2. 환경변수가 올바르게 주입되지 않았을 수 있습니다.\n` +
70
+ ` ⚠️ 이 파일(ai.ts)을 수정하지 마세요. npx gencow add AI로 재설치하세요.\n`
71
+ );
72
+ } else if (isAuthError) {
73
+ console.error(
74
+ `\n🔴 AI 인증 실패\n` +
75
+ ` API_KEY 설정 여부: ${API_KEY ? "✓ 설정됨" : "✗ 미설정"}\n` +
76
+ ` PROXY_URL: ${PROXY_URL || "(미설정 — OpenAI 직접 호출)"}\n` +
77
+ ` ⚠️ .env 파일의 OPENAI_API_KEY를 확인하세요.\n`
78
+ );
79
+ }
80
+ throw err;
81
+ }
82
+
43
83
  /**
44
84
  * Gencow AI Engine — Vercel AI SDK 래퍼
45
85
  *
@@ -73,32 +113,36 @@ export const ai = {
73
113
  responseFormat?: { type: string };
74
114
  }) {
75
115
  const resolvedModel = resolveModel(options.model);
76
- const result = await generateText({
77
- model: resolvedModel,
78
- system: options.system,
79
- messages: options.messages as Parameters<typeof generateText>[0]["messages"],
80
- tools: options.tools as Parameters<typeof generateText>[0]["tools"],
81
- ...(options.temperature !== undefined && { temperature: options.temperature }),
82
- ...(options.maxTokens !== undefined && { maxTokens: options.maxTokens }),
83
- // responseFormat providerOptions (OpenAI JSON mode)
84
- ...(options.responseFormat?.type === "json_object" && {
85
- providerOptions: {
86
- openai: { response_format: { type: "json_object" } },
116
+ try {
117
+ const result = await generateText({
118
+ model: resolvedModel,
119
+ system: options.system,
120
+ messages: options.messages as Parameters<typeof generateText>[0]["messages"],
121
+ tools: options.tools as Parameters<typeof generateText>[0]["tools"],
122
+ ...(options.temperature !== undefined && { temperature: options.temperature }),
123
+ ...(options.maxTokens !== undefined && { maxTokens: options.maxTokens }),
124
+ // responseFormat providerOptions (OpenAI JSON mode)
125
+ ...(options.responseFormat?.type === "json_object" && {
126
+ providerOptions: {
127
+ openai: { response_format: { type: "json_object" } },
128
+ },
129
+ }),
130
+ } as Parameters<typeof generateText>[0]);
131
+ // AIResult 호환 객체 반환
132
+ const usage = (result as unknown as { usage?: { promptTokens?: number; completionTokens?: number; totalTokens?: number } }).usage;
133
+ return {
134
+ text: result.text,
135
+ usage: {
136
+ promptTokens: usage?.promptTokens ?? 0,
137
+ completionTokens: usage?.completionTokens ?? 0,
138
+ totalTokens: usage?.totalTokens ?? 0,
87
139
  },
88
- }),
89
- } as Parameters<typeof generateText>[0]);
90
- // AIResult 호환 객체 반환
91
- const usage = (result as unknown as { usage?: { promptTokens?: number; completionTokens?: number; totalTokens?: number } }).usage;
92
- return {
93
- text: result.text,
94
- usage: {
95
- promptTokens: usage?.promptTokens ?? 0,
96
- completionTokens: usage?.completionTokens ?? 0,
97
- totalTokens: usage?.totalTokens ?? 0,
98
- },
99
- creditsCharged: 0, // 클라우드: 프록시 측에서 차감. 로컬: 차감 없음.
100
- model: (resolvedModel as unknown as { modelId?: string }).modelId ?? "gpt-4o-mini",
101
- };
140
+ creditsCharged: 0, // 클라우드: 프록시 측에서 차감. 로컬: 차감 없음.
141
+ model: (resolvedModel as unknown as { modelId?: string }).modelId ?? "gpt-4o-mini",
142
+ };
143
+ } catch (err: unknown) {
144
+ wrapProxyError(err);
145
+ }
102
146
  },
103
147
 
104
148
  /**
@@ -120,11 +164,15 @@ export const ai = {
120
164
  system?: string;
121
165
  messages: { role: string; content: string }[];
122
166
  }) {
123
- return streamText({
124
- model: resolveModel(options.model),
125
- system: options.system,
126
- messages: options.messages as Parameters<typeof streamText>[0]["messages"],
127
- });
167
+ try {
168
+ return streamText({
169
+ model: resolveModel(options.model),
170
+ system: options.system,
171
+ messages: options.messages as Parameters<typeof streamText>[0]["messages"],
172
+ });
173
+ } catch (err: unknown) {
174
+ wrapProxyError(err);
175
+ }
128
176
  },
129
177
 
130
178
  /**
@@ -134,11 +182,15 @@ export const ai = {
134
182
  * const vector = await ai.embed("검색할 텍스트");
135
183
  */
136
184
  async embed(text: string, model?: Parameters<typeof embed>[0]["model"]) {
137
- const { embedding } = await embed({
138
- model: model ?? openaiProvider.embedding("text-embedding-3-small"),
139
- value: text,
140
- });
141
- return embedding;
185
+ try {
186
+ const { embedding } = await embed({
187
+ model: model ?? openaiProvider.embedding("text-embedding-3-small"),
188
+ value: text,
189
+ });
190
+ return embedding;
191
+ } catch (err: unknown) {
192
+ wrapProxyError(err);
193
+ }
142
194
  },
143
195
 
144
196
  /**
@@ -151,11 +203,15 @@ export const ai = {
151
203
  * const embeddings = await ai.embedMany(["텍스트1", "텍스트2", "텍스트3"]);
152
204
  */
153
205
  async embedMany(texts: string[], model?: Parameters<typeof embedMany>[0]["model"]) {
154
- const { embeddings } = await embedMany({
155
- model: model ?? openaiProvider.embedding("text-embedding-3-small"),
156
- values: texts,
157
- });
158
- return embeddings;
206
+ try {
207
+ const { embeddings } = await embedMany({
208
+ model: model ?? openaiProvider.embedding("text-embedding-3-small"),
209
+ values: texts,
210
+ });
211
+ return embeddings;
212
+ } catch (err: unknown) {
213
+ wrapProxyError(err);
214
+ }
159
215
  },
160
216
 
161
217
  /**
@@ -175,12 +231,16 @@ export const ai = {
175
231
  prompt: string;
176
232
  system?: string;
177
233
  }) {
178
- return generateObject({
179
- model: resolveModel(options.model),
180
- schema: options.schema,
181
- prompt: options.prompt,
182
- system: options.system,
183
- } as Parameters<typeof generateObject>[0]);
234
+ try {
235
+ return generateObject({
236
+ model: resolveModel(options.model),
237
+ schema: options.schema,
238
+ prompt: options.prompt,
239
+ system: options.system,
240
+ } as Parameters<typeof generateObject>[0]);
241
+ } catch (err: unknown) {
242
+ wrapProxyError(err);
243
+ }
184
244
  },
185
245
 
186
246
  /**
@@ -207,23 +267,27 @@ export const ai = {
207
267
  maxSteps?: number;
208
268
  onStepFinish?: (step: unknown) => void;
209
269
  }) {
210
- const result = await generateText({
211
- model: resolveModel(options.model),
212
- system: options.system,
213
- messages: options.messages as Parameters<typeof generateText>[0]["messages"],
214
- tools: options.tools as Parameters<typeof generateText>[0]["tools"],
215
- maxSteps: options.maxSteps ?? 5,
216
- onStepFinish: options.onStepFinish,
217
- } as Parameters<typeof generateText>[0]);
270
+ try {
271
+ const result = await generateText({
272
+ model: resolveModel(options.model),
273
+ system: options.system,
274
+ messages: options.messages as Parameters<typeof generateText>[0]["messages"],
275
+ tools: options.tools as Parameters<typeof generateText>[0]["tools"],
276
+ maxSteps: options.maxSteps ?? 5,
277
+ onStepFinish: options.onStepFinish,
278
+ } as Parameters<typeof generateText>[0]);
218
279
 
219
- const r = result as unknown as Record<string, unknown>;
220
- return {
221
- text: result.text,
222
- steps: r.steps ?? [],
223
- toolCalls: r.toolCalls ?? [],
224
- toolResults: r.toolResults ?? [],
225
- usage: r.usage,
226
- };
280
+ const r = result as unknown as Record<string, unknown>;
281
+ return {
282
+ text: result.text,
283
+ steps: r.steps ?? [],
284
+ toolCalls: r.toolCalls ?? [],
285
+ toolResults: r.toolResults ?? [],
286
+ usage: r.usage,
287
+ };
288
+ } catch (err: unknown) {
289
+ wrapProxyError(err);
290
+ }
227
291
  },
228
292
 
229
293
  /**
package/templates/ai.ts CHANGED
@@ -1,3 +1,7 @@
1
+ // ⚠️ 이 파일은 Gencow AI 엔진이 자동 생성합니다. 직접 수정하지 마세요.
2
+ // ⚠️ 에러 발생 시 코드를 수정하지 말고 환경변수(.env)를 확인하거나 `npx gencow add AI`로 재설치하세요.
3
+ // ⚠️ PROXY_URL/API_KEY 관련 코드를 삭제하면 클라우드 배포 시 AI가 작동하지 않습니다.
4
+
1
5
  import { generateText, streamText, embed, embedMany, generateObject } from "ai";
2
6
  import { createOpenAI } from "@ai-sdk/openai";
3
7
 
@@ -40,6 +44,42 @@ function resolveModel(model?: Parameters<typeof generateText>[0]["model"]) {
40
44
  return model ?? defaultModel;
41
45
  }
42
46
 
47
+ /**
48
+ * AI 프록시 에러 진단 — 모든 AI 메서드에서 공통 사용.
49
+ * 에러 발생 시 구체적인 진단 메시지를 출력하고 원본 에러를 다시 throw.
50
+ * ⚠️ 이 함수는 ai.ts 수정 금지를 바이브코더에게 전달하는 핵심 방어 메커니즘.
51
+ */
52
+ function wrapProxyError(err: unknown): never {
53
+ // S2: HTTP status 코드 우선 확인 (AI SDK 에러 객체 구조)
54
+ const status = (err as { status?: number; statusCode?: number }).status
55
+ ?? (err as { status?: number; statusCode?: number }).statusCode;
56
+ const msg = err instanceof Error ? err.message : String(err);
57
+
58
+ const isNotFound = status === 404 || (!status && msg.includes("Not Found"));
59
+ const isAuthError = status === 401 || status === 403
60
+ || (!status && (msg.includes("Unauthenticated") || msg.includes("Unauthorized")));
61
+
62
+ if (isNotFound) {
63
+ const proxyUrl = process.env.GENCOW_AI_PROXY_URL;
64
+ console.error(
65
+ `\n🔴 AI Proxy 연결 실패 (404 Not Found)\n` +
66
+ ` GENCOW_AI_PROXY_URL: ${proxyUrl || "(미설정)"}\n` +
67
+ ` 가능한 원인:\n` +
68
+ ` 1. 플랫폼 서버가 최신 버전으로 배포되지 않았을 수 있습니다.\n` +
69
+ ` 2. 환경변수가 올바르게 주입되지 않았을 수 있습니다.\n` +
70
+ ` ⚠️ 이 파일(ai.ts)을 수정하지 마세요. npx gencow add AI로 재설치하세요.\n`
71
+ );
72
+ } else if (isAuthError) {
73
+ console.error(
74
+ `\n🔴 AI 인증 실패\n` +
75
+ ` API_KEY 설정 여부: ${API_KEY ? "✓ 설정됨" : "✗ 미설정"}\n` +
76
+ ` PROXY_URL: ${PROXY_URL || "(미설정 — OpenAI 직접 호출)"}\n` +
77
+ ` ⚠️ .env 파일의 OPENAI_API_KEY를 확인하세요.\n`
78
+ );
79
+ }
80
+ throw err;
81
+ }
82
+
43
83
  /**
44
84
  * Gencow AI Engine — Vercel AI SDK 래퍼
45
85
  *
@@ -73,32 +113,36 @@ export const ai = {
73
113
  responseFormat?: { type: string };
74
114
  }) {
75
115
  const resolvedModel = resolveModel(options.model);
76
- const result = await generateText({
77
- model: resolvedModel,
78
- system: options.system,
79
- messages: options.messages as Parameters<typeof generateText>[0]["messages"],
80
- tools: options.tools as Parameters<typeof generateText>[0]["tools"],
81
- ...(options.temperature !== undefined && { temperature: options.temperature }),
82
- ...(options.maxTokens !== undefined && { maxTokens: options.maxTokens }),
83
- // responseFormat providerOptions (OpenAI JSON mode)
84
- ...(options.responseFormat?.type === "json_object" && {
85
- providerOptions: {
86
- openai: { response_format: { type: "json_object" } },
116
+ try {
117
+ const result = await generateText({
118
+ model: resolvedModel,
119
+ system: options.system,
120
+ messages: options.messages as Parameters<typeof generateText>[0]["messages"],
121
+ tools: options.tools as Parameters<typeof generateText>[0]["tools"],
122
+ ...(options.temperature !== undefined && { temperature: options.temperature }),
123
+ ...(options.maxTokens !== undefined && { maxTokens: options.maxTokens }),
124
+ // responseFormat providerOptions (OpenAI JSON mode)
125
+ ...(options.responseFormat?.type === "json_object" && {
126
+ providerOptions: {
127
+ openai: { response_format: { type: "json_object" } },
128
+ },
129
+ }),
130
+ } as Parameters<typeof generateText>[0]);
131
+ // AIResult 호환 객체 반환
132
+ const usage = (result as unknown as { usage?: { promptTokens?: number; completionTokens?: number; totalTokens?: number } }).usage;
133
+ return {
134
+ text: result.text,
135
+ usage: {
136
+ promptTokens: usage?.promptTokens ?? 0,
137
+ completionTokens: usage?.completionTokens ?? 0,
138
+ totalTokens: usage?.totalTokens ?? 0,
87
139
  },
88
- }),
89
- } as Parameters<typeof generateText>[0]);
90
- // AIResult 호환 객체 반환
91
- const usage = (result as unknown as { usage?: { promptTokens?: number; completionTokens?: number; totalTokens?: number } }).usage;
92
- return {
93
- text: result.text,
94
- usage: {
95
- promptTokens: usage?.promptTokens ?? 0,
96
- completionTokens: usage?.completionTokens ?? 0,
97
- totalTokens: usage?.totalTokens ?? 0,
98
- },
99
- creditsCharged: 0, // 클라우드: 프록시 측에서 차감. 로컬: 차감 없음.
100
- model: (resolvedModel as unknown as { modelId?: string }).modelId ?? "gpt-4o-mini",
101
- };
140
+ creditsCharged: 0, // 클라우드: 프록시 측에서 차감. 로컬: 차감 없음.
141
+ model: (resolvedModel as unknown as { modelId?: string }).modelId ?? "gpt-4o-mini",
142
+ };
143
+ } catch (err: unknown) {
144
+ wrapProxyError(err);
145
+ }
102
146
  },
103
147
 
104
148
  /**
@@ -120,11 +164,15 @@ export const ai = {
120
164
  system?: string;
121
165
  messages: { role: string; content: string }[];
122
166
  }) {
123
- return streamText({
124
- model: resolveModel(options.model),
125
- system: options.system,
126
- messages: options.messages as Parameters<typeof streamText>[0]["messages"],
127
- });
167
+ try {
168
+ return streamText({
169
+ model: resolveModel(options.model),
170
+ system: options.system,
171
+ messages: options.messages as Parameters<typeof streamText>[0]["messages"],
172
+ });
173
+ } catch (err: unknown) {
174
+ wrapProxyError(err);
175
+ }
128
176
  },
129
177
 
130
178
  /**
@@ -134,11 +182,15 @@ export const ai = {
134
182
  * const vector = await ai.embed("검색할 텍스트");
135
183
  */
136
184
  async embed(text: string, model?: Parameters<typeof embed>[0]["model"]) {
137
- const { embedding } = await embed({
138
- model: model ?? openaiProvider.embedding("text-embedding-3-small"),
139
- value: text,
140
- });
141
- return embedding;
185
+ try {
186
+ const { embedding } = await embed({
187
+ model: model ?? openaiProvider.embedding("text-embedding-3-small"),
188
+ value: text,
189
+ });
190
+ return embedding;
191
+ } catch (err: unknown) {
192
+ wrapProxyError(err);
193
+ }
142
194
  },
143
195
 
144
196
  /**
@@ -151,11 +203,15 @@ export const ai = {
151
203
  * const embeddings = await ai.embedMany(["텍스트1", "텍스트2", "텍스트3"]);
152
204
  */
153
205
  async embedMany(texts: string[], model?: Parameters<typeof embedMany>[0]["model"]) {
154
- const { embeddings } = await embedMany({
155
- model: model ?? openaiProvider.embedding("text-embedding-3-small"),
156
- values: texts,
157
- });
158
- return embeddings;
206
+ try {
207
+ const { embeddings } = await embedMany({
208
+ model: model ?? openaiProvider.embedding("text-embedding-3-small"),
209
+ values: texts,
210
+ });
211
+ return embeddings;
212
+ } catch (err: unknown) {
213
+ wrapProxyError(err);
214
+ }
159
215
  },
160
216
 
161
217
  /**
@@ -175,12 +231,16 @@ export const ai = {
175
231
  prompt: string;
176
232
  system?: string;
177
233
  }) {
178
- return generateObject({
179
- model: resolveModel(options.model),
180
- schema: options.schema,
181
- prompt: options.prompt,
182
- system: options.system,
183
- } as Parameters<typeof generateObject>[0]);
234
+ try {
235
+ return generateObject({
236
+ model: resolveModel(options.model),
237
+ schema: options.schema,
238
+ prompt: options.prompt,
239
+ system: options.system,
240
+ } as Parameters<typeof generateObject>[0]);
241
+ } catch (err: unknown) {
242
+ wrapProxyError(err);
243
+ }
184
244
  },
185
245
 
186
246
  /**
@@ -207,23 +267,27 @@ export const ai = {
207
267
  maxSteps?: number;
208
268
  onStepFinish?: (step: unknown) => void;
209
269
  }) {
210
- const result = await generateText({
211
- model: resolveModel(options.model),
212
- system: options.system,
213
- messages: options.messages as Parameters<typeof generateText>[0]["messages"],
214
- tools: options.tools as Parameters<typeof generateText>[0]["tools"],
215
- maxSteps: options.maxSteps ?? 5,
216
- onStepFinish: options.onStepFinish,
217
- } as Parameters<typeof generateText>[0]);
270
+ try {
271
+ const result = await generateText({
272
+ model: resolveModel(options.model),
273
+ system: options.system,
274
+ messages: options.messages as Parameters<typeof generateText>[0]["messages"],
275
+ tools: options.tools as Parameters<typeof generateText>[0]["tools"],
276
+ maxSteps: options.maxSteps ?? 5,
277
+ onStepFinish: options.onStepFinish,
278
+ } as Parameters<typeof generateText>[0]);
218
279
 
219
- const r = result as unknown as Record<string, unknown>;
220
- return {
221
- text: result.text,
222
- steps: r.steps ?? [],
223
- toolCalls: r.toolCalls ?? [],
224
- toolResults: r.toolResults ?? [],
225
- usage: r.usage,
226
- };
280
+ const r = result as unknown as Record<string, unknown>;
281
+ return {
282
+ text: result.text,
283
+ steps: r.steps ?? [],
284
+ toolCalls: r.toolCalls ?? [],
285
+ toolResults: r.toolResults ?? [],
286
+ usage: r.usage,
287
+ };
288
+ } catch (err: unknown) {
289
+ wrapProxyError(err);
290
+ }
227
291
  },
228
292
 
229
293
  /**
@@ -1,3 +1,7 @@
1
+ // ⚠️ 이 파일은 Gencow AI 엔진이 자동 생성합니다. 직접 수정하지 마세요.
2
+ // ⚠️ 에러 발생 시 코드를 수정하지 말고 환경변수(.env)를 확인하거나 `npx gencow add AI`로 재설치하세요.
3
+ // ⚠️ PROXY_URL/API_KEY 관련 코드를 삭제하면 클라우드 배포 시 AI가 작동하지 않습니다.
4
+
1
5
  import { generateText, streamText, embed, embedMany, generateObject } from "ai";
2
6
  import { createOpenAI } from "@ai-sdk/openai";
3
7
 
@@ -40,6 +44,42 @@ function resolveModel(model?: Parameters<typeof generateText>[0]["model"]) {
40
44
  return model ?? defaultModel;
41
45
  }
42
46
 
47
+ /**
48
+ * AI 프록시 에러 진단 — 모든 AI 메서드에서 공통 사용.
49
+ * 에러 발생 시 구체적인 진단 메시지를 출력하고 원본 에러를 다시 throw.
50
+ * ⚠️ 이 함수는 ai.ts 수정 금지를 바이브코더에게 전달하는 핵심 방어 메커니즘.
51
+ */
52
+ function wrapProxyError(err: unknown): never {
53
+ // S2: HTTP status 코드 우선 확인 (AI SDK 에러 객체 구조)
54
+ const status = (err as { status?: number; statusCode?: number }).status
55
+ ?? (err as { status?: number; statusCode?: number }).statusCode;
56
+ const msg = err instanceof Error ? err.message : String(err);
57
+
58
+ const isNotFound = status === 404 || (!status && msg.includes("Not Found"));
59
+ const isAuthError = status === 401 || status === 403
60
+ || (!status && (msg.includes("Unauthenticated") || msg.includes("Unauthorized")));
61
+
62
+ if (isNotFound) {
63
+ const proxyUrl = process.env.GENCOW_AI_PROXY_URL;
64
+ console.error(
65
+ `\n🔴 AI Proxy 연결 실패 (404 Not Found)\n` +
66
+ ` GENCOW_AI_PROXY_URL: ${proxyUrl || "(미설정)"}\n` +
67
+ ` 가능한 원인:\n` +
68
+ ` 1. 플랫폼 서버가 최신 버전으로 배포되지 않았을 수 있습니다.\n` +
69
+ ` 2. 환경변수가 올바르게 주입되지 않았을 수 있습니다.\n` +
70
+ ` ⚠️ 이 파일(ai.ts)을 수정하지 마세요. npx gencow add AI로 재설치하세요.\n`
71
+ );
72
+ } else if (isAuthError) {
73
+ console.error(
74
+ `\n🔴 AI 인증 실패\n` +
75
+ ` API_KEY 설정 여부: ${API_KEY ? "✓ 설정됨" : "✗ 미설정"}\n` +
76
+ ` PROXY_URL: ${PROXY_URL || "(미설정 — OpenAI 직접 호출)"}\n` +
77
+ ` ⚠️ .env 파일의 OPENAI_API_KEY를 확인하세요.\n`
78
+ );
79
+ }
80
+ throw err;
81
+ }
82
+
43
83
  /**
44
84
  * Gencow AI Engine — Vercel AI SDK 래퍼
45
85
  *
@@ -73,32 +113,36 @@ export const ai = {
73
113
  responseFormat?: { type: string };
74
114
  }) {
75
115
  const resolvedModel = resolveModel(options.model);
76
- const result = await generateText({
77
- model: resolvedModel,
78
- system: options.system,
79
- messages: options.messages as Parameters<typeof generateText>[0]["messages"],
80
- tools: options.tools as Parameters<typeof generateText>[0]["tools"],
81
- ...(options.temperature !== undefined && { temperature: options.temperature }),
82
- ...(options.maxTokens !== undefined && { maxTokens: options.maxTokens }),
83
- // responseFormat providerOptions (OpenAI JSON mode)
84
- ...(options.responseFormat?.type === "json_object" && {
85
- providerOptions: {
86
- openai: { response_format: { type: "json_object" } },
116
+ try {
117
+ const result = await generateText({
118
+ model: resolvedModel,
119
+ system: options.system,
120
+ messages: options.messages as Parameters<typeof generateText>[0]["messages"],
121
+ tools: options.tools as Parameters<typeof generateText>[0]["tools"],
122
+ ...(options.temperature !== undefined && { temperature: options.temperature }),
123
+ ...(options.maxTokens !== undefined && { maxTokens: options.maxTokens }),
124
+ // responseFormat providerOptions (OpenAI JSON mode)
125
+ ...(options.responseFormat?.type === "json_object" && {
126
+ providerOptions: {
127
+ openai: { response_format: { type: "json_object" } },
128
+ },
129
+ }),
130
+ } as Parameters<typeof generateText>[0]);
131
+ // AIResult 호환 객체 반환
132
+ const usage = (result as unknown as { usage?: { promptTokens?: number; completionTokens?: number; totalTokens?: number } }).usage;
133
+ return {
134
+ text: result.text,
135
+ usage: {
136
+ promptTokens: usage?.promptTokens ?? 0,
137
+ completionTokens: usage?.completionTokens ?? 0,
138
+ totalTokens: usage?.totalTokens ?? 0,
87
139
  },
88
- }),
89
- } as Parameters<typeof generateText>[0]);
90
- // AIResult 호환 객체 반환
91
- const usage = (result as unknown as { usage?: { promptTokens?: number; completionTokens?: number; totalTokens?: number } }).usage;
92
- return {
93
- text: result.text,
94
- usage: {
95
- promptTokens: usage?.promptTokens ?? 0,
96
- completionTokens: usage?.completionTokens ?? 0,
97
- totalTokens: usage?.totalTokens ?? 0,
98
- },
99
- creditsCharged: 0, // 클라우드: 프록시 측에서 차감. 로컬: 차감 없음.
100
- model: (resolvedModel as unknown as { modelId?: string }).modelId ?? "gpt-4o-mini",
101
- };
140
+ creditsCharged: 0, // 클라우드: 프록시 측에서 차감. 로컬: 차감 없음.
141
+ model: (resolvedModel as unknown as { modelId?: string }).modelId ?? "gpt-4o-mini",
142
+ };
143
+ } catch (err: unknown) {
144
+ wrapProxyError(err);
145
+ }
102
146
  },
103
147
 
104
148
  /**
@@ -120,11 +164,15 @@ export const ai = {
120
164
  system?: string;
121
165
  messages: { role: string; content: string }[];
122
166
  }) {
123
- return streamText({
124
- model: resolveModel(options.model),
125
- system: options.system,
126
- messages: options.messages as Parameters<typeof streamText>[0]["messages"],
127
- });
167
+ try {
168
+ return streamText({
169
+ model: resolveModel(options.model),
170
+ system: options.system,
171
+ messages: options.messages as Parameters<typeof streamText>[0]["messages"],
172
+ });
173
+ } catch (err: unknown) {
174
+ wrapProxyError(err);
175
+ }
128
176
  },
129
177
 
130
178
  /**
@@ -134,11 +182,15 @@ export const ai = {
134
182
  * const vector = await ai.embed("검색할 텍스트");
135
183
  */
136
184
  async embed(text: string, model?: Parameters<typeof embed>[0]["model"]) {
137
- const { embedding } = await embed({
138
- model: model ?? openaiProvider.embedding("text-embedding-3-small"),
139
- value: text,
140
- });
141
- return embedding;
185
+ try {
186
+ const { embedding } = await embed({
187
+ model: model ?? openaiProvider.embedding("text-embedding-3-small"),
188
+ value: text,
189
+ });
190
+ return embedding;
191
+ } catch (err: unknown) {
192
+ wrapProxyError(err);
193
+ }
142
194
  },
143
195
 
144
196
  /**
@@ -151,11 +203,15 @@ export const ai = {
151
203
  * const embeddings = await ai.embedMany(["텍스트1", "텍스트2", "텍스트3"]);
152
204
  */
153
205
  async embedMany(texts: string[], model?: Parameters<typeof embedMany>[0]["model"]) {
154
- const { embeddings } = await embedMany({
155
- model: model ?? openaiProvider.embedding("text-embedding-3-small"),
156
- values: texts,
157
- });
158
- return embeddings;
206
+ try {
207
+ const { embeddings } = await embedMany({
208
+ model: model ?? openaiProvider.embedding("text-embedding-3-small"),
209
+ values: texts,
210
+ });
211
+ return embeddings;
212
+ } catch (err: unknown) {
213
+ wrapProxyError(err);
214
+ }
159
215
  },
160
216
 
161
217
  /**
@@ -175,12 +231,16 @@ export const ai = {
175
231
  prompt: string;
176
232
  system?: string;
177
233
  }) {
178
- return generateObject({
179
- model: resolveModel(options.model),
180
- schema: options.schema,
181
- prompt: options.prompt,
182
- system: options.system,
183
- } as Parameters<typeof generateObject>[0]);
234
+ try {
235
+ return generateObject({
236
+ model: resolveModel(options.model),
237
+ schema: options.schema,
238
+ prompt: options.prompt,
239
+ system: options.system,
240
+ } as Parameters<typeof generateObject>[0]);
241
+ } catch (err: unknown) {
242
+ wrapProxyError(err);
243
+ }
184
244
  },
185
245
 
186
246
  /**
@@ -207,23 +267,27 @@ export const ai = {
207
267
  maxSteps?: number;
208
268
  onStepFinish?: (step: unknown) => void;
209
269
  }) {
210
- const result = await generateText({
211
- model: resolveModel(options.model),
212
- system: options.system,
213
- messages: options.messages as Parameters<typeof generateText>[0]["messages"],
214
- tools: options.tools as Parameters<typeof generateText>[0]["tools"],
215
- maxSteps: options.maxSteps ?? 5,
216
- onStepFinish: options.onStepFinish,
217
- } as Parameters<typeof generateText>[0]);
270
+ try {
271
+ const result = await generateText({
272
+ model: resolveModel(options.model),
273
+ system: options.system,
274
+ messages: options.messages as Parameters<typeof generateText>[0]["messages"],
275
+ tools: options.tools as Parameters<typeof generateText>[0]["tools"],
276
+ maxSteps: options.maxSteps ?? 5,
277
+ onStepFinish: options.onStepFinish,
278
+ } as Parameters<typeof generateText>[0]);
218
279
 
219
- const r = result as unknown as Record<string, unknown>;
220
- return {
221
- text: result.text,
222
- steps: r.steps ?? [],
223
- toolCalls: r.toolCalls ?? [],
224
- toolResults: r.toolResults ?? [],
225
- usage: r.usage,
226
- };
280
+ const r = result as unknown as Record<string, unknown>;
281
+ return {
282
+ text: result.text,
283
+ steps: r.steps ?? [],
284
+ toolCalls: r.toolCalls ?? [],
285
+ toolResults: r.toolResults ?? [],
286
+ usage: r.usage,
287
+ };
288
+ } catch (err: unknown) {
289
+ wrapProxyError(err);
290
+ }
227
291
  },
228
292
 
229
293
  /**