@things-factory/ai-client-base 10.0.0-beta.64

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/config/config.development.js +72 -0
  2. package/config/config.production.js +24 -0
  3. package/dist-server/index.d.ts +19 -0
  4. package/dist-server/index.js +23 -0
  5. package/dist-server/index.js.map +1 -0
  6. package/dist-server/service/factory.d.ts +16 -0
  7. package/dist-server/service/factory.js +65 -0
  8. package/dist-server/service/factory.js.map +1 -0
  9. package/dist-server/service/index.d.ts +4 -0
  10. package/dist-server/service/index.js +8 -0
  11. package/dist-server/service/index.js.map +1 -0
  12. package/dist-server/service/providers/anthropic.d.ts +13 -0
  13. package/dist-server/service/providers/anthropic.js +201 -0
  14. package/dist-server/service/providers/anthropic.js.map +1 -0
  15. package/dist-server/service/providers/gemini.d.ts +14 -0
  16. package/dist-server/service/providers/gemini.js +292 -0
  17. package/dist-server/service/providers/gemini.js.map +1 -0
  18. package/dist-server/service/providers/index.d.ts +6 -0
  19. package/dist-server/service/providers/index.js +14 -0
  20. package/dist-server/service/providers/index.js.map +1 -0
  21. package/dist-server/service/providers/openai.d.ts +13 -0
  22. package/dist-server/service/providers/openai.js +232 -0
  23. package/dist-server/service/providers/openai.js.map +1 -0
  24. package/dist-server/service/types.d.ts +171 -0
  25. package/dist-server/service/types.js +8 -0
  26. package/dist-server/service/types.js.map +1 -0
  27. package/dist-server/service/utils/index.d.ts +1 -0
  28. package/dist-server/service/utils/index.js +5 -0
  29. package/dist-server/service/utils/index.js.map +1 -0
  30. package/dist-server/service/utils/parse-json.d.ts +20 -0
  31. package/dist-server/service/utils/parse-json.js +99 -0
  32. package/dist-server/service/utils/parse-json.js.map +1 -0
  33. package/dist-server/tsconfig.tsbuildinfo +1 -0
  34. package/package.json +31 -0
  35. package/server/index.ts +19 -0
  36. package/server/service/factory.ts +68 -0
  37. package/server/service/index.ts +4 -0
  38. package/server/service/providers/anthropic.ts +252 -0
  39. package/server/service/providers/gemini.ts +353 -0
  40. package/server/service/providers/index.ts +8 -0
  41. package/server/service/providers/openai.ts +284 -0
  42. package/server/service/types.ts +197 -0
  43. package/server/service/utils/index.ts +1 -0
  44. package/server/service/utils/parse-json.ts +100 -0
  45. package/things-factory.config.js +1 -0
  46. package/tsconfig.json +9 -0
