@things-factory/ai-client-base 10.1.260 → 10.1.261
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-server/service/agent/ai-client-for.js +1 -0
- package/dist-server/service/agent/ai-client-for.js.map +1 -1
- package/dist-server/service/agent/ai-usage.js +1 -1
- package/dist-server/service/agent/ai-usage.js.map +1 -1
- package/dist-server/service/providers/openai.d.ts +9 -0
- package/dist-server/service/providers/openai.js +11 -0
- package/dist-server/service/providers/openai.js.map +1 -1
- package/dist-server/service/types.d.ts +9 -0
- package/dist-server/service/types.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/server/service/agent/ai-client-for.test.ts +11 -0
- package/server/service/agent/ai-client-for.ts +1 -0
- package/server/service/agent/ai-usage.ts +1 -1
- package/server/service/providers/openai.ts +12 -0
- package/server/service/providers/transcribe.test.ts +18 -0
- package/server/service/types.ts +3 -0
|
@@ -124,6 +124,7 @@ function meter(client, entry, request) {
|
|
|
124
124
|
agent: { id: spec.id, name: spec.name, provider: spec.provider, model: spec.model },
|
|
125
125
|
chat: (messages, options) => measured('chat', options, o => client.chat(messages, o)),
|
|
126
126
|
generateJSON: (prompt, options) => measured('generateJSON', options, o => client.generateJSON(prompt, o)),
|
|
127
|
+
...(client.transcribe && { transcribe: (audio, options) => measured('transcribe', options, o => client.transcribe(audio, o)) }),
|
|
127
128
|
...(client.vision && {
|
|
128
129
|
vision: (image, prompt, options) => measured('vision', options, o => client.vision(image, prompt, o))
|
|
129
130
|
}),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-client-for.js","sourceRoot":"","sources":["../../../server/service/agent/ai-client-for.ts"],"names":[],"mappings":"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","sourcesContent":["import { Domain, getRepository, Refusal } from '@things-factory/shell'\n\nimport { createAIClient } from '../factory.js'\nimport type { AIChatOptions, AIClient, AIProviderUsage } from '../types.js'\nimport { type AiAgentEntry, readAiAgentDirectory } from './agent-directory.js'\nimport type { AiUsage } from './ai-usage.js'\n\n/**\n * **The one door to a model** (design/ai/agent-settings.md §4).\n *\n * Every call that reaches a provider gets its client here, for a domain: the agent is the one asked for, or the\n * domain's default, and its key is the closest one on the domain chain. There is no deployment-wide client - a domain\n * with no usable agent has no AI (user decision 2026-10-12).\n *\n * The client handed back is metered: each provider request writes one AiUsage row with the domain, the person, the\n * agent and what was used (ADR-0061 decision 6).\n */\nexport interface AiClientRequest {\n domain: Domain\n /** The person calling. Empty for a call with no person, such as a webhook. */\n user?: { id: string } | null\n /** The agent the person picked. Empty takes the domain's default. */\n agentId?: string | null\n /** The conversation mode, when the call is part of a conversation. */\n modeKind?: string | null\n /** The conversation, by id. */\n sessionId?: string | null\n}\n\n/** The client of one agent, as a call got it. */\nexport interface AgentClient extends AIClient {\n readonly agent: { id: string; name: string; provider: string; model: string }\n}\n\n/* The usage rows still being written: a request's answer does not wait for its row. */\nconst recording = new Set<Promise<unknown>>()\n\n/**\n * Once every usage row asked for so far is written (or failed and said so). For a process that closes its database\n * after its last call -- a script running scenarios -- so the last rows are not lost to a closed handle.\n */\nexport async function aiUsageRecorded(): Promise<void> {\n while (recording.size) await Promise.allSettled([...recording])\n}\n\nexport async function aiClientFor(request: AiClientRequest): Promise<AgentClient> {\n if (_testing) return _testing(request)\n if (!request.domain) throw new Error('aiClientFor needs the domain the call is made in')\n\n const directory = await readAiAgentDirectory(request.domain)\n const entry = pickAgent(directory.entries, request.agentId, directory.defaultAgentId)\n return meter(providerClientOf(entry), entry, request)\n}\n\n/**\n * The agent a request is answered by. A named agent that this domain cannot use is refused, never swapped for\n * another; no agent at all is \"not configured\".\n */\nexport function pickAgent(entries: AiAgentEntry[], agentId: string | null | undefined, defaultAgentId?: string): AiAgentEntry {\n if (agentId) {\n const named = entries.find(one => one.spec.id === agentId)\n if (!named?.usable) {\n throw new Refusal('ai-agent-not-available', { agentId }, 'The AI agent you picked is not available here.')\n }\n return named\n }\n const fallback = entries.find(one => one.spec.id === defaultAgentId && one.usable)\n if (!fallback) {\n throw new Refusal('ai-not-configured', {}, 'AI is not set up for this domain.')\n }\n return fallback\n}\n\n/* Provider clients are kept per agent and rebuilt when its definition or its key changes. */\nconst _clients = new Map<string, { version: string; client: AIClient }>()\n\nfunction providerClientOf(entry: AiAgentEntry): AIClient {\n const { spec } = entry\n const version = JSON.stringify([spec.provider, spec.model, spec.baseURL, spec.organization, entry.keyVersion])\n const cached = _clients.get(spec.id)\n if (cached?.version === version) return cached.client\n\n const client = createAIClient({\n provider: spec.provider,\n model: spec.model,\n apiKey: entry.apiKey!,\n ...(spec.baseURL && { baseURL: spec.baseURL }),\n ...(spec.organization && { organization: spec.organization })\n })\n _clients.set(spec.id, { version, client })\n return client\n}\n\nfunction meter(client: AIClient, entry: AiAgentEntry, request: AiClientRequest): AgentClient {\n const { spec } = entry\n\n async function measured<T>(operation: string, options: AIChatOptions | undefined, call: (options: AIChatOptions) => Promise<T>): Promise<T> {\n const started = new Date()\n const reported: AIProviderUsage[] = []\n const withUsage: AIChatOptions = {\n ...options,\n onUsage: usage => {\n reported.push(usage)\n options?.onUsage?.(usage)\n }\n }\n try {\n const result = await call(withUsage)\n record(operation, started, reported, 'ok')\n return result\n } catch (error: any) {\n record(operation, started, reported, outcomeOf(error))\n /* A failure must say why somewhere: the loop above turns it into a sentence for the person, and the usage row\n keeps only its code. The provider's own message names no key. */\n console.error(\n `[ai-client-base] ${spec.provider}:${spec.model} (${spec.name}) ${operation} failed:`,\n error?.status ?? '',\n error?.message ?? error\n )\n throw error\n }\n }\n\n function record(operation: string, started: Date, reported: AIProviderUsage[], outcome: string) {\n const durationMs = Date.now() - started.valueOf()\n /* A failed request reports nothing, and is still one row - a failure costs time and often money. */\n const rows = (reported.length ? reported : [{ model: spec.model } as AIProviderUsage]).map((usage, index, all) => ({\n domain: { id: request.domain.id },\n ...(request.user?.id && { creator: { id: request.user.id } }),\n agentId: spec.id,\n agentDomainId: entry.ownerDomainId,\n keyDomainId: entry.keyDomainId,\n provider: spec.provider,\n model: usage.model || spec.model,\n modeKind: request.modeKind ?? undefined,\n operation,\n sessionId: request.sessionId ?? undefined,\n inputTokens: usage.inputTokens,\n outputTokens: usage.outputTokens,\n /* The whole call's time goes on its last request; the rows of one call still add up to it. */\n durationMs: index === all.length - 1 ? durationMs : 0,\n outcome: index === all.length - 1 ? outcome : 'ok',\n hourStart: hourOf(started)\n }))\n /* Outside the caller's transaction: a rolled-back write still spent the tokens. By name, so the decorated entity\n stays out of this module's imports. */\n const written: Promise<unknown> = getRepository<AiUsage>('AiUsage')\n .save(rows as any)\n .catch(error => console.error('[ai-client-base] could not record AI usage:', error?.message ?? error))\n .finally(() => recording.delete(written))\n recording.add(written)\n }\n\n const metered: AgentClient = {\n id: `${spec.provider}:${spec.model}`,\n agent: { id: spec.id, name: spec.name, provider: spec.provider, model: spec.model },\n chat: (messages, options) => measured('chat', options, o => client.chat(messages, o)),\n generateJSON: (prompt, options) => measured('generateJSON', options, o => client.generateJSON(prompt, o)),\n ...(client.vision && {\n vision: (image, prompt, options) => measured('vision', options, o => client.vision!(image, prompt, o))\n }),\n ...(client.chatWithTools && {\n chatWithTools: (messages, tools, options) =>\n measured('chatWithTools', options, o => client.chatWithTools!(messages, tools, o))\n }),\n listModels: () => client.listModels()\n }\n return metered\n}\n\n/** The UTC hour a moment falls in. */\nexport function hourOf(moment: Date): Date {\n const hour = new Date(moment)\n hour.setUTCMinutes(0, 0, 0)\n return hour\n}\n\n/** Why a provider request failed, as the usage row keeps it. */\nexport function outcomeOf(error: any): string {\n const status = Number(error?.status ?? error?.statusCode ?? error?.response?.status)\n if (status === 401 || status === 403) return 'key-rejected'\n if (status === 429) return 'rate-limited'\n return 'provider-error'\n}\n\ntype TestClientResolver = (request: AiClientRequest) => AgentClient\n\nlet _testing: TestClientResolver | undefined\n\nexport function isAIClientForTesting(): boolean {\n return !!_testing\n}\n\n/**\n * Tests answer aiClientFor with a client of their own - no database, no provider, no usage rows. A client is wrapped\n * as an agent named after it; a function decides per request. `undefined` puts the real door back.\n */\nexport function setAIClientForTesting(client: AIClient | TestClientResolver | undefined): void {\n if (!client) {\n _testing = undefined\n } else if (typeof client === 'function') {\n _testing = client\n } else {\n /* The test's own object answers - its counters and `this` stay its own; it only gains the agent it is named as. */\n if (!(client as AgentClient).agent) {\n const [provider, model] = String(client.id ?? 'test:test').split(':')\n Object.defineProperty(client, 'agent', {\n value: { id: 'test-agent', name: client.id ?? 'test', provider, model },\n configurable: true\n })\n }\n _testing = () => client as AgentClient\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ai-client-for.js","sourceRoot":"","sources":["../../../server/service/agent/ai-client-for.ts"],"names":[],"mappings":"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","sourcesContent":["import { Domain, getRepository, Refusal } from '@things-factory/shell'\n\nimport { createAIClient } from '../factory.js'\nimport type { AIChatOptions, AIClient, AIProviderUsage } from '../types.js'\nimport { type AiAgentEntry, readAiAgentDirectory } from './agent-directory.js'\nimport type { AiUsage } from './ai-usage.js'\n\n/**\n * **The one door to a model** (design/ai/agent-settings.md §4).\n *\n * Every call that reaches a provider gets its client here, for a domain: the agent is the one asked for, or the\n * domain's default, and its key is the closest one on the domain chain. There is no deployment-wide client - a domain\n * with no usable agent has no AI (user decision 2026-10-12).\n *\n * The client handed back is metered: each provider request writes one AiUsage row with the domain, the person, the\n * agent and what was used (ADR-0061 decision 6).\n */\nexport interface AiClientRequest {\n domain: Domain\n /** The person calling. Empty for a call with no person, such as a webhook. */\n user?: { id: string } | null\n /** The agent the person picked. Empty takes the domain's default. */\n agentId?: string | null\n /** The conversation mode, when the call is part of a conversation. */\n modeKind?: string | null\n /** The conversation, by id. */\n sessionId?: string | null\n}\n\n/** The client of one agent, as a call got it. */\nexport interface AgentClient extends AIClient {\n readonly agent: { id: string; name: string; provider: string; model: string }\n}\n\n/* The usage rows still being written: a request's answer does not wait for its row. */\nconst recording = new Set<Promise<unknown>>()\n\n/**\n * Once every usage row asked for so far is written (or failed and said so). For a process that closes its database\n * after its last call -- a script running scenarios -- so the last rows are not lost to a closed handle.\n */\nexport async function aiUsageRecorded(): Promise<void> {\n while (recording.size) await Promise.allSettled([...recording])\n}\n\nexport async function aiClientFor(request: AiClientRequest): Promise<AgentClient> {\n if (_testing) return _testing(request)\n if (!request.domain) throw new Error('aiClientFor needs the domain the call is made in')\n\n const directory = await readAiAgentDirectory(request.domain)\n const entry = pickAgent(directory.entries, request.agentId, directory.defaultAgentId)\n return meter(providerClientOf(entry), entry, request)\n}\n\n/**\n * The agent a request is answered by. A named agent that this domain cannot use is refused, never swapped for\n * another; no agent at all is \"not configured\".\n */\nexport function pickAgent(entries: AiAgentEntry[], agentId: string | null | undefined, defaultAgentId?: string): AiAgentEntry {\n if (agentId) {\n const named = entries.find(one => one.spec.id === agentId)\n if (!named?.usable) {\n throw new Refusal('ai-agent-not-available', { agentId }, 'The AI agent you picked is not available here.')\n }\n return named\n }\n const fallback = entries.find(one => one.spec.id === defaultAgentId && one.usable)\n if (!fallback) {\n throw new Refusal('ai-not-configured', {}, 'AI is not set up for this domain.')\n }\n return fallback\n}\n\n/* Provider clients are kept per agent and rebuilt when its definition or its key changes. */\nconst _clients = new Map<string, { version: string; client: AIClient }>()\n\nfunction providerClientOf(entry: AiAgentEntry): AIClient {\n const { spec } = entry\n const version = JSON.stringify([spec.provider, spec.model, spec.baseURL, spec.organization, entry.keyVersion])\n const cached = _clients.get(spec.id)\n if (cached?.version === version) return cached.client\n\n const client = createAIClient({\n provider: spec.provider,\n model: spec.model,\n apiKey: entry.apiKey!,\n ...(spec.baseURL && { baseURL: spec.baseURL }),\n ...(spec.organization && { organization: spec.organization })\n })\n _clients.set(spec.id, { version, client })\n return client\n}\n\nfunction meter(client: AIClient, entry: AiAgentEntry, request: AiClientRequest): AgentClient {\n const { spec } = entry\n\n async function measured<T>(operation: string, options: AIChatOptions | undefined, call: (options: AIChatOptions) => Promise<T>): Promise<T> {\n const started = new Date()\n const reported: AIProviderUsage[] = []\n const withUsage: AIChatOptions = {\n ...options,\n onUsage: usage => {\n reported.push(usage)\n options?.onUsage?.(usage)\n }\n }\n try {\n const result = await call(withUsage)\n record(operation, started, reported, 'ok')\n return result\n } catch (error: any) {\n record(operation, started, reported, outcomeOf(error))\n /* A failure must say why somewhere: the loop above turns it into a sentence for the person, and the usage row\n keeps only its code. The provider's own message names no key. */\n console.error(\n `[ai-client-base] ${spec.provider}:${spec.model} (${spec.name}) ${operation} failed:`,\n error?.status ?? '',\n error?.message ?? error\n )\n throw error\n }\n }\n\n function record(operation: string, started: Date, reported: AIProviderUsage[], outcome: string) {\n const durationMs = Date.now() - started.valueOf()\n /* A failed request reports nothing, and is still one row - a failure costs time and often money. */\n const rows = (reported.length ? reported : [{ model: spec.model } as AIProviderUsage]).map((usage, index, all) => ({\n domain: { id: request.domain.id },\n ...(request.user?.id && { creator: { id: request.user.id } }),\n agentId: spec.id,\n agentDomainId: entry.ownerDomainId,\n keyDomainId: entry.keyDomainId,\n provider: spec.provider,\n model: usage.model || spec.model,\n modeKind: request.modeKind ?? undefined,\n operation,\n sessionId: request.sessionId ?? undefined,\n inputTokens: usage.inputTokens,\n outputTokens: usage.outputTokens,\n /* The whole call's time goes on its last request; the rows of one call still add up to it. */\n durationMs: index === all.length - 1 ? durationMs : 0,\n outcome: index === all.length - 1 ? outcome : 'ok',\n hourStart: hourOf(started)\n }))\n /* Outside the caller's transaction: a rolled-back write still spent the tokens. By name, so the decorated entity\n stays out of this module's imports. */\n const written: Promise<unknown> = getRepository<AiUsage>('AiUsage')\n .save(rows as any)\n .catch(error => console.error('[ai-client-base] could not record AI usage:', error?.message ?? error))\n .finally(() => recording.delete(written))\n recording.add(written)\n }\n\n const metered: AgentClient = {\n id: `${spec.provider}:${spec.model}`,\n agent: { id: spec.id, name: spec.name, provider: spec.provider, model: spec.model },\n chat: (messages, options) => measured('chat', options, o => client.chat(messages, o)),\n generateJSON: (prompt, options) => measured('generateJSON', options, o => client.generateJSON(prompt, o)),\n ...(client.transcribe && { transcribe: (audio, options) => measured('transcribe', options, o => client.transcribe!(audio, o)) }),\n ...(client.vision && {\n vision: (image, prompt, options) => measured('vision', options, o => client.vision!(image, prompt, o))\n }),\n ...(client.chatWithTools && {\n chatWithTools: (messages, tools, options) =>\n measured('chatWithTools', options, o => client.chatWithTools!(messages, tools, o))\n }),\n listModels: () => client.listModels()\n }\n return metered\n}\n\n/** The UTC hour a moment falls in. */\nexport function hourOf(moment: Date): Date {\n const hour = new Date(moment)\n hour.setUTCMinutes(0, 0, 0)\n return hour\n}\n\n/** Why a provider request failed, as the usage row keeps it. */\nexport function outcomeOf(error: any): string {\n const status = Number(error?.status ?? error?.statusCode ?? error?.response?.status)\n if (status === 401 || status === 403) return 'key-rejected'\n if (status === 429) return 'rate-limited'\n return 'provider-error'\n}\n\ntype TestClientResolver = (request: AiClientRequest) => AgentClient\n\nlet _testing: TestClientResolver | undefined\n\nexport function isAIClientForTesting(): boolean {\n return !!_testing\n}\n\n/**\n * Tests answer aiClientFor with a client of their own - no database, no provider, no usage rows. A client is wrapped\n * as an agent named after it; a function decides per request. `undefined` puts the real door back.\n */\nexport function setAIClientForTesting(client: AIClient | TestClientResolver | undefined): void {\n if (!client) {\n _testing = undefined\n } else if (typeof client === 'function') {\n _testing = client\n } else {\n /* The test's own object answers - its counters and `this` stay its own; it only gains the agent it is named as. */\n if (!(client as AgentClient).agent) {\n const [provider, model] = String(client.id ?? 'test:test').split(':')\n Object.defineProperty(client, 'agent', {\n value: { id: 'test-agent', name: client.id ?? 'test', provider, model },\n configurable: true\n })\n }\n _testing = () => client as AgentClient\n }\n}\n"]}
|
|
@@ -62,7 +62,7 @@ tslib_1.__decorate([
|
|
|
62
62
|
], AiUsage.prototype, "modeKind", void 0);
|
|
63
63
|
tslib_1.__decorate([
|
|
64
64
|
(0, typeorm_1.Column)(),
|
|
65
|
-
(0, type_graphql_1.Field)(type => String, { description: 'chat, chatWithTools, generateJSON or
|
|
65
|
+
(0, type_graphql_1.Field)(type => String, { description: 'chat, chatWithTools, generateJSON, vision or transcribe.' }),
|
|
66
66
|
tslib_1.__metadata("design:type", String)
|
|
67
67
|
], AiUsage.prototype, "operation", void 0);
|
|
68
68
|
tslib_1.__decorate([
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-usage.js","sourceRoot":"","sources":["../../../server/service/agent/ai-usage.ts"],"names":[],"mappings":";;;;AAAA,qCAAgH;AAChH,+CAAyD;AAEzD,yDAAgD;AAChD,iDAA8C;AAE9C;;;;;;GAMG;AAKI,IAAM,OAAO,GAAb,MAAM,OAAO;CA+EnB,CAAA;AA/EY,0BAAO;AAGT;IAFR,IAAA,gCAAsB,EAAC,MAAM,CAAC;IAC9B,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,iBAAE,EAAE,EAAE,WAAW,EAAE,SAAS,EAAE,CAAC;;mCAC3B;AAInB;IAFC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,cAAM,CAAC;IACzB,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,cAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,kCAAkC,EAAE,CAAC;sCAClF,cAAM;uCAAA;AAGf;IADC,IAAA,oBAAU,EAAC,CAAC,KAAc,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC;;yCAC5B;AAIjB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,0BAA0B,EAAE,CAAC;;wCACnE;AAIhB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,mFAAmF,EAAE,CAAC;;8CACtH;AAItB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,6DAA6D,EAAE,CAAC;;4CAClG;AAIpB;IAFC,IAAA,gBAAM,GAAE;IACR,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,WAAW,EAAE,8BAA8B,EAAE,CAAC;;yCACvD;AAIhB;IAFC,IAAA,gBAAM,GAAE;IACR,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,WAAW,EAAE,4CAA4C,EAAE,CAAC;;sCACxE;AAIb;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,uEAAuE,EAAE,CAAC;;yCAC/G;AAIjB;IAFC,IAAA,gBAAM,GAAE;IACR,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,WAAW,EAAE,
|
|
1
|
+
{"version":3,"file":"ai-usage.js","sourceRoot":"","sources":["../../../server/service/agent/ai-usage.ts"],"names":[],"mappings":"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","sourcesContent":["import { Column, CreateDateColumn, Entity, Index, ManyToOne, PrimaryGeneratedColumn, RelationId } from 'typeorm'\nimport { Field, ID, Int, ObjectType } from 'type-graphql'\n\nimport { User } from '@things-factory/auth-base'\nimport { Domain } from '@things-factory/shell'\n\n/**\n * One provider request - the metering boundary writes a row each time a model is called (ADR-0061 decision 6,\n * design/ai/agent-settings.md §6).\n *\n * A turn that loops through tools makes several rows. The row is joined to a conversation by id only and is never\n * copied onto a message. It is written once and never changed, so it has no updater.\n */\n@Entity()\n@Index('ix_ai_usage_0', (usage: AiUsage) => [usage.domain, usage.hourStart])\n@Index('ix_ai_usage_1', (usage: AiUsage) => [usage.agentDomainId, usage.hourStart])\n@ObjectType({ description: 'One request an AI agent sent to its provider, with what it used.' })\nexport class AiUsage {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { description: 'Row id.' })\n readonly id: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain, { nullable: true, description: 'The domain the call was made in.' })\n domain?: Domain\n\n @RelationId((usage: AiUsage) => usage.domain)\n domainId?: string\n\n @Column({ nullable: true })\n @Field(type => String, { nullable: true, description: 'The agent that answered.' })\n agentId?: string\n\n @Column({ nullable: true })\n @Field(type => String, { nullable: true, description: 'The domain that declared the agent - a domain above when the agent was inherited.' })\n agentDomainId?: string\n\n @Column({ nullable: true })\n @Field(type => String, { nullable: true, description: 'The domain whose API key answered - whose cost the call is.' })\n keyDomainId?: string\n\n @Column()\n @Field(type => String, { description: 'openai, anthropic or gemini.' })\n provider: string\n\n @Column()\n @Field(type => String, { description: 'The model the provider reported answering.' })\n model: string\n\n @Column({ nullable: true })\n @Field(type => String, { nullable: true, description: 'The conversation mode kind, when the call was part of a conversation.' })\n modeKind?: string\n\n @Column()\n @Field(type => String, { description: 'chat, chatWithTools, generateJSON, vision or transcribe.' })\n operation: string\n\n @Column({ nullable: true })\n @Field(type => String, { nullable: true, description: 'The conversation the call belongs to, by id only.' })\n sessionId?: string\n\n @Column({ type: 'int', nullable: true })\n @Field(type => Int, { nullable: true, description: 'Input tokens the provider reported.' })\n inputTokens?: number\n\n @Column({ type: 'int', nullable: true })\n @Field(type => Int, { nullable: true, description: 'Output tokens the provider reported.' })\n outputTokens?: number\n\n @Column({ type: 'int' })\n @Field(type => Int, { description: 'How long the request took, in milliseconds.' })\n durationMs: number\n\n /** `ok`, or why it failed: key-rejected · rate-limited · provider-error. */\n @Column()\n @Field(type => String, { description: 'ok, or why the request failed: key-rejected, rate-limited or provider-error.' })\n outcome: string\n\n /**\n * The UTC hour the call started in. Usage is grouped by this column, which every database can do; days and months\n * are rolled up from it in the domain's time zone.\n */\n @Column()\n @Field(type => Date, { description: 'The UTC hour the call started in.' })\n hourStart: Date\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true, description: 'The person who made the call. Empty for a call with no person, such as a webhook.' })\n creator?: User\n\n @RelationId((usage: AiUsage) => usage.creator)\n creatorId?: string\n\n @CreateDateColumn()\n @Field(type => Date, { description: 'When the call was made.' })\n createdAt: Date\n}\n"]}
|
|
@@ -4,6 +4,15 @@ export declare class OpenAIAIClient implements AIClient {
|
|
|
4
4
|
private sdk;
|
|
5
5
|
private defaultModel;
|
|
6
6
|
constructor(config: AIClientConfig);
|
|
7
|
+
/** Whisper accepts the small MP3 prepared by the material reader and returns speech timestamps. */
|
|
8
|
+
transcribe(audio: Buffer, options?: AIChatOptions): Promise<{
|
|
9
|
+
text: string;
|
|
10
|
+
segments: {
|
|
11
|
+
start: number;
|
|
12
|
+
end: number;
|
|
13
|
+
text: string;
|
|
14
|
+
}[];
|
|
15
|
+
}>;
|
|
7
16
|
chat(messages: AIMessage[], options?: AIChatOptions): Promise<string>;
|
|
8
17
|
generateJSON<T = unknown>(prompt: string | AIMessage[], options?: AIJSONOptions): Promise<T>;
|
|
9
18
|
vision(image: Buffer | Uint8Array | {
|
|
@@ -13,6 +13,7 @@ const tslib_1 = require("tslib");
|
|
|
13
13
|
* organization 필드 지원.
|
|
14
14
|
*/
|
|
15
15
|
const openai_1 = tslib_1.__importDefault(require("openai"));
|
|
16
|
+
const uploads_1 = require("openai/uploads");
|
|
16
17
|
const factory_js_1 = require("../factory.js");
|
|
17
18
|
const parse_json_js_1 = require("../utils/parse-json.js");
|
|
18
19
|
const request_options_js_1 = require("./request-options.js");
|
|
@@ -28,6 +29,16 @@ class OpenAIAIClient {
|
|
|
28
29
|
this.defaultModel = config.model ?? DEFAULT_MODEL;
|
|
29
30
|
this.id = `openai:${this.defaultModel}`;
|
|
30
31
|
}
|
|
32
|
+
/** Whisper accepts the small MP3 prepared by the material reader and returns speech timestamps. */
|
|
33
|
+
async transcribe(audio, options = {}) {
|
|
34
|
+
const model = 'whisper-1';
|
|
35
|
+
const answer = await this.sdk.audio.transcriptions.create({
|
|
36
|
+
file: await (0, uploads_1.toFile)(audio, 'speech.mp3', { type: 'audio/mpeg' }),
|
|
37
|
+
model, response_format: 'verbose_json', timestamp_granularities: ['segment']
|
|
38
|
+
}, (0, request_options_js_1.requestOptionsOf)(options));
|
|
39
|
+
options.onUsage?.({ model }); // Speech billing does not report text token counts.
|
|
40
|
+
return { text: answer.text, segments: (answer.segments ?? []).map(s => ({ start: s.start, end: s.end, text: s.text })) };
|
|
41
|
+
}
|
|
31
42
|
async chat(messages, options = {}) {
|
|
32
43
|
const sdkMessages = buildSDKMessages(messages, options.systemPrompt);
|
|
33
44
|
const response = await this.sdk.chat.completions.create({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openai.js","sourceRoot":"","sources":["../../../server/service/providers/openai.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * OpenAI AI Client adapter.\n *\n * 기본 모델: gpt-4o (구성에서 override 가능).\n * SDK: openai\n *\n * organization 필드 지원.\n */\nimport OpenAI from 'openai'\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'\nimport { requestOptionsOf } from './request-options.js'\n\nconst DEFAULT_MODEL = 'gpt-4o'\nconst DEFAULT_MAX_TOKENS = 4096\n\nexport class OpenAIAIClient implements AIClient {\n readonly id: string\n private sdk: OpenAI\n private defaultModel: string\n\n constructor(config: AIClientConfig) {\n this.sdk = new OpenAI({\n apiKey: config.apiKey,\n ...(config.organization && { organization: config.organization }),\n ...(config.baseURL && { baseURL: config.baseURL })\n })\n this.defaultModel = config.model ?? DEFAULT_MODEL\n this.id = `openai:${this.defaultModel}`\n }\n\n async chat(messages: AIMessage[], options: AIChatOptions = {}): Promise<string> {\n const sdkMessages = buildSDKMessages(messages, options.systemPrompt)\n const response = await this.sdk.chat.completions.create({\n model: options.model ?? this.defaultModel,\n /* `max_tokens` is refused by the gpt-5 and o-series models; every chat model takes this one. */\n max_completion_tokens: options.maxTokens ?? DEFAULT_MAX_TOKENS,\n ...(options.temperature !== undefined && { temperature: options.temperature }),\n ...(options.stopSequences && { stop: options.stopSequences }),\n messages: sdkMessages as any\n }, requestOptionsOf(options))\n reportUsage(response, options, options.model ?? this.defaultModel)\n return response.choices[0]?.message?.content ?? ''\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\n const schemaHint = options.schema\n ? `\\n\\nResponse MUST conform to this JSON schema:\\n${JSON.stringify(options.schema)}`\n : ''\n const enforcedSystem =\n (options.systemPrompt ? options.systemPrompt + '\\n\\n' : '') +\n 'Return only valid JSON. No markdown code fences, no commentary.' +\n schemaHint\n\n const sdkMessages = buildSDKMessages(messages, enforcedSystem)\n const response = await this.sdk.chat.completions.create({\n model: options.model ?? this.defaultModel,\n /* `max_tokens` is refused by the gpt-5 and o-series models; every chat model takes this one. */\n max_completion_tokens: options.maxTokens ?? DEFAULT_MAX_TOKENS,\n ...(options.temperature !== undefined && { temperature: options.temperature }),\n response_format: { type: 'json_object' },\n messages: sdkMessages as any\n }, requestOptionsOf(options))\n reportUsage(response, options, options.model ?? this.defaultModel)\n const text = response.choices[0]?.message?.content ?? '{}'\n return parseJSONResponse<T>(text, { cutShort: response.choices[0]?.finish_reason === 'length' })\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 async chatWithTools(\n messages: AIMessage[],\n tools: AITool[],\n options: AIToolChatOptions = {}\n ): Promise<AIToolChatResult> {\n const sdkMessages = buildSDKMessages(messages, options.systemPrompt)\n\n const sdkTools = tools.map(t => ({\n type: 'function' as const,\n function: {\n name: t.name,\n description: t.description,\n parameters: t.parameters\n }\n }))\n\n const toolChoice = buildOpenAIToolChoice(options)\n\n const response = await this.sdk.chat.completions.create({\n model: options.model ?? this.defaultModel,\n /* `max_tokens` is refused by the gpt-5 and o-series models; every chat model takes this one. */\n max_completion_tokens: options.maxTokens ?? DEFAULT_MAX_TOKENS,\n ...(options.temperature !== undefined && { temperature: options.temperature }),\n ...(options.stopSequences && { stop: options.stopSequences }),\n messages: sdkMessages as any,\n tools: sdkTools,\n ...(toolChoice !== undefined && { tool_choice: toolChoice as any }),\n ...(options.allowParallelToolCalls === false && { parallel_tool_calls: false })\n }, requestOptionsOf(options))\n reportUsage(response, options, options.model ?? this.defaultModel)\n\n const choice = response.choices?.[0]\n const message = choice?.message\n const text = message?.content || undefined\n\n const toolCalls: AIToolCall[] = (message?.tool_calls || []).map((tc: any) => {\n const fn = tc.function\n let args: Record<string, any> = {}\n try {\n args = fn?.arguments ? JSON.parse(fn.arguments) : {}\n } catch {\n args = {}\n }\n return {\n id: typeof tc.id === 'string' && tc.id ? tc.id : `call_${Math.random().toString(36).slice(2, 12)}`,\n name: fn?.name ?? '',\n arguments: args\n }\n })\n\n return {\n text,\n toolCalls,\n stopReason: normalizeOpenAIStopReason(choice?.finish_reason)\n }\n }\n\n async listModels(): Promise<string[]> {\n const ids: string[] = []\n for await (const model of this.sdk.models.list()) ids.push(model.id)\n return ids.filter(isOpenAIChatModel).sort()\n }\n}\n\n/**\n * OpenAI's model list has no capability field, so the conversation models are told apart by name: the GPT and\n * o-series families, without their audio, realtime, speech, image, search and instruct variants. One rule, kept by a test.\n */\nexport function isOpenAIChatModel(id: string): boolean {\n if (!/^(gpt-|chatgpt-|o\\d)/.test(id)) return false\n return !/(audio|realtime|tts|transcribe|image|search|instruct|embedding|moderation)/.test(id)\n}\n\nfunction reportUsage(response: any, options: AIChatOptions, requested: string): void {\n options.onUsage?.({\n model: response?.model ?? requested,\n inputTokens: response?.usage?.prompt_tokens,\n outputTokens: response?.usage?.completion_tokens\n })\n}\n\n// ── 변환 유틸 ─────────────────────────────────────────────────────────\n\nfunction buildSDKMessages(messages: AIMessage[], externalSystem?: string): any[] {\n const out: any[] = []\n\n const systemFromMessages = messages\n .filter(m => m.role === 'system')\n .map(extractTextContent)\n .filter(Boolean)\n .join('\\n\\n')\n const combinedSystem = [externalSystem, systemFromMessages].filter(Boolean).join('\\n\\n')\n if (combinedSystem) {\n out.push({ role: 'system', content: combinedSystem })\n }\n\n for (const msg of messages) {\n if (msg.role === 'system') continue\n const sdkMsgs = toSDKMessages(msg)\n out.push(...sdkMsgs)\n }\n\n return out\n}\n\n/**\n * 한 AIMessage 가 다중 tool_result 를 가지면 OpenAI 측에서 여러 role:'tool' 메시지로\n * 분리해야 함 (OpenAI 는 메시지당 tool_call_id 하나만 매핑). 따라서 array 를 반환.\n */\nexport function toSDKMessages(msg: AIMessage): any[] {\n if (typeof msg.content === 'string') {\n return [{ role: msg.role, content: msg.content }]\n }\n\n const parts = msg.content\n const toolUseParts = parts.filter(p => p.type === 'tool_use') as any[]\n const toolResultParts = parts.filter(p => p.type === 'tool_result') as any[]\n const otherParts = parts.filter(p => p.type !== 'tool_use' && p.type !== 'tool_result')\n\n // tool_result 가 있으면 각각을 role='tool' 메시지로 분리 (OpenAI 요구사항).\n if (toolResultParts.length > 0) {\n const results = toolResultParts.map((r: any) => ({\n role: 'tool',\n tool_call_id: r.toolUseId,\n content: r.content\n }))\n /* A tool message holds text only - pictures a read tool returned follow as one user message (agentic-loop takeToolImages). */\n const images = otherParts.filter(p => p.type === 'image')\n return images.length ? [...results, { role: 'user', content: images.map(toSDKContentPart) }] : results\n }\n\n // assistant 측 tool_use 가 있으면 message.tool_calls 로\n const out: any = { role: msg.role }\n if (otherParts.length > 0) {\n out.content = otherParts.map(toSDKContentPart)\n } else {\n out.content = null\n }\n if (toolUseParts.length > 0) {\n out.tool_calls = toolUseParts.map(p => ({\n id: p.id,\n type: 'function',\n function: { name: p.name, arguments: JSON.stringify(p.arguments) }\n }))\n }\n return [out]\n}\n\nfunction toSDKContentPart(part: AIContentPart): any {\n if (part.type === 'text') {\n return { type: 'text', text: part.text }\n }\n if (part.type === 'tool_use' || part.type === 'tool_result') {\n // 처리는 toSDKMessage 가 함 — 도달하면 버그.\n return { type: 'text', text: '' }\n }\n // image\n if (part.source.kind === 'url') {\n return { type: 'image_url', image_url: { url: part.source.url } }\n }\n const b64 = Buffer.isBuffer(part.source.data)\n ? part.source.data.toString('base64')\n : Buffer.from(part.source.data).toString('base64')\n return {\n type: 'image_url',\n image_url: { url: `data:${part.source.mediaType};base64,${b64}` }\n }\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 buildOpenAIToolChoice(options: AIToolChatOptions): any {\n const choice = options.toolChoice ?? 'auto'\n if (choice === 'none') return 'none'\n if (choice === 'required') {\n if (options.forceTool) {\n return { type: 'function', function: { name: options.forceTool } }\n }\n return 'required'\n }\n return 'auto'\n}\n\nfunction normalizeOpenAIStopReason(raw: any): AIToolChatResult['stopReason'] {\n switch (raw) {\n case 'tool_calls':\n return 'tool_use'\n case 'function_call':\n return 'tool_use'\n case 'stop':\n return 'end_turn'\n case 'length':\n return 'max_tokens'\n case 'content_filter':\n return 'stop_sequence'\n default:\n return raw ?? 'end_turn'\n }\n}\n\n// Auto-register at module load\nregisterAIProvider('openai', config => new OpenAIAIClient(config))\n"]}
|
|
1
|
+
{"version":3,"file":"openai.js","sourceRoot":"","sources":["../../../server/service/providers/openai.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * OpenAI AI Client adapter.\n *\n * 기본 모델: gpt-4o (구성에서 override 가능).\n * SDK: openai\n *\n * organization 필드 지원.\n */\nimport OpenAI from 'openai'\nimport { toFile } from 'openai/uploads'\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'\nimport { requestOptionsOf } from './request-options.js'\n\nconst DEFAULT_MODEL = 'gpt-4o'\nconst DEFAULT_MAX_TOKENS = 4096\n\nexport class OpenAIAIClient implements AIClient {\n readonly id: string\n private sdk: OpenAI\n private defaultModel: string\n\n constructor(config: AIClientConfig) {\n this.sdk = new OpenAI({\n apiKey: config.apiKey,\n ...(config.organization && { organization: config.organization }),\n ...(config.baseURL && { baseURL: config.baseURL })\n })\n this.defaultModel = config.model ?? DEFAULT_MODEL\n this.id = `openai:${this.defaultModel}`\n }\n\n /** Whisper accepts the small MP3 prepared by the material reader and returns speech timestamps. */\n async transcribe(audio: Buffer, options: AIChatOptions = {}) {\n const model = 'whisper-1'\n const answer = await this.sdk.audio.transcriptions.create({\n file: await toFile(audio, 'speech.mp3', { type: 'audio/mpeg' }),\n model, response_format: 'verbose_json', timestamp_granularities: ['segment']\n }, requestOptionsOf(options))\n options.onUsage?.({ model }) // Speech billing does not report text token counts.\n return { text: answer.text, segments: (answer.segments ?? []).map(s => ({ start: s.start, end: s.end, text: s.text })) }\n }\n\n async chat(messages: AIMessage[], options: AIChatOptions = {}): Promise<string> {\n const sdkMessages = buildSDKMessages(messages, options.systemPrompt)\n const response = await this.sdk.chat.completions.create({\n model: options.model ?? this.defaultModel,\n /* `max_tokens` is refused by the gpt-5 and o-series models; every chat model takes this one. */\n max_completion_tokens: options.maxTokens ?? DEFAULT_MAX_TOKENS,\n ...(options.temperature !== undefined && { temperature: options.temperature }),\n ...(options.stopSequences && { stop: options.stopSequences }),\n messages: sdkMessages as any\n }, requestOptionsOf(options))\n reportUsage(response, options, options.model ?? this.defaultModel)\n return response.choices[0]?.message?.content ?? ''\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\n const schemaHint = options.schema\n ? `\\n\\nResponse MUST conform to this JSON schema:\\n${JSON.stringify(options.schema)}`\n : ''\n const enforcedSystem =\n (options.systemPrompt ? options.systemPrompt + '\\n\\n' : '') +\n 'Return only valid JSON. No markdown code fences, no commentary.' +\n schemaHint\n\n const sdkMessages = buildSDKMessages(messages, enforcedSystem)\n const response = await this.sdk.chat.completions.create({\n model: options.model ?? this.defaultModel,\n /* `max_tokens` is refused by the gpt-5 and o-series models; every chat model takes this one. */\n max_completion_tokens: options.maxTokens ?? DEFAULT_MAX_TOKENS,\n ...(options.temperature !== undefined && { temperature: options.temperature }),\n response_format: { type: 'json_object' },\n messages: sdkMessages as any\n }, requestOptionsOf(options))\n reportUsage(response, options, options.model ?? this.defaultModel)\n const text = response.choices[0]?.message?.content ?? '{}'\n return parseJSONResponse<T>(text, { cutShort: response.choices[0]?.finish_reason === 'length' })\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 async chatWithTools(\n messages: AIMessage[],\n tools: AITool[],\n options: AIToolChatOptions = {}\n ): Promise<AIToolChatResult> {\n const sdkMessages = buildSDKMessages(messages, options.systemPrompt)\n\n const sdkTools = tools.map(t => ({\n type: 'function' as const,\n function: {\n name: t.name,\n description: t.description,\n parameters: t.parameters\n }\n }))\n\n const toolChoice = buildOpenAIToolChoice(options)\n\n const response = await this.sdk.chat.completions.create({\n model: options.model ?? this.defaultModel,\n /* `max_tokens` is refused by the gpt-5 and o-series models; every chat model takes this one. */\n max_completion_tokens: options.maxTokens ?? DEFAULT_MAX_TOKENS,\n ...(options.temperature !== undefined && { temperature: options.temperature }),\n ...(options.stopSequences && { stop: options.stopSequences }),\n messages: sdkMessages as any,\n tools: sdkTools,\n ...(toolChoice !== undefined && { tool_choice: toolChoice as any }),\n ...(options.allowParallelToolCalls === false && { parallel_tool_calls: false })\n }, requestOptionsOf(options))\n reportUsage(response, options, options.model ?? this.defaultModel)\n\n const choice = response.choices?.[0]\n const message = choice?.message\n const text = message?.content || undefined\n\n const toolCalls: AIToolCall[] = (message?.tool_calls || []).map((tc: any) => {\n const fn = tc.function\n let args: Record<string, any> = {}\n try {\n args = fn?.arguments ? JSON.parse(fn.arguments) : {}\n } catch {\n args = {}\n }\n return {\n id: typeof tc.id === 'string' && tc.id ? tc.id : `call_${Math.random().toString(36).slice(2, 12)}`,\n name: fn?.name ?? '',\n arguments: args\n }\n })\n\n return {\n text,\n toolCalls,\n stopReason: normalizeOpenAIStopReason(choice?.finish_reason)\n }\n }\n\n async listModels(): Promise<string[]> {\n const ids: string[] = []\n for await (const model of this.sdk.models.list()) ids.push(model.id)\n return ids.filter(isOpenAIChatModel).sort()\n }\n}\n\n/**\n * OpenAI's model list has no capability field, so the conversation models are told apart by name: the GPT and\n * o-series families, without their audio, realtime, speech, image, search and instruct variants. One rule, kept by a test.\n */\nexport function isOpenAIChatModel(id: string): boolean {\n if (!/^(gpt-|chatgpt-|o\\d)/.test(id)) return false\n return !/(audio|realtime|tts|transcribe|image|search|instruct|embedding|moderation)/.test(id)\n}\n\nfunction reportUsage(response: any, options: AIChatOptions, requested: string): void {\n options.onUsage?.({\n model: response?.model ?? requested,\n inputTokens: response?.usage?.prompt_tokens,\n outputTokens: response?.usage?.completion_tokens\n })\n}\n\n// ── 변환 유틸 ─────────────────────────────────────────────────────────\n\nfunction buildSDKMessages(messages: AIMessage[], externalSystem?: string): any[] {\n const out: any[] = []\n\n const systemFromMessages = messages\n .filter(m => m.role === 'system')\n .map(extractTextContent)\n .filter(Boolean)\n .join('\\n\\n')\n const combinedSystem = [externalSystem, systemFromMessages].filter(Boolean).join('\\n\\n')\n if (combinedSystem) {\n out.push({ role: 'system', content: combinedSystem })\n }\n\n for (const msg of messages) {\n if (msg.role === 'system') continue\n const sdkMsgs = toSDKMessages(msg)\n out.push(...sdkMsgs)\n }\n\n return out\n}\n\n/**\n * 한 AIMessage 가 다중 tool_result 를 가지면 OpenAI 측에서 여러 role:'tool' 메시지로\n * 분리해야 함 (OpenAI 는 메시지당 tool_call_id 하나만 매핑). 따라서 array 를 반환.\n */\nexport function toSDKMessages(msg: AIMessage): any[] {\n if (typeof msg.content === 'string') {\n return [{ role: msg.role, content: msg.content }]\n }\n\n const parts = msg.content\n const toolUseParts = parts.filter(p => p.type === 'tool_use') as any[]\n const toolResultParts = parts.filter(p => p.type === 'tool_result') as any[]\n const otherParts = parts.filter(p => p.type !== 'tool_use' && p.type !== 'tool_result')\n\n // tool_result 가 있으면 각각을 role='tool' 메시지로 분리 (OpenAI 요구사항).\n if (toolResultParts.length > 0) {\n const results = toolResultParts.map((r: any) => ({\n role: 'tool',\n tool_call_id: r.toolUseId,\n content: r.content\n }))\n /* A tool message holds text only - pictures a read tool returned follow as one user message (agentic-loop takeToolImages). */\n const images = otherParts.filter(p => p.type === 'image')\n return images.length ? [...results, { role: 'user', content: images.map(toSDKContentPart) }] : results\n }\n\n // assistant 측 tool_use 가 있으면 message.tool_calls 로\n const out: any = { role: msg.role }\n if (otherParts.length > 0) {\n out.content = otherParts.map(toSDKContentPart)\n } else {\n out.content = null\n }\n if (toolUseParts.length > 0) {\n out.tool_calls = toolUseParts.map(p => ({\n id: p.id,\n type: 'function',\n function: { name: p.name, arguments: JSON.stringify(p.arguments) }\n }))\n }\n return [out]\n}\n\nfunction toSDKContentPart(part: AIContentPart): any {\n if (part.type === 'text') {\n return { type: 'text', text: part.text }\n }\n if (part.type === 'tool_use' || part.type === 'tool_result') {\n // 처리는 toSDKMessage 가 함 — 도달하면 버그.\n return { type: 'text', text: '' }\n }\n // image\n if (part.source.kind === 'url') {\n return { type: 'image_url', image_url: { url: part.source.url } }\n }\n const b64 = Buffer.isBuffer(part.source.data)\n ? part.source.data.toString('base64')\n : Buffer.from(part.source.data).toString('base64')\n return {\n type: 'image_url',\n image_url: { url: `data:${part.source.mediaType};base64,${b64}` }\n }\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 buildOpenAIToolChoice(options: AIToolChatOptions): any {\n const choice = options.toolChoice ?? 'auto'\n if (choice === 'none') return 'none'\n if (choice === 'required') {\n if (options.forceTool) {\n return { type: 'function', function: { name: options.forceTool } }\n }\n return 'required'\n }\n return 'auto'\n}\n\nfunction normalizeOpenAIStopReason(raw: any): AIToolChatResult['stopReason'] {\n switch (raw) {\n case 'tool_calls':\n return 'tool_use'\n case 'function_call':\n return 'tool_use'\n case 'stop':\n return 'end_turn'\n case 'length':\n return 'max_tokens'\n case 'content_filter':\n return 'stop_sequence'\n default:\n return raw ?? 'end_turn'\n }\n}\n\n// Auto-register at module load\nregisterAIProvider('openai', config => new OpenAIAIClient(config))\n"]}
|
|
@@ -172,6 +172,15 @@ export interface AIClient {
|
|
|
172
172
|
* 어댑터별로 Anthropic tool_use / OpenAI tools / Gemini functionCalls 로 매핑.
|
|
173
173
|
*/
|
|
174
174
|
chatWithTools?(messages: AIMessage[], tools: AITool[], options?: AIToolChatOptions): Promise<AIToolChatResult>;
|
|
175
|
+
/** Optional speech recognition. The provider returns timestamped speech, never inferred video visuals. */
|
|
176
|
+
transcribe?(audio: Buffer, options?: AIChatOptions): Promise<{
|
|
177
|
+
text: string;
|
|
178
|
+
segments: {
|
|
179
|
+
start: number;
|
|
180
|
+
end: number;
|
|
181
|
+
text: string;
|
|
182
|
+
}[];
|
|
183
|
+
}>;
|
|
175
184
|
/** The ids of the conversation models this key may call, sorted. Reading it also proves the key is accepted. */
|
|
176
185
|
listModels(): Promise<string[]>;
|
|
177
186
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../server/service/types.ts"],"names":[],"mappings":";AAAA;;;;GAIG","sourcesContent":["/**\n * Provider-agnostic AI client types.\n *\n * 공통 인터페이스만 정의 — 실제 호출은 provider 별 어댑터에서.\n */\n\nexport type AIRole = 'system' | 'user' | 'assistant'\n\nexport interface AITextPart {\n type: 'text'\n text: string\n}\n\nexport type AIImageMediaType = 'image/png' | 'image/jpeg' | 'image/gif' | 'image/webp'\n\nexport interface AIImagePart {\n type: 'image'\n source:\n | { kind: 'base64'; data: Buffer | Uint8Array; mediaType: AIImageMediaType }\n | { kind: 'url'; url: string }\n}\n\n/**\n * Assistant 가 tool 을 호출하라고 한 시점의 content part.\n * 다음 턴에서 같은 id 로 tool_result 를 매핑한다.\n */\nexport interface AIToolUsePart {\n type: 'tool_use'\n /** provider-stable id — Anthropic tool_use.id / OpenAI tool_call.id / Gemini 의 자체 매핑 */\n id: string\n /** tool 이름 */\n name: string\n /** 호출 인자 (JSON object) */\n arguments: Record<string, any>\n /**\n * provider-specific opaque metadata that must be echoed back when this tool_use\n * is referenced in subsequent turns.\n *\n * Gemini: `thoughtSignature` — 안 보내면 400 에러.\n * Anthropic: 없음.\n * OpenAI: 없음.\n *\n * 호출자(assistant loop)는 AIToolCall.providerMeta 를 그대로 복사해 넣는다.\n */\n providerMeta?: Record<string, any>\n}\n\n/**\n * 도구 실행 결과를 LLM 에 회신하는 user 측 content part.\n *\n * Anthropic: { type: 'tool_result', tool_use_id, content } — user role 에 들어감.\n * OpenAI: tool role 메시지로 전송.\n * Gemini: functionResponse part — user role 에 들어감.\n */\nexport interface AIToolResultPart {\n type: 'tool_result'\n /** 매핑되는 AIToolUsePart.id */\n toolUseId: string\n /** 도구 실행 결과 (text 또는 JSON-serializable) */\n content: string\n /** 실행 중 에러 발생 표시 (LLM 이 retry/대안 판단) */\n isError?: boolean\n}\n\nexport type AIContentPart = AITextPart | AIImagePart | AIToolUsePart | AIToolResultPart\n\nexport interface AIMessage {\n role: AIRole\n content: string | AIContentPart[]\n}\n\nexport interface AIChatOptions {\n /** 모델 override (없으면 client 기본값) */\n model?: string\n maxTokens?: number\n temperature?: number\n /**\n * system prompt — messages 의 system 메시지와 합쳐서 전달.\n * Anthropic 의 system 파라미터 / OpenAI 의 system 메시지로 매핑.\n */\n systemPrompt?: string\n stopSequences?: string[]\n /**\n * Told what one provider call used, right after it returns. The metering boundary (aiClientFor) sets this per\n * call so the usage row knows whose call it was; adapters call it once for every request they send.\n */\n onUsage?: (usage: AIProviderUsage) => void\n /**\n * Stops waiting for the request - a person's stop, or an unattended run's time limit. Every adapter hands it to its\n * SDK. The provider may still complete and bill a request it already received.\n */\n signal?: AbortSignal\n}\n\n/** What one provider request used, as the provider reported it. A count the provider did not report stays undefined. */\nexport interface AIProviderUsage {\n model: string\n inputTokens?: number\n outputTokens?: number\n}\n\nexport interface AIJSONOptions extends AIChatOptions {\n /** JSON Schema (옵션). 어댑터가 지원하면 structured output 강제 */\n schema?: object\n}\n\nexport interface AIVisionOptions extends AIChatOptions {}\n\n// ── Tool calling ──────────────────────────────────────────────────\n\n/**\n * Provider-agnostic 도구 정의.\n * Anthropic input_schema / OpenAI parameters / Gemini parameters 로 어댑터에서 정규화.\n */\nexport interface AITool {\n name: string\n description: string\n /** JSON Schema (object 권장). enum / required / properties 등 표준 키 사용. */\n parameters: Record<string, any>\n}\n\n/**\n * LLM 이 호출하라고 지시한 도구 1 건.\n * id 는 multi-turn 흐름에서 tool_result 매핑에 사용 — 어댑터가 항상 채워야 한다.\n */\nexport interface AIToolCall {\n id: string\n name: string\n arguments: Record<string, any>\n /**\n * provider-specific opaque metadata to round-trip on subsequent turns.\n * Gemini 는 `thoughtSignature` 를 여기에 담는다 — 다음 턴에 functionCall.thoughtSignature\n * 로 복원되어야 400 에러 회피.\n */\n providerMeta?: Record<string, any>\n}\n\nexport interface AIToolChatOptions extends AIChatOptions {\n /**\n * tool 선택 모드:\n * - 'auto': LLM 자율 (tool 호출 또는 텍스트 응답)\n * - 'required': 반드시 하나 이상 tool 호출\n * - 'none': tool 무시, 텍스트만\n */\n toolChoice?: 'auto' | 'required' | 'none'\n /** 'required' 와 함께 특정 tool 강제. provider 별 지원 다름. */\n forceTool?: string\n /**\n * 병렬 tool 호출 허용 여부 (default true).\n * false 시 LLM 이 한 번에 하나의 tool 만 호출.\n */\n allowParallelToolCalls?: boolean\n}\n\nexport interface AIToolChatResult {\n /** 사용자에게 보여줄 텍스트 (tool 호출과 함께 동봉될 수도 있음) */\n text?: string\n /** LLM 이 호출하라고 지시한 도구들 (병렬 가능) */\n toolCalls: AIToolCall[]\n /**\n * 정규화된 stop reason:\n * 'tool_use' — tool 호출 발생\n * 'end_turn' — 텍스트 응답 종료\n * 'max_tokens' — 토큰 한도\n * 'stop_sequence' — stop sequence 도달\n * 기타 — provider 원본 그대로\n */\n stopReason: 'tool_use' | 'end_turn' | 'max_tokens' | 'stop_sequence' | string\n}\n\n// ── Client ────────────────────────────────────────────────────────\n\nexport interface AIClient {\n /** 'anthropic:claude-sonnet-4-6' 등 사람이 읽을 식별자 */\n readonly id: string\n\n /** 텍스트 chat completion */\n chat(messages: AIMessage[], options?: AIChatOptions): Promise<string>\n\n /** JSON 출력 강제. 파싱된 객체 반환. */\n generateJSON<T = unknown>(prompt: string | AIMessage[], options?: AIJSONOptions): Promise<T>\n\n /** 이미지 입력 — 텍스트 응답. 어댑터가 vision 모델을 지원해야 함. */\n vision?(\n image: Buffer | Uint8Array | { url: string },\n prompt: string,\n options?: AIVisionOptions\n ): Promise<string>\n\n /**\n * Tool calling — LLM 이 자유 텍스트 대신 정의된 도구를 schema-검증된 인자로 호출.\n * 각 도구의 parameters 가 enum/required 강제 → invalid type / 누락 필드 봉쇄.\n * 어댑터별로 Anthropic tool_use / OpenAI tools / Gemini functionCalls 로 매핑.\n */\n chatWithTools?(\n messages: AIMessage[],\n tools: AITool[],\n options?: AIToolChatOptions\n ): Promise<AIToolChatResult>\n\n /** The ids of the conversation models this key may call, sorted. Reading it also proves the key is accepted. */\n listModels(): Promise<string[]>\n}\n\nexport interface AIClientConfig {\n /** 등록된 provider 이름 (예: 'anthropic', 'openai', 'gemini') */\n provider: string\n /** The model every call uses unless a call names another. */\n model?: string\n /** Required - a provider is never reached with a key from the process environment. */\n apiKey: string\n /** OpenAI organization (provider:'openai' 전용) */\n organization?: string\n baseURL?: string\n [k: string]: any\n}\n\nexport type AIProviderFactory = (config: AIClientConfig) => AIClient\n"]}
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../server/service/types.ts"],"names":[],"mappings":";AAAA;;;;GAIG","sourcesContent":["/**\n * Provider-agnostic AI client types.\n *\n * 공통 인터페이스만 정의 — 실제 호출은 provider 별 어댑터에서.\n */\n\nexport type AIRole = 'system' | 'user' | 'assistant'\n\nexport interface AITextPart {\n type: 'text'\n text: string\n}\n\nexport type AIImageMediaType = 'image/png' | 'image/jpeg' | 'image/gif' | 'image/webp'\n\nexport interface AIImagePart {\n type: 'image'\n source:\n | { kind: 'base64'; data: Buffer | Uint8Array; mediaType: AIImageMediaType }\n | { kind: 'url'; url: string }\n}\n\n/**\n * Assistant 가 tool 을 호출하라고 한 시점의 content part.\n * 다음 턴에서 같은 id 로 tool_result 를 매핑한다.\n */\nexport interface AIToolUsePart {\n type: 'tool_use'\n /** provider-stable id — Anthropic tool_use.id / OpenAI tool_call.id / Gemini 의 자체 매핑 */\n id: string\n /** tool 이름 */\n name: string\n /** 호출 인자 (JSON object) */\n arguments: Record<string, any>\n /**\n * provider-specific opaque metadata that must be echoed back when this tool_use\n * is referenced in subsequent turns.\n *\n * Gemini: `thoughtSignature` — 안 보내면 400 에러.\n * Anthropic: 없음.\n * OpenAI: 없음.\n *\n * 호출자(assistant loop)는 AIToolCall.providerMeta 를 그대로 복사해 넣는다.\n */\n providerMeta?: Record<string, any>\n}\n\n/**\n * 도구 실행 결과를 LLM 에 회신하는 user 측 content part.\n *\n * Anthropic: { type: 'tool_result', tool_use_id, content } — user role 에 들어감.\n * OpenAI: tool role 메시지로 전송.\n * Gemini: functionResponse part — user role 에 들어감.\n */\nexport interface AIToolResultPart {\n type: 'tool_result'\n /** 매핑되는 AIToolUsePart.id */\n toolUseId: string\n /** 도구 실행 결과 (text 또는 JSON-serializable) */\n content: string\n /** 실행 중 에러 발생 표시 (LLM 이 retry/대안 판단) */\n isError?: boolean\n}\n\nexport type AIContentPart = AITextPart | AIImagePart | AIToolUsePart | AIToolResultPart\n\nexport interface AIMessage {\n role: AIRole\n content: string | AIContentPart[]\n}\n\nexport interface AIChatOptions {\n /** 모델 override (없으면 client 기본값) */\n model?: string\n maxTokens?: number\n temperature?: number\n /**\n * system prompt — messages 의 system 메시지와 합쳐서 전달.\n * Anthropic 의 system 파라미터 / OpenAI 의 system 메시지로 매핑.\n */\n systemPrompt?: string\n stopSequences?: string[]\n /**\n * Told what one provider call used, right after it returns. The metering boundary (aiClientFor) sets this per\n * call so the usage row knows whose call it was; adapters call it once for every request they send.\n */\n onUsage?: (usage: AIProviderUsage) => void\n /**\n * Stops waiting for the request - a person's stop, or an unattended run's time limit. Every adapter hands it to its\n * SDK. The provider may still complete and bill a request it already received.\n */\n signal?: AbortSignal\n}\n\n/** What one provider request used, as the provider reported it. A count the provider did not report stays undefined. */\nexport interface AIProviderUsage {\n model: string\n inputTokens?: number\n outputTokens?: number\n}\n\nexport interface AIJSONOptions extends AIChatOptions {\n /** JSON Schema (옵션). 어댑터가 지원하면 structured output 강제 */\n schema?: object\n}\n\nexport interface AIVisionOptions extends AIChatOptions {}\n\n// ── Tool calling ──────────────────────────────────────────────────\n\n/**\n * Provider-agnostic 도구 정의.\n * Anthropic input_schema / OpenAI parameters / Gemini parameters 로 어댑터에서 정규화.\n */\nexport interface AITool {\n name: string\n description: string\n /** JSON Schema (object 권장). enum / required / properties 등 표준 키 사용. */\n parameters: Record<string, any>\n}\n\n/**\n * LLM 이 호출하라고 지시한 도구 1 건.\n * id 는 multi-turn 흐름에서 tool_result 매핑에 사용 — 어댑터가 항상 채워야 한다.\n */\nexport interface AIToolCall {\n id: string\n name: string\n arguments: Record<string, any>\n /**\n * provider-specific opaque metadata to round-trip on subsequent turns.\n * Gemini 는 `thoughtSignature` 를 여기에 담는다 — 다음 턴에 functionCall.thoughtSignature\n * 로 복원되어야 400 에러 회피.\n */\n providerMeta?: Record<string, any>\n}\n\nexport interface AIToolChatOptions extends AIChatOptions {\n /**\n * tool 선택 모드:\n * - 'auto': LLM 자율 (tool 호출 또는 텍스트 응답)\n * - 'required': 반드시 하나 이상 tool 호출\n * - 'none': tool 무시, 텍스트만\n */\n toolChoice?: 'auto' | 'required' | 'none'\n /** 'required' 와 함께 특정 tool 강제. provider 별 지원 다름. */\n forceTool?: string\n /**\n * 병렬 tool 호출 허용 여부 (default true).\n * false 시 LLM 이 한 번에 하나의 tool 만 호출.\n */\n allowParallelToolCalls?: boolean\n}\n\nexport interface AIToolChatResult {\n /** 사용자에게 보여줄 텍스트 (tool 호출과 함께 동봉될 수도 있음) */\n text?: string\n /** LLM 이 호출하라고 지시한 도구들 (병렬 가능) */\n toolCalls: AIToolCall[]\n /**\n * 정규화된 stop reason:\n * 'tool_use' — tool 호출 발생\n * 'end_turn' — 텍스트 응답 종료\n * 'max_tokens' — 토큰 한도\n * 'stop_sequence' — stop sequence 도달\n * 기타 — provider 원본 그대로\n */\n stopReason: 'tool_use' | 'end_turn' | 'max_tokens' | 'stop_sequence' | string\n}\n\n// ── Client ────────────────────────────────────────────────────────\n\nexport interface AIClient {\n /** 'anthropic:claude-sonnet-4-6' 등 사람이 읽을 식별자 */\n readonly id: string\n\n /** 텍스트 chat completion */\n chat(messages: AIMessage[], options?: AIChatOptions): Promise<string>\n\n /** JSON 출력 강제. 파싱된 객체 반환. */\n generateJSON<T = unknown>(prompt: string | AIMessage[], options?: AIJSONOptions): Promise<T>\n\n /** 이미지 입력 — 텍스트 응답. 어댑터가 vision 모델을 지원해야 함. */\n vision?(\n image: Buffer | Uint8Array | { url: string },\n prompt: string,\n options?: AIVisionOptions\n ): Promise<string>\n\n /**\n * Tool calling — LLM 이 자유 텍스트 대신 정의된 도구를 schema-검증된 인자로 호출.\n * 각 도구의 parameters 가 enum/required 강제 → invalid type / 누락 필드 봉쇄.\n * 어댑터별로 Anthropic tool_use / OpenAI tools / Gemini functionCalls 로 매핑.\n */\n chatWithTools?(\n messages: AIMessage[],\n tools: AITool[],\n options?: AIToolChatOptions\n ): Promise<AIToolChatResult>\n\n /** Optional speech recognition. The provider returns timestamped speech, never inferred video visuals. */\n transcribe?(audio: Buffer, options?: AIChatOptions): Promise<{ text: string; segments: { start: number; end: number; text: string }[] }>\n\n /** The ids of the conversation models this key may call, sorted. Reading it also proves the key is accepted. */\n listModels(): Promise<string[]>\n}\n\nexport interface AIClientConfig {\n /** 등록된 provider 이름 (예: 'anthropic', 'openai', 'gemini') */\n provider: string\n /** The model every call uses unless a call names another. */\n model?: string\n /** Required - a provider is never reached with a key from the process environment. */\n apiKey: string\n /** OpenAI organization (provider:'openai' 전용) */\n organization?: string\n baseURL?: string\n [k: string]: any\n}\n\nexport type AIProviderFactory = (config: AIClientConfig) => AIClient\n"]}
|