@things-factory/ai-client-base 10.1.149 → 10.1.157
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/config/config.development.js +2 -68
- package/config/config.production.js +2 -20
- package/dist-server/service/agent/agent-directory.d.ts +71 -0
- package/dist-server/service/agent/agent-directory.js +129 -0
- package/dist-server/service/agent/agent-directory.js.map +1 -0
- package/dist-server/service/agent/agent-settings-save.d.ts +38 -0
- package/dist-server/service/agent/agent-settings-save.js +153 -0
- package/dist-server/service/agent/agent-settings-save.js.map +1 -0
- package/dist-server/service/agent/agent-spec.d.ts +43 -0
- package/dist-server/service/agent/agent-spec.js +83 -0
- package/dist-server/service/agent/agent-spec.js.map +1 -0
- package/dist-server/service/agent/ai-client-for.d.ts +53 -0
- package/dist-server/service/agent/ai-client-for.js +166 -0
- package/dist-server/service/agent/ai-client-for.js.map +1 -0
- package/dist-server/service/agent/ai-usage-summary.d.ts +37 -0
- package/dist-server/service/agent/ai-usage-summary.js +108 -0
- package/dist-server/service/agent/ai-usage-summary.js.map +1 -0
- package/dist-server/service/agent/ai-usage.d.ts +35 -0
- package/dist-server/service/agent/ai-usage.js +118 -0
- package/dist-server/service/agent/ai-usage.js.map +1 -0
- package/dist-server/service/agent/index.d.ts +7 -0
- package/dist-server/service/agent/index.js +13 -0
- package/dist-server/service/agent/index.js.map +1 -0
- package/dist-server/service/agent/provider-models.d.ts +12 -0
- package/dist-server/service/agent/provider-models.js +37 -0
- package/dist-server/service/agent/provider-models.js.map +1 -0
- package/dist-server/service/capability.d.ts +21 -10
- package/dist-server/service/capability.js +20 -49
- package/dist-server/service/capability.js.map +1 -1
- package/dist-server/service/factory.d.ts +7 -12
- package/dist-server/service/factory.js +0 -45
- package/dist-server/service/factory.js.map +1 -1
- package/dist-server/service/index.d.ts +3 -0
- package/dist-server/service/index.js +6 -1
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/providers/anthropic.d.ts +2 -0
- package/dist-server/service/providers/anthropic.js +16 -0
- package/dist-server/service/providers/anthropic.js.map +1 -1
- package/dist-server/service/providers/gemini.d.ts +13 -0
- package/dist-server/service/providers/gemini.js +52 -12
- package/dist-server/service/providers/gemini.js.map +1 -1
- package/dist-server/service/providers/openai.d.ts +6 -0
- package/dist-server/service/providers/openai.js +34 -8
- package/dist-server/service/providers/openai.js.map +1 -1
- package/dist-server/service/types.d.ts +16 -2
- package/dist-server/service/types.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -2
- package/server/service/agent/agent-directory.test.ts +206 -0
- package/server/service/agent/agent-directory.ts +198 -0
- package/server/service/agent/agent-settings-save.test.ts +151 -0
- package/server/service/agent/agent-settings-save.ts +205 -0
- package/server/service/agent/agent-spec.ts +103 -0
- package/server/service/agent/ai-client-for.test.ts +141 -0
- package/server/service/agent/ai-client-for.ts +201 -0
- package/server/service/agent/ai-usage-summary.test.ts +65 -0
- package/server/service/agent/ai-usage-summary.ts +146 -0
- package/server/service/agent/ai-usage.ts +97 -0
- package/server/service/agent/index.ts +7 -0
- package/server/service/agent/one-door.test.ts +57 -0
- package/server/service/agent/provider-models.ts +43 -0
- package/server/service/capability.test.ts +26 -55
- package/server/service/capability.ts +25 -42
- package/server/service/factory.ts +4 -44
- package/server/service/index.ts +6 -1
- package/server/service/providers/anthropic.ts +17 -0
- package/server/service/providers/gemini.ts +54 -12
- package/server/service/providers/openai-models.test.ts +30 -0
- package/server/service/providers/openai.ts +34 -8
- package/server/service/types.ts +18 -2
- package/translations/en.json +13 -0
- package/translations/es.json +13 -0
- package/translations/ja.json +13 -0
- package/translations/ko.json +13 -0
- package/translations/ms.json +13 -0
- package/translations/zh.json +13 -0
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Provider registration and client construction.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Each adapter registers itself when its module loads. `createAIClient` builds a client from a provider, model and
|
|
5
|
+
* key; it is how this package's own door (aiClientFor) and the model list reach a provider. Code outside this package
|
|
6
|
+
* gets a client from aiClientFor, for a domain - there is no deployment-wide client.
|
|
7
7
|
*/
|
|
8
|
-
import { config as envConfig } from '@things-factory/env'
|
|
9
8
|
import type { AIClient, AIClientConfig, AIProviderFactory } from './types.js'
|
|
10
9
|
|
|
11
10
|
const REGISTERED_PROVIDERS = new Map<string, AIProviderFactory>()
|
|
@@ -27,42 +26,3 @@ export function createAIClient(config: AIClientConfig): AIClient {
|
|
|
27
26
|
}
|
|
28
27
|
return factory(config)
|
|
29
28
|
}
|
|
30
|
-
|
|
31
|
-
let _defaultClient: AIClient | undefined
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* 기본 client.
|
|
35
|
-
* 1) `setDefaultAIClient()` 로 명시 설정된 게 있으면 그것
|
|
36
|
-
* 2) env config 의 `aiClient` 키가 있으면 거기서 생성
|
|
37
|
-
* 3) 없으면 undefined (호출자는 nullable 처리)
|
|
38
|
-
*
|
|
39
|
-
* 다른 모듈 (예: codelingua) 의 `openai` 키와는 의도적으로 분리. 두 키는 서로 모르고 동작.
|
|
40
|
-
*
|
|
41
|
-
* 잘못된 config (provider 누락, 등록되지 않은 provider) 는 console.warn 후 undefined.
|
|
42
|
-
*/
|
|
43
|
-
export function getDefaultAIClient(): AIClient | undefined {
|
|
44
|
-
if (_defaultClient) return _defaultClient
|
|
45
|
-
const cfg = envConfig.get('aiClient', null) as AIClientConfig | null
|
|
46
|
-
if (!cfg) return undefined
|
|
47
|
-
if (!cfg.provider) {
|
|
48
|
-
console.warn(
|
|
49
|
-
'[ai-client-base] config.aiClient is set but missing required "provider" field. ' +
|
|
50
|
-
'See @things-factory/ai-client-base/config/config.development.js for examples.'
|
|
51
|
-
)
|
|
52
|
-
return undefined
|
|
53
|
-
}
|
|
54
|
-
try {
|
|
55
|
-
_defaultClient = createAIClient(cfg)
|
|
56
|
-
} catch (e) {
|
|
57
|
-
console.warn(
|
|
58
|
-
`[ai-client-base] Failed to create default AI client: ${(e as Error).message}. ` +
|
|
59
|
-
`Registered providers: ${listRegisteredProviders().join(', ') || '(none)'}`
|
|
60
|
-
)
|
|
61
|
-
return undefined
|
|
62
|
-
}
|
|
63
|
-
return _defaultClient
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function setDefaultAIClient(client: AIClient | undefined): void {
|
|
67
|
-
_defaultClient = client
|
|
68
|
-
}
|
package/server/service/index.ts
CHANGED
|
@@ -7,6 +7,11 @@ export * from './agentic-loop/index.js'
|
|
|
7
7
|
export * from './assistant/index.js'
|
|
8
8
|
export * from './analysis-contract.js'
|
|
9
9
|
export * from './decision/index.js'
|
|
10
|
-
/* 「이
|
|
10
|
+
/* 「이 사업장에 AI 가 있나」 — 시도해서 알아내지 않고 먼저 읽는다(표준 A7-a). */
|
|
11
11
|
export * from './capability.js'
|
|
12
|
+
/* AI agents per domain - the one door to a model, and its usage (design/ai/agent-settings.md). */
|
|
13
|
+
export * from './agent/index.js'
|
|
14
|
+
|
|
15
|
+
import { AiUsage } from './agent/ai-usage.js'
|
|
16
|
+
export const entities = [AiUsage]
|
|
12
17
|
export * from './testing/tool-call-context.js'
|
|
@@ -58,6 +58,7 @@ export class AnthropicAIClient implements AIClient {
|
|
|
58
58
|
...(options.stopSequences && { stop_sequences: options.stopSequences }),
|
|
59
59
|
messages: conversation
|
|
60
60
|
} as any)
|
|
61
|
+
reportUsage(response, options, options.model ?? this.defaultModel)
|
|
61
62
|
|
|
62
63
|
return extractResponseText(response)
|
|
63
64
|
}
|
|
@@ -131,6 +132,7 @@ export class AnthropicAIClient implements AIClient {
|
|
|
131
132
|
tools: sdkTools,
|
|
132
133
|
...(toolChoice && { tool_choice: toolChoice })
|
|
133
134
|
} as any)
|
|
135
|
+
reportUsage(response, options, options.model ?? this.defaultModel)
|
|
134
136
|
|
|
135
137
|
const text = extractResponseText(response) || undefined
|
|
136
138
|
const toolCalls: AIToolCall[] = Array.isArray(response.content)
|
|
@@ -149,6 +151,21 @@ export class AnthropicAIClient implements AIClient {
|
|
|
149
151
|
stopReason: normalizeAnthropicStopReason(response.stop_reason)
|
|
150
152
|
}
|
|
151
153
|
}
|
|
154
|
+
|
|
155
|
+
/** Every model Anthropic lists is a conversation model. */
|
|
156
|
+
async listModels(): Promise<string[]> {
|
|
157
|
+
const ids: string[] = []
|
|
158
|
+
for await (const model of this.sdk.models.list()) ids.push(model.id)
|
|
159
|
+
return ids.sort()
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function reportUsage(response: any, options: AIChatOptions, requested: string): void {
|
|
164
|
+
options.onUsage?.({
|
|
165
|
+
model: response?.model ?? requested,
|
|
166
|
+
inputTokens: response?.usage?.input_tokens,
|
|
167
|
+
outputTokens: response?.usage?.output_tokens
|
|
168
|
+
})
|
|
152
169
|
}
|
|
153
170
|
|
|
154
171
|
// ── 변환 유틸 ─────────────────────────────────────────────────────────
|
|
@@ -34,17 +34,13 @@ export class GeminiAIClient implements AIClient {
|
|
|
34
34
|
readonly id: string
|
|
35
35
|
private sdk: GoogleGenerativeAI
|
|
36
36
|
private defaultModel: string
|
|
37
|
+
private apiKey: string
|
|
38
|
+
private baseURL?: string
|
|
37
39
|
|
|
38
40
|
constructor(config: AIClientConfig) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
config.apiKey ??
|
|
43
|
-
process.env.GOOGLE_API_KEY ??
|
|
44
|
-
process.env.GEMINI_API_KEY ??
|
|
45
|
-
process.env.GOOGLE_GENERATIVE_AI_API_KEY ??
|
|
46
|
-
''
|
|
47
|
-
this.sdk = new GoogleGenerativeAI(apiKey)
|
|
41
|
+
this.apiKey = config.apiKey
|
|
42
|
+
this.baseURL = config.baseURL
|
|
43
|
+
this.sdk = new GoogleGenerativeAI(config.apiKey)
|
|
48
44
|
this.defaultModel = config.model ?? DEFAULT_MODEL
|
|
49
45
|
this.id = `gemini:${this.defaultModel}`
|
|
50
46
|
}
|
|
@@ -53,6 +49,7 @@ export class GeminiAIClient implements AIClient {
|
|
|
53
49
|
const { systemPrompt, contents } = splitMessages(messages, options.systemPrompt)
|
|
54
50
|
const model = this.buildModel(options, systemPrompt)
|
|
55
51
|
const result = await model.generateContent({ contents } as any)
|
|
52
|
+
reportUsage(result, options, options.model ?? this.defaultModel)
|
|
56
53
|
return result.response.text()
|
|
57
54
|
}
|
|
58
55
|
|
|
@@ -72,8 +69,9 @@ export class GeminiAIClient implements AIClient {
|
|
|
72
69
|
model: options.model ?? this.defaultModel,
|
|
73
70
|
...(systemPrompt && { systemInstruction: systemPrompt }),
|
|
74
71
|
generationConfig
|
|
75
|
-
})
|
|
72
|
+
}, this.requestOptions)
|
|
76
73
|
const result = await model.generateContent({ contents } as any)
|
|
74
|
+
reportUsage(result, options, options.model ?? this.defaultModel)
|
|
77
75
|
const text = result.response.text()
|
|
78
76
|
return parseJSONResponse<T>(text)
|
|
79
77
|
}
|
|
@@ -102,7 +100,7 @@ export class GeminiAIClient implements AIClient {
|
|
|
102
100
|
model: options.model ?? this.defaultModel,
|
|
103
101
|
...(systemInstruction && { systemInstruction }),
|
|
104
102
|
generationConfig: baseGenerationConfig(options)
|
|
105
|
-
})
|
|
103
|
+
}, this.requestOptions)
|
|
106
104
|
}
|
|
107
105
|
|
|
108
106
|
async chatWithTools(
|
|
@@ -128,9 +126,10 @@ export class GeminiAIClient implements AIClient {
|
|
|
128
126
|
generationConfig: baseGenerationConfig(options),
|
|
129
127
|
tools: [{ functionDeclarations }] as any,
|
|
130
128
|
...(toolConfig && { toolConfig })
|
|
131
|
-
} as any)
|
|
129
|
+
} as any, this.requestOptions)
|
|
132
130
|
|
|
133
131
|
const result = await model.generateContent({ contents } as any)
|
|
132
|
+
reportUsage(result, options, options.model ?? this.defaultModel)
|
|
134
133
|
const response: any = result.response
|
|
135
134
|
|
|
136
135
|
const candidate = response?.candidates?.[0]
|
|
@@ -176,6 +175,49 @@ export class GeminiAIClient implements AIClient {
|
|
|
176
175
|
stopReason: normalizeGeminiStopReason(candidate?.finishReason, toolCalls.length > 0)
|
|
177
176
|
}
|
|
178
177
|
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The SDK has no model list, so this reads the REST one. A conversation model is one that answers
|
|
181
|
+
* generateContent; embedding and other models are left out.
|
|
182
|
+
*/
|
|
183
|
+
async listModels(): Promise<string[]> {
|
|
184
|
+
const base = (this.baseURL || 'https://generativelanguage.googleapis.com').replace(/\/+$/, '')
|
|
185
|
+
const ids: string[] = []
|
|
186
|
+
let pageToken = ''
|
|
187
|
+
do {
|
|
188
|
+
const url = `${base}/v1beta/models?pageSize=1000${pageToken ? `&pageToken=${encodeURIComponent(pageToken)}` : ''}`
|
|
189
|
+
const response = await fetch(url, { headers: { 'x-goog-api-key': this.apiKey } })
|
|
190
|
+
if (!response.ok) throw new ProviderListError(response.status, await response.text())
|
|
191
|
+
const page: any = await response.json()
|
|
192
|
+
for (const model of page.models ?? []) {
|
|
193
|
+
if ((model.supportedGenerationMethods ?? []).includes('generateContent')) {
|
|
194
|
+
ids.push(String(model.name).replace(/^models\//, ''))
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
pageToken = page.nextPageToken ?? ''
|
|
198
|
+
} while (pageToken)
|
|
199
|
+
return ids.sort()
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private get requestOptions() {
|
|
203
|
+
return this.baseURL ? { baseUrl: this.baseURL } : undefined
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** A model list the provider refused, with the HTTP status the caller turns into a reason. */
|
|
208
|
+
export class ProviderListError extends Error {
|
|
209
|
+
constructor(readonly status: number, body: string) {
|
|
210
|
+
super(`model list failed with ${status}: ${body.slice(0, 200)}`)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function reportUsage(result: any, options: AIChatOptions, requested: string): void {
|
|
215
|
+
const usage = result?.response?.usageMetadata
|
|
216
|
+
options.onUsage?.({
|
|
217
|
+
model: result?.response?.modelVersion ?? requested,
|
|
218
|
+
inputTokens: usage?.promptTokenCount,
|
|
219
|
+
outputTokens: usage?.candidatesTokenCount
|
|
220
|
+
})
|
|
179
221
|
}
|
|
180
222
|
|
|
181
223
|
// ── 변환 유틸 ─────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* What this guards: OpenAI's model list has no capability field, so the agent's model picker keeps the conversation
|
|
3
|
+
* models by name. The rule lives in one place and this fixes what it keeps.
|
|
4
|
+
*/
|
|
5
|
+
import { isOpenAIChatModel } from './openai'
|
|
6
|
+
|
|
7
|
+
it('★ keeps the GPT and o-series conversation models', () => {
|
|
8
|
+
for (const id of ['gpt-4o', 'gpt-4o-mini', 'gpt-4.1', 'gpt-5', 'chatgpt-4o-latest', 'o1', 'o3-mini', 'o4-mini']) {
|
|
9
|
+
expect([id, isOpenAIChatModel(id)]).toEqual([id, true])
|
|
10
|
+
}
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('★ leaves out embedding, speech, image, realtime, search and instruct models', () => {
|
|
14
|
+
for (const id of [
|
|
15
|
+
'text-embedding-3-large',
|
|
16
|
+
'tts-1',
|
|
17
|
+
'whisper-1',
|
|
18
|
+
'dall-e-3',
|
|
19
|
+
'gpt-image-1',
|
|
20
|
+
'gpt-4o-audio-preview',
|
|
21
|
+
'gpt-4o-realtime-preview',
|
|
22
|
+
'gpt-4o-transcribe',
|
|
23
|
+
'gpt-4o-search-preview',
|
|
24
|
+
'gpt-3.5-turbo-instruct',
|
|
25
|
+
'omni-moderation-latest',
|
|
26
|
+
'davinci-002'
|
|
27
|
+
]) {
|
|
28
|
+
expect([id, isOpenAIChatModel(id)]).toEqual([id, false])
|
|
29
|
+
}
|
|
30
|
+
})
|
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
* 기본 모델: gpt-4o (구성에서 override 가능).
|
|
5
5
|
* SDK: openai
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* organization 필드 지원.
|
|
7
|
+
* organization 필드 지원.
|
|
9
8
|
*/
|
|
10
9
|
import OpenAI from 'openai'
|
|
11
10
|
import type {
|
|
@@ -33,10 +32,8 @@ export class OpenAIAIClient implements AIClient {
|
|
|
33
32
|
private defaultModel: string
|
|
34
33
|
|
|
35
34
|
constructor(config: AIClientConfig) {
|
|
36
|
-
// OpenAI SDK 는 apiKey 없을 시 OPENAI_API_KEY 환경변수 자동 인식.
|
|
37
|
-
const apiKey = config.apiKey ?? process.env.OPENAI_API_KEY
|
|
38
35
|
this.sdk = new OpenAI({
|
|
39
|
-
apiKey,
|
|
36
|
+
apiKey: config.apiKey,
|
|
40
37
|
...(config.organization && { organization: config.organization }),
|
|
41
38
|
...(config.baseURL && { baseURL: config.baseURL })
|
|
42
39
|
})
|
|
@@ -48,11 +45,13 @@ export class OpenAIAIClient implements AIClient {
|
|
|
48
45
|
const sdkMessages = buildSDKMessages(messages, options.systemPrompt)
|
|
49
46
|
const response = await this.sdk.chat.completions.create({
|
|
50
47
|
model: options.model ?? this.defaultModel,
|
|
51
|
-
max_tokens
|
|
48
|
+
/* `max_tokens` is refused by the gpt-5 and o-series models; every chat model takes this one. */
|
|
49
|
+
max_completion_tokens: options.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
52
50
|
...(options.temperature !== undefined && { temperature: options.temperature }),
|
|
53
51
|
...(options.stopSequences && { stop: options.stopSequences }),
|
|
54
52
|
messages: sdkMessages as any
|
|
55
53
|
})
|
|
54
|
+
reportUsage(response, options, options.model ?? this.defaultModel)
|
|
56
55
|
return response.choices[0]?.message?.content ?? ''
|
|
57
56
|
}
|
|
58
57
|
|
|
@@ -74,11 +73,13 @@ export class OpenAIAIClient implements AIClient {
|
|
|
74
73
|
const sdkMessages = buildSDKMessages(messages, enforcedSystem)
|
|
75
74
|
const response = await this.sdk.chat.completions.create({
|
|
76
75
|
model: options.model ?? this.defaultModel,
|
|
77
|
-
max_tokens
|
|
76
|
+
/* `max_tokens` is refused by the gpt-5 and o-series models; every chat model takes this one. */
|
|
77
|
+
max_completion_tokens: options.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
78
78
|
...(options.temperature !== undefined && { temperature: options.temperature }),
|
|
79
79
|
response_format: { type: 'json_object' },
|
|
80
80
|
messages: sdkMessages as any
|
|
81
81
|
})
|
|
82
|
+
reportUsage(response, options, options.model ?? this.defaultModel)
|
|
82
83
|
const text = response.choices[0]?.message?.content ?? '{}'
|
|
83
84
|
return parseJSONResponse<T>(text)
|
|
84
85
|
}
|
|
@@ -122,7 +123,8 @@ export class OpenAIAIClient implements AIClient {
|
|
|
122
123
|
|
|
123
124
|
const response = await this.sdk.chat.completions.create({
|
|
124
125
|
model: options.model ?? this.defaultModel,
|
|
125
|
-
max_tokens
|
|
126
|
+
/* `max_tokens` is refused by the gpt-5 and o-series models; every chat model takes this one. */
|
|
127
|
+
max_completion_tokens: options.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
126
128
|
...(options.temperature !== undefined && { temperature: options.temperature }),
|
|
127
129
|
...(options.stopSequences && { stop: options.stopSequences }),
|
|
128
130
|
messages: sdkMessages as any,
|
|
@@ -130,6 +132,7 @@ export class OpenAIAIClient implements AIClient {
|
|
|
130
132
|
...(toolChoice !== undefined && { tool_choice: toolChoice as any }),
|
|
131
133
|
...(options.allowParallelToolCalls === false && { parallel_tool_calls: false })
|
|
132
134
|
})
|
|
135
|
+
reportUsage(response, options, options.model ?? this.defaultModel)
|
|
133
136
|
|
|
134
137
|
const choice = response.choices?.[0]
|
|
135
138
|
const message = choice?.message
|
|
@@ -156,6 +159,29 @@ export class OpenAIAIClient implements AIClient {
|
|
|
156
159
|
stopReason: normalizeOpenAIStopReason(choice?.finish_reason)
|
|
157
160
|
}
|
|
158
161
|
}
|
|
162
|
+
|
|
163
|
+
async listModels(): Promise<string[]> {
|
|
164
|
+
const ids: string[] = []
|
|
165
|
+
for await (const model of this.sdk.models.list()) ids.push(model.id)
|
|
166
|
+
return ids.filter(isOpenAIChatModel).sort()
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* OpenAI's model list has no capability field, so the conversation models are told apart by name: the GPT and
|
|
172
|
+
* o-series families, without their audio, realtime, speech, image, search and instruct variants. One rule, kept by a test.
|
|
173
|
+
*/
|
|
174
|
+
export function isOpenAIChatModel(id: string): boolean {
|
|
175
|
+
if (!/^(gpt-|chatgpt-|o\d)/.test(id)) return false
|
|
176
|
+
return !/(audio|realtime|tts|transcribe|image|search|instruct|embedding|moderation)/.test(id)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function reportUsage(response: any, options: AIChatOptions, requested: string): void {
|
|
180
|
+
options.onUsage?.({
|
|
181
|
+
model: response?.model ?? requested,
|
|
182
|
+
inputTokens: response?.usage?.prompt_tokens,
|
|
183
|
+
outputTokens: response?.usage?.completion_tokens
|
|
184
|
+
})
|
|
159
185
|
}
|
|
160
186
|
|
|
161
187
|
// ── 변환 유틸 ─────────────────────────────────────────────────────────
|
package/server/service/types.ts
CHANGED
|
@@ -80,6 +80,18 @@ export interface AIChatOptions {
|
|
|
80
80
|
*/
|
|
81
81
|
systemPrompt?: string
|
|
82
82
|
stopSequences?: string[]
|
|
83
|
+
/**
|
|
84
|
+
* Told what one provider call used, right after it returns. The metering boundary (aiClientFor) sets this per
|
|
85
|
+
* call so the usage row knows whose call it was; adapters call it once for every request they send.
|
|
86
|
+
*/
|
|
87
|
+
onUsage?: (usage: AIProviderUsage) => void
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** What one provider request used, as the provider reported it. A count the provider did not report stays undefined. */
|
|
91
|
+
export interface AIProviderUsage {
|
|
92
|
+
model: string
|
|
93
|
+
inputTokens?: number
|
|
94
|
+
outputTokens?: number
|
|
83
95
|
}
|
|
84
96
|
|
|
85
97
|
export interface AIJSONOptions extends AIChatOptions {
|
|
@@ -180,14 +192,18 @@ export interface AIClient {
|
|
|
180
192
|
tools: AITool[],
|
|
181
193
|
options?: AIToolChatOptions
|
|
182
194
|
): Promise<AIToolChatResult>
|
|
195
|
+
|
|
196
|
+
/** The ids of the conversation models this key may call, sorted. Reading it also proves the key is accepted. */
|
|
197
|
+
listModels(): Promise<string[]>
|
|
183
198
|
}
|
|
184
199
|
|
|
185
200
|
export interface AIClientConfig {
|
|
186
201
|
/** 등록된 provider 이름 (예: 'anthropic', 'openai', 'gemini') */
|
|
187
202
|
provider: string
|
|
188
|
-
/**
|
|
203
|
+
/** The model every call uses unless a call names another. */
|
|
189
204
|
model?: string
|
|
190
|
-
|
|
205
|
+
/** Required - a provider is never reached with a key from the process environment. */
|
|
206
|
+
apiKey: string
|
|
191
207
|
/** OpenAI organization (provider:'openai' 전용) */
|
|
192
208
|
organization?: string
|
|
193
209
|
baseURL?: string
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"error.ai-not-configured": "AI is not set up for this domain.",
|
|
3
|
+
"error.ai-agent-not-available": "The AI agent you picked is not available here.",
|
|
4
|
+
"error.ai-agent-provider-unknown": "\"{provider}\" is not a provider an agent can use.",
|
|
5
|
+
"error.ai-agent-key-required": "Enter the API key of \"{agent}\".",
|
|
6
|
+
"error.ai-agent-name-required": "Every agent needs a name.",
|
|
7
|
+
"error.ai-agent-model-required": "Pick the model of \"{agent}\".",
|
|
8
|
+
"error.ai-agent-model-not-offered": "The provider does not offer \"{model}\" to this key.",
|
|
9
|
+
"error.ai-agent-name-taken": "There is already an agent named \"{name}\".",
|
|
10
|
+
"error.ai-agent-not-in-domain": "That agent does not belong to this domain.",
|
|
11
|
+
"error.ai-provider-key-rejected": "The provider did not accept this API key.",
|
|
12
|
+
"error.ai-provider-unreachable": "The provider could not be reached."
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"error.ai-not-configured": "La IA no está configurada para este dominio.",
|
|
3
|
+
"error.ai-agent-not-available": "El agente de IA elegido no está disponible aquí.",
|
|
4
|
+
"error.ai-agent-provider-unknown": "\"{provider}\" no es un proveedor que un agente pueda usar.",
|
|
5
|
+
"error.ai-agent-key-required": "Introduzca la clave de API de \"{agent}\".",
|
|
6
|
+
"error.ai-agent-name-required": "Cada agente necesita un nombre.",
|
|
7
|
+
"error.ai-agent-model-required": "Elija el modelo de \"{agent}\".",
|
|
8
|
+
"error.ai-agent-model-not-offered": "El proveedor no ofrece \"{model}\" a esta clave.",
|
|
9
|
+
"error.ai-agent-name-taken": "Ya existe un agente llamado \"{name}\".",
|
|
10
|
+
"error.ai-agent-not-in-domain": "Ese agente no pertenece a este dominio.",
|
|
11
|
+
"error.ai-provider-key-rejected": "El proveedor no aceptó esta clave de API.",
|
|
12
|
+
"error.ai-provider-unreachable": "No se pudo contactar con el proveedor."
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"error.ai-not-configured": "この事業所には AI が設定されていません。",
|
|
3
|
+
"error.ai-agent-not-available": "選んだ AI エージェントはこの事業所では使えません。",
|
|
4
|
+
"error.ai-agent-provider-unknown": "「{provider}」はエージェントが使える提供元ではありません。",
|
|
5
|
+
"error.ai-agent-key-required": "「{agent}」の API キーを入力してください。",
|
|
6
|
+
"error.ai-agent-name-required": "エージェントごとに名前が必要です。",
|
|
7
|
+
"error.ai-agent-model-required": "「{agent}」のモデルを選んでください。",
|
|
8
|
+
"error.ai-agent-model-not-offered": "提供元はこのキーに「{model}」を提供していません。",
|
|
9
|
+
"error.ai-agent-name-taken": "「{name}」という名前のエージェントがすでにあります。",
|
|
10
|
+
"error.ai-agent-not-in-domain": "そのエージェントはこの事業所のものではありません。",
|
|
11
|
+
"error.ai-provider-key-rejected": "提供元がこの API キーを受け付けませんでした。",
|
|
12
|
+
"error.ai-provider-unreachable": "提供元に接続できませんでした。"
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"error.ai-not-configured": "이 사업장에는 AI 가 설정되어 있지 않습니다.",
|
|
3
|
+
"error.ai-agent-not-available": "고른 AI 에이전트를 이 사업장에서 쓸 수 없습니다.",
|
|
4
|
+
"error.ai-agent-provider-unknown": "\"{provider}\" 은(는) 에이전트가 쓸 수 있는 제공처가 아닙니다.",
|
|
5
|
+
"error.ai-agent-key-required": "\"{agent}\" 의 API 키를 넣어 주세요.",
|
|
6
|
+
"error.ai-agent-name-required": "에이전트마다 이름이 있어야 합니다.",
|
|
7
|
+
"error.ai-agent-model-required": "\"{agent}\" 의 모델을 골라 주세요.",
|
|
8
|
+
"error.ai-agent-model-not-offered": "제공처가 이 키에 \"{model}\" 모델을 제공하지 않습니다.",
|
|
9
|
+
"error.ai-agent-name-taken": "\"{name}\" 이라는 에이전트가 이미 있습니다.",
|
|
10
|
+
"error.ai-agent-not-in-domain": "그 에이전트는 이 사업장의 것이 아닙니다.",
|
|
11
|
+
"error.ai-provider-key-rejected": "제공처가 이 API 키를 받지 않았습니다.",
|
|
12
|
+
"error.ai-provider-unreachable": "제공처에 닿지 않았습니다."
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"error.ai-not-configured": "AI belum disediakan untuk domain ini.",
|
|
3
|
+
"error.ai-agent-not-available": "Ejen AI yang dipilih tidak tersedia di sini.",
|
|
4
|
+
"error.ai-agent-provider-unknown": "\"{provider}\" bukan penyedia yang boleh digunakan oleh ejen.",
|
|
5
|
+
"error.ai-agent-key-required": "Masukkan kunci API untuk \"{agent}\".",
|
|
6
|
+
"error.ai-agent-name-required": "Setiap ejen memerlukan nama.",
|
|
7
|
+
"error.ai-agent-model-required": "Pilih model untuk \"{agent}\".",
|
|
8
|
+
"error.ai-agent-model-not-offered": "Penyedia tidak menawarkan \"{model}\" kepada kunci ini.",
|
|
9
|
+
"error.ai-agent-name-taken": "Sudah ada ejen bernama \"{name}\".",
|
|
10
|
+
"error.ai-agent-not-in-domain": "Ejen itu bukan milik domain ini.",
|
|
11
|
+
"error.ai-provider-key-rejected": "Penyedia tidak menerima kunci API ini.",
|
|
12
|
+
"error.ai-provider-unreachable": "Penyedia tidak dapat dihubungi."
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"error.ai-not-configured": "此站点尚未设置 AI。",
|
|
3
|
+
"error.ai-agent-not-available": "所选的 AI 代理在此站点不可用。",
|
|
4
|
+
"error.ai-agent-provider-unknown": "“{provider}”不是代理可用的提供商。",
|
|
5
|
+
"error.ai-agent-key-required": "请输入“{agent}”的 API 密钥。",
|
|
6
|
+
"error.ai-agent-name-required": "每个代理都需要名称。",
|
|
7
|
+
"error.ai-agent-model-required": "请选择“{agent}”的模型。",
|
|
8
|
+
"error.ai-agent-model-not-offered": "提供商未向此密钥提供“{model}”。",
|
|
9
|
+
"error.ai-agent-name-taken": "已存在名为“{name}”的代理。",
|
|
10
|
+
"error.ai-agent-not-in-domain": "该代理不属于此站点。",
|
|
11
|
+
"error.ai-provider-key-rejected": "提供商未接受此 API 密钥。",
|
|
12
|
+
"error.ai-provider-unreachable": "无法连接到提供商。"
|
|
13
|
+
}
|