@@ -0,0 +1,292 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GeminiAIClient = void 0;
4
+ /**
5
+ * Google Gemini AI Client adapter.
6
+ *
7
+ * 기본 모델: gemini-3-pro (구성에서 override 가능).
8
+ * SDK: @google/generative-ai
9
+ *
10
+ * 비고: Antigravity (Google 의 agentic IDE) 가 기본 백엔드로 사용하는 모델군.
11
+ * 이 어댑터는 Gemini API 직접 호출 — Antigravity IDE 와는 별개.
12
+ */
13
+ const generative_ai_1 = require("@google/generative-ai");
14
+ const factory_js_1 = require("../factory.js");
15
+ const parse_json_js_1 = require("../utils/parse-json.js");
16
+ // `gemini-3-pro` 는 2026-03-09 종료됨. 후속 권장 모델로 변경.
17
+ // 사용자가 자기 환경에서 활용 가능한 정확한 ID 는 ListModels 로 확인:
18
+ // curl "https://generativelanguage.googleapis.com/v1beta/models?key=$KEY"
19
+ const DEFAULT_MODEL = 'gemini-3-flash-preview';
20
+ const DEFAULT_MAX_TOKENS = 8192;
21
+ class GeminiAIClient {
22
+ constructor(config) {
23
+ // Gemini SDK 는 환경변수 자동 인식이 없으므로 어댑터에서 명시적으로 fallback.
24
+ // 우선순위: config.apiKey → GOOGLE_API_KEY → GEMINI_API_KEY → GOOGLE_GENERATIVE_AI_API_KEY
25
+ const apiKey = config.apiKey ??
26
+ process.env.GOOGLE_API_KEY ??
27
+ process.env.GEMINI_API_KEY ??
28
+ process.env.GOOGLE_GENERATIVE_AI_API_KEY ??
29
+ '';
30
+ this.sdk = new generative_ai_1.GoogleGenerativeAI(apiKey);
31
+ this.defaultModel = config.model ?? DEFAULT_MODEL;
32
+ this.id = `gemini:${this.defaultModel}`;
33
+ }
34
+ async chat(messages, options = {}) {
35
+ const { systemPrompt, contents } = splitMessages(messages, options.systemPrompt);
36
+ const model = this.buildModel(options, systemPrompt);
37
+ const result = await model.generateContent({ contents });
38
+ return result.response.text();
39
+ }
40
+ async generateJSON(prompt, options = {}) {
41
+ const messages = typeof prompt === 'string' ? [{ role: 'user', content: prompt }] : prompt;
42
+ const { systemPrompt, contents } = splitMessages(messages, options.systemPrompt);
43
+ const generationConfig = baseGenerationConfig(options);
44
+ generationConfig.responseMimeType = 'application/json';
45
+ if (options.schema)
46
+ generationConfig.responseSchema = options.schema;
47
+ const model = this.sdk.getGenerativeModel({
48
+ model: options.model ?? this.defaultModel,
49
+ ...(systemPrompt && { systemInstruction: systemPrompt }),
50
+ generationConfig
51
+ });
52
+ const result = await model.generateContent({ contents });
53
+ const text = result.response.text();
54
+ return (0, parse_json_js_1.parseJSONResponse)(text);
55
+ }
56
+ async vision(image, prompt, options = {}) {
57
+ const imagePart = typeof image === 'object' && 'url' in image
58
+ ? { type: 'image', source: { kind: 'url', url: image.url } }
59
+ : {
60
+ type: 'image',
61
+ source: { kind: 'base64', data: image, mediaType: 'image/png' }
62
+ };
63
+ return this.chat([{ role: 'user', content: [imagePart, { type: 'text', text: prompt }] }], options);
64
+ }
65
+ buildModel(options, systemInstruction) {
66
+ return this.sdk.getGenerativeModel({
67
+ model: options.model ?? this.defaultModel,
68
+ ...(systemInstruction && { systemInstruction }),
69
+ generationConfig: baseGenerationConfig(options)
70
+ });
71
+ }
72
+ async chatWithTools(messages, tools, options = {}) {
73
+ const { systemPrompt, contents } = splitMessages(messages, options.systemPrompt);
74
+ // Gemini 는 OpenAPI 3.0 subset — JSON Schema 의 일부 키를 거부함.
75
+ // 'additionalProperties' / '$schema' / '$defs' 등을 제거.
76
+ const functionDeclarations = tools.map(t => ({
77
+ name: t.name,
78
+ description: t.description,
79
+ parameters: sanitizeForGeminiSchema(t.parameters)
80
+ }));
81
+ const toolConfig = buildGeminiToolConfig(options);
82
+ const model = this.sdk.getGenerativeModel({
83
+ model: options.model ?? this.defaultModel,
84
+ ...(systemPrompt && { systemInstruction: systemPrompt }),
85
+ generationConfig: baseGenerationConfig(options),
86
+ tools: [{ functionDeclarations }],
87
+ ...(toolConfig && { toolConfig })
88
+ });
89
+ const result = await model.generateContent({ contents });
90
+ const response = result.response;
91
+ const candidate = response?.candidates?.[0];
92
+ const parts = candidate?.content?.parts ?? [];
93
+ // Gemini 는 한 응답에서 (a) thinking part (p.thought === true) 와
94
+ // (b) 본문 text part 를 함께 emit 할 수 있다. thought 는 사용자에게 노출하면
95
+ // "구조화 답변 + 산문 답변" 형태의 중복으로 보이므로 제거.
96
+ const textParts = parts
97
+ .filter((p) => typeof p?.text === 'string' && p?.thought !== true)
98
+ .map((p) => p.text);
99
+ const text = textParts.join('') || undefined;
100
+ // Gemini 는 functionCall 에 자체 id 가 없음 — 어댑터가 turn 내부에서 부여.
101
+ // id 포맷 'g:<name>:<seq>' — assistant 가 tool_result 를 만들 때 이 id 를 그대로
102
+ // 넘기면 toGeminiPart 가 name 을 추출해 functionResponse.name 에 채운다.
103
+ //
104
+ // thoughtSignature: Gemini 2.5/3 부터 thinking 모델이 functionCall 에 서명을 붙임.
105
+ // 다음 turn 에 그대로 echo 해야 400 에러 안 남 (cite: ai.google.dev/.../thought-signatures).
106
+ let geminiCallSeq = 0;
107
+ const ts = Date.now();
108
+ const toolCalls = parts
109
+ .filter((p) => p?.functionCall && typeof p.functionCall.name === 'string')
110
+ .map((p) => {
111
+ // SDK 가 camelCase 또는 snake_case 둘 중 하나로 emit — 둘 다 받음.
112
+ const sig = p.thoughtSignature ?? p.thought_signature;
113
+ return {
114
+ id: `g:${p.functionCall.name}:${ts}_${geminiCallSeq++}`,
115
+ name: p.functionCall.name,
116
+ arguments: p.functionCall.args && typeof p.functionCall.args === 'object'
117
+ ? p.functionCall.args
118
+ : {},
119
+ ...(typeof sig === 'string' && sig.length > 0 && {
120
+ providerMeta: { thoughtSignature: sig }
121
+ })
122
+ };
123
+ });
124
+ return {
125
+ text,
126
+ toolCalls,
127
+ stopReason: normalizeGeminiStopReason(candidate?.finishReason, toolCalls.length > 0)
128
+ };
129
+ }
130
+ }
131
+ exports.GeminiAIClient = GeminiAIClient;
132
+ // ── 변환 유틸 ─────────────────────────────────────────────────────────
133
+ function baseGenerationConfig(options) {
134
+ return {
135
+ maxOutputTokens: options.maxTokens ?? DEFAULT_MAX_TOKENS,
136
+ ...(options.temperature !== undefined && { temperature: options.temperature }),
137
+ ...(options.stopSequences && { stopSequences: options.stopSequences })
138
+ };
139
+ }
140
+ function splitMessages(messages, externalSystem) {
141
+ const systemMessages = messages
142
+ .filter(m => m.role === 'system')
143
+ .map(extractTextContent)
144
+ .filter(Boolean);
145
+ const conversation = messages.filter(m => m.role !== 'system');
146
+ const systemPrompt = [externalSystem, ...systemMessages].filter(Boolean).join('\n\n');
147
+ return { systemPrompt, contents: conversation.map(toGeminiContent) };
148
+ }
149
+ function extractTextContent(msg) {
150
+ if (typeof msg.content === 'string')
151
+ return msg.content;
152
+ return msg.content
153
+ .filter(p => p.type === 'text')
154
+ .map(p => p.text)
155
+ .join('\n');
156
+ }
157
+ function toGeminiContent(msg) {
158
+ // Our 'assistant' role → Gemini's 'model' role
159
+ const role = msg.role === 'assistant' ? 'model' : msg.role;
160
+ const parts = typeof msg.content === 'string'
161
+ ? [{ text: msg.content }]
162
+ : msg.content.map(toGeminiPart);
163
+ return { role, parts };
164
+ }
165
+ function toGeminiPart(part) {
166
+ if (part.type === 'text') {
167
+ return { text: part.text };
168
+ }
169
+ if (part.type === 'tool_use') {
170
+ // Gemini: assistant 가 호출 의도 표현. functionCall.name + args 만 가짐 (id 없음).
171
+ // thoughtSignature 는 thinking 모델 사용 시 필수 — providerMeta 에서 복원해 echo.
172
+ const sig = part.providerMeta?.thoughtSignature;
173
+ return {
174
+ functionCall: { name: part.name, args: part.arguments },
175
+ ...(typeof sig === 'string' && sig.length > 0 && { thoughtSignature: sig })
176
+ };
177
+ }
178
+ if (part.type === 'tool_result') {
179
+ // Gemini: user role 에 functionResponse part 로 회신. 이름은 호출했던 tool name 과
180
+ // 같아야 함 — assistant.ts 의 loop 에서 part 생성 시 함께 전달.
181
+ let parsed = part.content;
182
+ try {
183
+ parsed = JSON.parse(part.content);
184
+ }
185
+ catch {
186
+ parsed = { result: part.content };
187
+ }
188
+ return {
189
+ functionResponse: {
190
+ // name 은 원래 호출했던 tool name. toolUseId 에 임베드되어 있지 않으므로
191
+ // assistant 가 메시지 생성 시 별도 채워줘야 함 — 여기서는 placeholder.
192
+ // 실제 사용 시 toolUseId 를 'tool_<name>_<seq>' 형태로 만들어 name 추출.
193
+ name: extractGeminiNameFromId(part.toolUseId),
194
+ response: parsed
195
+ }
196
+ };
197
+ }
198
+ // image
199
+ if (part.source.kind === 'url') {
200
+ // Gemini SDK 는 inlineData(base64) 만 지원. URL 은 호출 전에 fetch 필요.
201
+ throw new Error('Gemini adapter: image URL is not supported directly. Fetch the URL and pass as Buffer.');
202
+ }
203
+ const data = Buffer.isBuffer(part.source.data)
204
+ ? part.source.data.toString('base64')
205
+ : Buffer.from(part.source.data).toString('base64');
206
+ return {
207
+ inlineData: { data, mimeType: part.source.mediaType }
208
+ };
209
+ }
210
+ /**
211
+ * Gemini 의 functionCall 에는 자체 id 가 없어서 chatWithTools 가 'gemini_<ts>_<seq>'
212
+ * 형태의 가짜 id 를 만든다. 그런데 functionResponse 에는 name 이 필수 — assistant 의
213
+ * tool 실행기는 이 id 와 name 을 별도 매핑해야 한다. assistant 가 tool_result part 를
214
+ * 만들 때 toolUseId 에 'name:<actualName>;id:<originalId>' 같은 인코딩을 할 수 있지만
215
+ * 그건 invasive — 대신 toolUseId 가 'g:<name>:<id>' 형태면 name 을 직접 추출.
216
+ *
217
+ * 현재는 fallback: id 가 'g:' 로 시작하면 두 번째 segment 를 name 으로,
218
+ * 아니면 그대로 사용 (Gemini 가 받지 않으면 에러 — assistant 가 책임지고 인코딩).
219
+ */
220
+ function extractGeminiNameFromId(id) {
221
+ if (id.startsWith('g:')) {
222
+ const parts = id.split(':');
223
+ return parts[1] || id;
224
+ }
225
+ return id;
226
+ }
227
+ /**
228
+ * JSON Schema → Gemini OpenAPI 3.0 subset 변환.
229
+ * Gemini API 가 거부하는 키들 (deep) 제거.
230
+ * 보존: type / properties / items / required / enum / description / format / nullable
231
+ */
232
+ const GEMINI_REJECTED_KEYS = new Set([
233
+ 'additionalProperties',
234
+ '$schema',
235
+ '$defs',
236
+ '$ref',
237
+ '$id',
238
+ 'definitions',
239
+ 'patternProperties',
240
+ 'dependencies'
241
+ ]);
242
+ function sanitizeForGeminiSchema(schema) {
243
+ if (schema === null || schema === undefined)
244
+ return schema;
245
+ if (Array.isArray(schema))
246
+ return schema.map(sanitizeForGeminiSchema);
247
+ if (typeof schema !== 'object')
248
+ return schema;
249
+ const out = {};
250
+ for (const [k, v] of Object.entries(schema)) {
251
+ if (GEMINI_REJECTED_KEYS.has(k))
252
+ continue;
253
+ out[k] = sanitizeForGeminiSchema(v);
254
+ }
255
+ return out;
256
+ }
257
+ function buildGeminiToolConfig(options) {
258
+ const choice = options.toolChoice ?? 'auto';
259
+ if (choice === 'none') {
260
+ return { functionCallingConfig: { mode: 'NONE' } };
261
+ }
262
+ if (choice === 'required') {
263
+ return {
264
+ functionCallingConfig: {
265
+ mode: 'ANY',
266
+ ...(options.forceTool && { allowedFunctionNames: [options.forceTool] })
267
+ }
268
+ };
269
+ }
270
+ return { functionCallingConfig: { mode: 'AUTO' } };
271
+ }
272
+ function normalizeGeminiStopReason(raw, hasToolCalls) {
273
+ // Gemini 는 function call 발생 시 finishReason 을 'STOP' 으로 마킹할 수 있어
274
+ // toolCalls 존재 여부로 우선 판정.
275
+ if (hasToolCalls)
276
+ return 'tool_use';
277
+ switch (raw) {
278
+ case 'STOP':
279
+ return 'end_turn';
280
+ case 'MAX_TOKENS':
281
+ return 'max_tokens';
282
+ case 'SAFETY':
283
+ case 'RECITATION':
284
+ case 'OTHER':
285
+ return 'stop_sequence';
286
+ default:
287
+ return raw ?? 'end_turn';
288
+ }
289
+ }
290
+ // Auto-register at module load
291
+ (0, factory_js_1.registerAIProvider)('gemini', config => new GeminiAIClient(config));
292
+ //# sourceMappingURL=gemini.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gemini.js","sourceRoot":"","sources":["../../../server/service/providers/gemini.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Google Gemini AI Client adapter.\n *\n * 기본 모델: gemini-3-pro (구성에서 override 가능).\n * SDK: @google/generative-ai\n *\n * 비고: Antigravity (Google 의 agentic IDE) 가 기본 백엔드로 사용하는 모델군.\n * 이 어댑터는 Gemini API 직접 호출 — Antigravity IDE 와는 별개.\n */\nimport { GoogleGenerativeAI } from '@google/generative-ai'\nimport type {\n AIChatOptions,\n AIClient,\n AIClientConfig,\n AIContentPart,\n AIJSONOptions,\n AIMessage,\n AITool,\n AIToolCall,\n AIToolChatOptions,\n AIToolChatResult,\n AIVisionOptions\n} from '../types.js'\nimport { registerAIProvider } from '../factory.js'\nimport { parseJSONResponse } from '../utils/parse-json.js'\n\n// `gemini-3-pro` 는 2026-03-09 종료됨. 후속 권장 모델로 변경.\n// 사용자가 자기 환경에서 활용 가능한 정확한 ID 는 ListModels 로 확인:\n// curl \"https://generativelanguage.googleapis.com/v1beta/models?key=$KEY\"\nconst DEFAULT_MODEL = 'gemini-3-flash-preview'\nconst DEFAULT_MAX_TOKENS = 8192\n\nexport class GeminiAIClient implements AIClient {\n readonly id: string\n private sdk: GoogleGenerativeAI\n private defaultModel: string\n\n constructor(config: AIClientConfig) {\n // Gemini SDK 는 환경변수 자동 인식이 없으므로 어댑터에서 명시적으로 fallback.\n // 우선순위: config.apiKey → GOOGLE_API_KEY → GEMINI_API_KEY → GOOGLE_GENERATIVE_AI_API_KEY\n const apiKey =\n config.apiKey ??\n process.env.GOOGLE_API_KEY ??\n process.env.GEMINI_API_KEY ??\n process.env.GOOGLE_GENERATIVE_AI_API_KEY ??\n ''\n this.sdk = new GoogleGenerativeAI(apiKey)\n this.defaultModel = config.model ?? DEFAULT_MODEL\n this.id = `gemini:${this.defaultModel}`\n }\n\n async chat(messages: AIMessage[], options: AIChatOptions = {}): Promise<string> {\n const { systemPrompt, contents } = splitMessages(messages, options.systemPrompt)\n const model = this.buildModel(options, systemPrompt)\n const result = await model.generateContent({ contents } as any)\n return result.response.text()\n }\n\n async generateJSON<T = unknown>(\n prompt: string | AIMessage[],\n options: AIJSONOptions = {}\n ): Promise<T> {\n const messages: AIMessage[] =\n typeof prompt === 'string' ? [{ role: 'user', content: prompt }] : prompt\n const { systemPrompt, contents } = splitMessages(messages, options.systemPrompt)\n\n const generationConfig: any = baseGenerationConfig(options)\n generationConfig.responseMimeType = 'application/json'\n if (options.schema) generationConfig.responseSchema = options.schema\n\n const model = this.sdk.getGenerativeModel({\n model: options.model ?? this.defaultModel,\n ...(systemPrompt && { systemInstruction: systemPrompt }),\n generationConfig\n })\n const result = await model.generateContent({ contents } as any)\n const text = result.response.text()\n return parseJSONResponse<T>(text)\n }\n\n async vision(\n image: Buffer | Uint8Array | { url: string },\n prompt: string,\n options: AIVisionOptions = {}\n ): Promise<string> {\n const imagePart: AIContentPart =\n typeof image === 'object' && 'url' in image\n ? { type: 'image', source: { kind: 'url', url: image.url } }\n : {\n type: 'image',\n source: { kind: 'base64', data: image as Buffer | Uint8Array, mediaType: 'image/png' }\n }\n\n return this.chat(\n [{ role: 'user', content: [imagePart, { type: 'text', text: prompt }] }],\n options\n )\n }\n\n private buildModel(options: AIChatOptions, systemInstruction?: string) {\n return this.sdk.getGenerativeModel({\n model: options.model ?? this.defaultModel,\n ...(systemInstruction && { systemInstruction }),\n generationConfig: baseGenerationConfig(options)\n })\n }\n\n async chatWithTools(\n messages: AIMessage[],\n tools: AITool[],\n options: AIToolChatOptions = {}\n ): Promise<AIToolChatResult> {\n const { systemPrompt, contents } = splitMessages(messages, options.systemPrompt)\n\n // Gemini 는 OpenAPI 3.0 subset — JSON Schema 의 일부 키를 거부함.\n // 'additionalProperties' / '$schema' / '$defs' 등을 제거.\n const functionDeclarations = tools.map(t => ({\n name: t.name,\n description: t.description,\n parameters: sanitizeForGeminiSchema(t.parameters)\n }))\n\n const toolConfig = buildGeminiToolConfig(options)\n\n const model = this.sdk.getGenerativeModel({\n model: options.model ?? this.defaultModel,\n ...(systemPrompt && { systemInstruction: systemPrompt }),\n generationConfig: baseGenerationConfig(options),\n tools: [{ functionDeclarations }] as any,\n ...(toolConfig && { toolConfig })\n } as any)\n\n const result = await model.generateContent({ contents } as any)\n const response: any = result.response\n\n const candidate = response?.candidates?.[0]\n const parts = candidate?.content?.parts ?? []\n\n // Gemini 는 한 응답에서 (a) thinking part (p.thought === true) 와\n // (b) 본문 text part 를 함께 emit 할 수 있다. thought 는 사용자에게 노출하면\n // \"구조화 답변 + 산문 답변\" 형태의 중복으로 보이므로 제거.\n const textParts = parts\n .filter((p: any) => typeof p?.text === 'string' && p?.thought !== true)\n .map((p: any) => p.text)\n const text = textParts.join('') || undefined\n\n // Gemini 는 functionCall 에 자체 id 가 없음 — 어댑터가 turn 내부에서 부여.\n // id 포맷 'g:<name>:<seq>' — assistant 가 tool_result 를 만들 때 이 id 를 그대로\n // 넘기면 toGeminiPart 가 name 을 추출해 functionResponse.name 에 채운다.\n //\n // thoughtSignature: Gemini 2.5/3 부터 thinking 모델이 functionCall 에 서명을 붙임.\n // 다음 turn 에 그대로 echo 해야 400 에러 안 남 (cite: ai.google.dev/.../thought-signatures).\n let geminiCallSeq = 0\n const ts = Date.now()\n const toolCalls: AIToolCall[] = parts\n .filter((p: any) => p?.functionCall && typeof p.functionCall.name === 'string')\n .map((p: any) => {\n // SDK 가 camelCase 또는 snake_case 둘 중 하나로 emit — 둘 다 받음.\n const sig: string | undefined = p.thoughtSignature ?? p.thought_signature\n return {\n id: `g:${p.functionCall.name}:${ts}_${geminiCallSeq++}`,\n name: p.functionCall.name,\n arguments:\n p.functionCall.args && typeof p.functionCall.args === 'object'\n ? (p.functionCall.args as Record<string, any>)\n : {},\n ...(typeof sig === 'string' && sig.length > 0 && {\n providerMeta: { thoughtSignature: sig }\n })\n }\n })\n\n return {\n text,\n toolCalls,\n stopReason: normalizeGeminiStopReason(candidate?.finishReason, toolCalls.length > 0)\n }\n }\n}\n\n// ── 변환 유틸 ─────────────────────────────────────────────────────────\n\nfunction baseGenerationConfig(options: AIChatOptions): any {\n return {\n maxOutputTokens: options.maxTokens ?? DEFAULT_MAX_TOKENS,\n ...(options.temperature !== undefined && { temperature: options.temperature }),\n ...(options.stopSequences && { stopSequences: options.stopSequences })\n }\n}\n\nfunction splitMessages(\n messages: AIMessage[],\n externalSystem?: string\n): { systemPrompt: string; contents: any[] } {\n const systemMessages = messages\n .filter(m => m.role === 'system')\n .map(extractTextContent)\n .filter(Boolean)\n const conversation = messages.filter(m => m.role !== 'system')\n const systemPrompt = [externalSystem, ...systemMessages].filter(Boolean).join('\\n\\n')\n return { systemPrompt, contents: conversation.map(toGeminiContent) }\n}\n\nfunction extractTextContent(msg: AIMessage): string {\n if (typeof msg.content === 'string') return msg.content\n return msg.content\n .filter(p => p.type === 'text')\n .map(p => (p as { type: 'text'; text: string }).text)\n .join('\\n')\n}\n\nfunction toGeminiContent(msg: AIMessage): any {\n // Our 'assistant' role → Gemini's 'model' role\n const role = msg.role === 'assistant' ? 'model' : msg.role\n const parts =\n typeof msg.content === 'string'\n ? [{ text: msg.content }]\n : msg.content.map(toGeminiPart)\n return { role, parts }\n}\n\nfunction toGeminiPart(part: AIContentPart): any {\n if (part.type === 'text') {\n return { text: part.text }\n }\n if (part.type === 'tool_use') {\n // Gemini: assistant 가 호출 의도 표현. functionCall.name + args 만 가짐 (id 없음).\n // thoughtSignature 는 thinking 모델 사용 시 필수 — providerMeta 에서 복원해 echo.\n const sig = part.providerMeta?.thoughtSignature\n return {\n functionCall: { name: part.name, args: part.arguments },\n ...(typeof sig === 'string' && sig.length > 0 && { thoughtSignature: sig })\n }\n }\n if (part.type === 'tool_result') {\n // Gemini: user role 에 functionResponse part 로 회신. 이름은 호출했던 tool name 과\n // 같아야 함 — assistant.ts 의 loop 에서 part 생성 시 함께 전달.\n let parsed: any = part.content\n try {\n parsed = JSON.parse(part.content)\n } catch {\n parsed = { result: part.content }\n }\n return {\n functionResponse: {\n // name 은 원래 호출했던 tool name. toolUseId 에 임베드되어 있지 않으므로\n // assistant 가 메시지 생성 시 별도 채워줘야 함 — 여기서는 placeholder.\n // 실제 사용 시 toolUseId 를 'tool_<name>_<seq>' 형태로 만들어 name 추출.\n name: extractGeminiNameFromId(part.toolUseId),\n response: parsed\n }\n }\n }\n // image\n if (part.source.kind === 'url') {\n // Gemini SDK 는 inlineData(base64) 만 지원. URL 은 호출 전에 fetch 필요.\n throw new Error(\n 'Gemini adapter: image URL is not supported directly. Fetch the URL and pass as Buffer.'\n )\n }\n const data = Buffer.isBuffer(part.source.data)\n ? part.source.data.toString('base64')\n : Buffer.from(part.source.data).toString('base64')\n return {\n inlineData: { data, mimeType: part.source.mediaType }\n }\n}\n\n/**\n * Gemini 의 functionCall 에는 자체 id 가 없어서 chatWithTools 가 'gemini_<ts>_<seq>'\n * 형태의 가짜 id 를 만든다. 그런데 functionResponse 에는 name 이 필수 — assistant 의\n * tool 실행기는 이 id 와 name 을 별도 매핑해야 한다. assistant 가 tool_result part 를\n * 만들 때 toolUseId 에 'name:<actualName>;id:<originalId>' 같은 인코딩을 할 수 있지만\n * 그건 invasive — 대신 toolUseId 가 'g:<name>:<id>' 형태면 name 을 직접 추출.\n *\n * 현재는 fallback: id 가 'g:' 로 시작하면 두 번째 segment 를 name 으로,\n * 아니면 그대로 사용 (Gemini 가 받지 않으면 에러 — assistant 가 책임지고 인코딩).\n */\nfunction extractGeminiNameFromId(id: string): string {\n if (id.startsWith('g:')) {\n const parts = id.split(':')\n return parts[1] || id\n }\n return id\n}\n\n/**\n * JSON Schema → Gemini OpenAPI 3.0 subset 변환.\n * Gemini API 가 거부하는 키들 (deep) 제거.\n * 보존: type / properties / items / required / enum / description / format / nullable\n */\nconst GEMINI_REJECTED_KEYS = new Set([\n 'additionalProperties',\n '$schema',\n '$defs',\n '$ref',\n '$id',\n 'definitions',\n 'patternProperties',\n 'dependencies'\n])\n\nfunction sanitizeForGeminiSchema(schema: any): any {\n if (schema === null || schema === undefined) return schema\n if (Array.isArray(schema)) return schema.map(sanitizeForGeminiSchema)\n if (typeof schema !== 'object') return schema\n const out: any = {}\n for (const [k, v] of Object.entries(schema)) {\n if (GEMINI_REJECTED_KEYS.has(k)) continue\n out[k] = sanitizeForGeminiSchema(v)\n }\n return out\n}\n\nfunction buildGeminiToolConfig(options: AIToolChatOptions): any | undefined {\n const choice = options.toolChoice ?? 'auto'\n if (choice === 'none') {\n return { functionCallingConfig: { mode: 'NONE' } }\n }\n if (choice === 'required') {\n return {\n functionCallingConfig: {\n mode: 'ANY',\n ...(options.forceTool && { allowedFunctionNames: [options.forceTool] })\n }\n }\n }\n return { functionCallingConfig: { mode: 'AUTO' } }\n}\n\nfunction normalizeGeminiStopReason(\n raw: any,\n hasToolCalls: boolean\n): AIToolChatResult['stopReason'] {\n // Gemini 는 function call 발생 시 finishReason 을 'STOP' 으로 마킹할 수 있어\n // toolCalls 존재 여부로 우선 판정.\n if (hasToolCalls) return 'tool_use'\n switch (raw) {\n case 'STOP':\n return 'end_turn'\n case 'MAX_TOKENS':\n return 'max_tokens'\n case 'SAFETY':\n case 'RECITATION':\n case 'OTHER':\n return 'stop_sequence'\n default:\n return raw ?? 'end_turn'\n }\n}\n\n// Auto-register at module load\nregisterAIProvider('gemini', config => new GeminiAIClient(config))\n"]}
@@ -0,0 +1,6 @@
1
+ import './anthropic.js';
2
+ import './gemini.js';
3
+ import './openai.js';
4
+ export { AnthropicAIClient } from './anthropic.js';
5
+ export { GeminiAIClient } from './gemini.js';
6
+ export { OpenAIAIClient } from './openai.js';
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OpenAIAIClient = exports.GeminiAIClient = exports.AnthropicAIClient = void 0;
4
+ // Auto-register providers on module load
5
+ require("./anthropic.js");
6
+ require("./gemini.js");
7
+ require("./openai.js");
8
+ var anthropic_js_1 = require("./anthropic.js");
9
+ Object.defineProperty(exports, "AnthropicAIClient", { enumerable: true, get: function () { return anthropic_js_1.AnthropicAIClient; } });
10
+ var gemini_js_1 = require("./gemini.js");
11
+ Object.defineProperty(exports, "GeminiAIClient", { enumerable: true, get: function () { return gemini_js_1.GeminiAIClient; } });
12
+ var openai_js_1 = require("./openai.js");
13
+ Object.defineProperty(exports, "OpenAIAIClient", { enumerable: true, get: function () { return openai_js_1.OpenAIAIClient; } });
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/service/providers/index.ts"],"names":[],"mappings":";;;AAAA,yCAAyC;AACzC,0BAAuB;AACvB,uBAAoB;AACpB,uBAAoB;AAEpB,+CAAkD;AAAzC,iHAAA,iBAAiB,OAAA;AAC1B,yCAA4C;AAAnC,2GAAA,cAAc,OAAA;AACvB,yCAA4C;AAAnC,2GAAA,cAAc,OAAA","sourcesContent":["// Auto-register providers on module load\nimport './anthropic.js'\nimport './gemini.js'\nimport './openai.js'\n\nexport { AnthropicAIClient } from './anthropic.js'\nexport { GeminiAIClient } from './gemini.js'\nexport { OpenAIAIClient } from './openai.js'\n"]}
@@ -0,0 +1,13 @@
1
+ import type { AIChatOptions, AIClient, AIClientConfig, AIJSONOptions, AIMessage, AITool, AIToolChatOptions, AIToolChatResult, AIVisionOptions } from '../types.js';
2
+ export declare class OpenAIAIClient implements AIClient {
3
+ readonly id: string;
4
+ private sdk;
5
+ private defaultModel;
6
+ constructor(config: AIClientConfig);
7
+ chat(messages: AIMessage[], options?: AIChatOptions): Promise<string>;
8
+ generateJSON<T = unknown>(prompt: string | AIMessage[], options?: AIJSONOptions): Promise<T>;
9
+ vision(image: Buffer | Uint8Array | {
10
+ url: string;
11
+ }, prompt: string, options?: AIVisionOptions): Promise<string>;
12
+ chatWithTools(messages: AIMessage[], tools: AITool[], options?: AIToolChatOptions): Promise<AIToolChatResult>;
13
+ }
@@ -0,0 +1,232 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OpenAIAIClient = void 0;
4
+ const tslib_1 = require("tslib");
5
+ /**
6
+ * OpenAI AI Client adapter.
7
+ *
8
+ * 기본 모델: gpt-4o (구성에서 override 가능).
9
+ * SDK: openai
10
+ *
11
+ * 비고: legacy `openai` config 키 (codelingua 의 사용처) 도 factory.ts 가 이 어댑터로 자동 매핑.
12
+ * organization 필드 지원.
13
+ */
14
+ const openai_1 = tslib_1.__importDefault(require("openai"));
15
+ const factory_js_1 = require("../factory.js");
16
+ const parse_json_js_1 = require("../utils/parse-json.js");
17
+ const DEFAULT_MODEL = 'gpt-4o';
18
+ const DEFAULT_MAX_TOKENS = 4096;
19
+ class OpenAIAIClient {
20
+ constructor(config) {
21
+ // OpenAI SDK 는 apiKey 없을 시 OPENAI_API_KEY 환경변수 자동 인식.
22
+ const apiKey = config.apiKey ?? process.env.OPENAI_API_KEY;
23
+ this.sdk = new openai_1.default({
24
+ apiKey,
25
+ ...(config.organization && { organization: config.organization }),
26
+ ...(config.baseURL && { baseURL: config.baseURL })
27
+ });
28
+ this.defaultModel = config.model ?? DEFAULT_MODEL;
29
+ this.id = `openai:${this.defaultModel}`;
30
+ }
31
+ async chat(messages, options = {}) {
32
+ const sdkMessages = buildSDKMessages(messages, options.systemPrompt);
33
+ const response = await this.sdk.chat.completions.create({
34
+ model: options.model ?? this.defaultModel,
35
+ max_tokens: options.maxTokens ?? DEFAULT_MAX_TOKENS,
36
+ ...(options.temperature !== undefined && { temperature: options.temperature }),
37
+ ...(options.stopSequences && { stop: options.stopSequences }),
38
+ messages: sdkMessages
39
+ });
40
+ return response.choices[0]?.message?.content ?? '';
41
+ }
42
+ async generateJSON(prompt, options = {}) {
43
+ const messages = typeof prompt === 'string' ? [{ role: 'user', content: prompt }] : prompt;
44
+ const schemaHint = options.schema
45
+ ? `\n\nResponse MUST conform to this JSON schema:\n${JSON.stringify(options.schema)}`
46
+ : '';
47
+ const enforcedSystem = (options.systemPrompt ? options.systemPrompt + '\n\n' : '') +
48
+ 'Return only valid JSON. No markdown code fences, no commentary.' +
49
+ schemaHint;
50
+ const sdkMessages = buildSDKMessages(messages, enforcedSystem);
51
+ const response = await this.sdk.chat.completions.create({
52
+ model: options.model ?? this.defaultModel,
53
+ max_tokens: options.maxTokens ?? DEFAULT_MAX_TOKENS,
54
+ ...(options.temperature !== undefined && { temperature: options.temperature }),
55
+ response_format: { type: 'json_object' },
56
+ messages: sdkMessages
57
+ });
58
+ const text = response.choices[0]?.message?.content ?? '{}';
59
+ return (0, parse_json_js_1.parseJSONResponse)(text);
60
+ }
61
+ async vision(image, prompt, options = {}) {
62
+ const imagePart = typeof image === 'object' && 'url' in image
63
+ ? { type: 'image', source: { kind: 'url', url: image.url } }
64
+ : {
65
+ type: 'image',
66
+ source: { kind: 'base64', data: image, mediaType: 'image/png' }
67
+ };
68
+ return this.chat([{ role: 'user', content: [imagePart, { type: 'text', text: prompt }] }], options);
69
+ }
70
+ async chatWithTools(messages, tools, options = {}) {
71
+ const sdkMessages = buildSDKMessages(messages, options.systemPrompt);
72
+ const sdkTools = tools.map(t => ({
73
+ type: 'function',
74
+ function: {
75
+ name: t.name,
76
+ description: t.description,
77
+ parameters: t.parameters
78
+ }
79
+ }));
80
+ const toolChoice = buildOpenAIToolChoice(options);
81
+ const response = await this.sdk.chat.completions.create({
82
+ model: options.model ?? this.defaultModel,
83
+ max_tokens: options.maxTokens ?? DEFAULT_MAX_TOKENS,
84
+ ...(options.temperature !== undefined && { temperature: options.temperature }),
85
+ ...(options.stopSequences && { stop: options.stopSequences }),
86
+ messages: sdkMessages,
87
+ tools: sdkTools,
88
+ ...(toolChoice !== undefined && { tool_choice: toolChoice }),
89
+ ...(options.allowParallelToolCalls === false && { parallel_tool_calls: false })
90
+ });
91
+ const choice = response.choices?.[0];
92
+ const message = choice?.message;
93
+ const text = message?.content || undefined;
94
+ const toolCalls = (message?.tool_calls || []).map((tc) => {
95
+ const fn = tc.function;
96
+ let args = {};
97
+ try {
98
+ args = fn?.arguments ? JSON.parse(fn.arguments) : {};
99
+ }
100
+ catch {
101
+ args = {};
102
+ }
103
+ return {
104
+ id: typeof tc.id === 'string' && tc.id ? tc.id : `call_${Math.random().toString(36).slice(2, 12)}`,
105
+ name: fn?.name ?? '',
106
+ arguments: args
107
+ };
108
+ });
109
+ return {
110
+ text,
111
+ toolCalls,
112
+ stopReason: normalizeOpenAIStopReason(choice?.finish_reason)
113
+ };
114
+ }
115
+ }
116
+ exports.OpenAIAIClient = OpenAIAIClient;
117
+ // ── 변환 유틸 ─────────────────────────────────────────────────────────
118
+ function buildSDKMessages(messages, externalSystem) {
119
+ const out = [];
120
+ const systemFromMessages = messages
121
+ .filter(m => m.role === 'system')
122
+ .map(extractTextContent)
123
+ .filter(Boolean)
124
+ .join('\n\n');
125
+ const combinedSystem = [externalSystem, systemFromMessages].filter(Boolean).join('\n\n');
126
+ if (combinedSystem) {
127
+ out.push({ role: 'system', content: combinedSystem });
128
+ }
129
+ for (const msg of messages) {
130
+ if (msg.role === 'system')
131
+ continue;
132
+ const sdkMsgs = toSDKMessages(msg);
133
+ out.push(...sdkMsgs);
134
+ }
135
+ return out;
136
+ }
137
+ /**
138
+ * 한 AIMessage 가 다중 tool_result 를 가지면 OpenAI 측에서 여러 role:'tool' 메시지로
139
+ * 분리해야 함 (OpenAI 는 메시지당 tool_call_id 하나만 매핑). 따라서 array 를 반환.
140
+ */
141
+ function toSDKMessages(msg) {
142
+ if (typeof msg.content === 'string') {
143
+ return [{ role: msg.role, content: msg.content }];
144
+ }
145
+ const parts = msg.content;
146
+ const toolUseParts = parts.filter(p => p.type === 'tool_use');
147
+ const toolResultParts = parts.filter(p => p.type === 'tool_result');
148
+ const otherParts = parts.filter(p => p.type !== 'tool_use' && p.type !== 'tool_result');
149
+ // tool_result 가 있으면 각각을 role='tool' 메시지로 분리 (OpenAI 요구사항).
150
+ if (toolResultParts.length > 0) {
151
+ return toolResultParts.map((r) => ({
152
+ role: 'tool',
153
+ tool_call_id: r.toolUseId,
154
+ content: r.content
155
+ }));
156
+ }
157
+ // assistant 측 tool_use 가 있으면 message.tool_calls 로
158
+ const out = { role: msg.role };
159
+ if (otherParts.length > 0) {
160
+ out.content = otherParts.map(toSDKContentPart);
161
+ }
162
+ else {
163
+ out.content = null;
164
+ }
165
+ if (toolUseParts.length > 0) {
166
+ out.tool_calls = toolUseParts.map(p => ({
167
+ id: p.id,
168
+ type: 'function',
169
+ function: { name: p.name, arguments: JSON.stringify(p.arguments) }
170
+ }));
171
+ }
172
+ return [out];
173
+ }
174
+ function toSDKContentPart(part) {
175
+ if (part.type === 'text') {
176
+ return { type: 'text', text: part.text };
177
+ }
178
+ if (part.type === 'tool_use' || part.type === 'tool_result') {
179
+ // 처리는 toSDKMessage 가 함 — 도달하면 버그.
180
+ return { type: 'text', text: '' };
181
+ }
182
+ // image
183
+ if (part.source.kind === 'url') {
184
+ return { type: 'image_url', image_url: { url: part.source.url } };
185
+ }
186
+ const b64 = Buffer.isBuffer(part.source.data)
187
+ ? part.source.data.toString('base64')
188
+ : Buffer.from(part.source.data).toString('base64');
189
+ return {
190
+ type: 'image_url',
191
+ image_url: { url: `data:${part.source.mediaType};base64,${b64}` }
192
+ };
193
+ }
194
+ function extractTextContent(msg) {
195
+ if (typeof msg.content === 'string')
196
+ return msg.content;
197
+ return msg.content
198
+ .filter(p => p.type === 'text')
199
+ .map(p => p.text)
200
+ .join('\n');
201
+ }
202
+ function buildOpenAIToolChoice(options) {
203
+ const choice = options.toolChoice ?? 'auto';
204
+ if (choice === 'none')
205
+ return 'none';
206
+ if (choice === 'required') {
207
+ if (options.forceTool) {
208
+ return { type: 'function', function: { name: options.forceTool } };
209
+ }
210
+ return 'required';
211
+ }
212
+ return 'auto';
213
+ }
214
+ function normalizeOpenAIStopReason(raw) {
215
+ switch (raw) {
216
+ case 'tool_calls':
217
+ return 'tool_use';
218
+ case 'function_call':
219
+ return 'tool_use';
220
+ case 'stop':
221
+ return 'end_turn';
222
+ case 'length':
223
+ return 'max_tokens';
224
+ case 'content_filter':
225
+ return 'stop_sequence';
226
+ default:
227
+ return raw ?? 'end_turn';
228
+ }
229
+ }
230
+ // Auto-register at module load
231
+ (0, factory_js_1.registerAIProvider)('openai', config => new OpenAIAIClient(config));
232
+ //# sourceMappingURL=openai.js.map