@things-factory/ai-client-base 10.1.245 → 10.1.246
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.d.ts +5 -0
- package/dist-server/service/agent/ai-client-for.js +15 -2
- package/dist-server/service/agent/ai-client-for.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 +20 -2
- package/server/service/agent/ai-client-for.ts +14 -1
|
@@ -33,6 +33,11 @@ export interface AgentClient extends AIClient {
|
|
|
33
33
|
model: string;
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Once every usage row asked for so far is written (or failed and said so). For a process that closes its database
|
|
38
|
+
* after its last call -- a script running scenarios -- so the last rows are not lost to a closed handle.
|
|
39
|
+
*/
|
|
40
|
+
export declare function aiUsageRecorded(): Promise<void>;
|
|
36
41
|
export declare function aiClientFor(request: AiClientRequest): Promise<AgentClient>;
|
|
37
42
|
/**
|
|
38
43
|
* The agent a request is answered by. A named agent that this domain cannot use is refused, never swapped for
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.aiUsageRecorded = aiUsageRecorded;
|
|
3
4
|
exports.aiClientFor = aiClientFor;
|
|
4
5
|
exports.pickAgent = pickAgent;
|
|
5
6
|
exports.hourOf = hourOf;
|
|
@@ -9,6 +10,16 @@ exports.setAIClientForTesting = setAIClientForTesting;
|
|
|
9
10
|
const shell_1 = require("@things-factory/shell");
|
|
10
11
|
const factory_js_1 = require("../factory.js");
|
|
11
12
|
const agent_directory_js_1 = require("./agent-directory.js");
|
|
13
|
+
/* The usage rows still being written: a request's answer does not wait for its row. */
|
|
14
|
+
const recording = new Set();
|
|
15
|
+
/**
|
|
16
|
+
* Once every usage row asked for so far is written (or failed and said so). For a process that closes its database
|
|
17
|
+
* after its last call -- a script running scenarios -- so the last rows are not lost to a closed handle.
|
|
18
|
+
*/
|
|
19
|
+
async function aiUsageRecorded() {
|
|
20
|
+
while (recording.size)
|
|
21
|
+
await Promise.allSettled([...recording]);
|
|
22
|
+
}
|
|
12
23
|
async function aiClientFor(request) {
|
|
13
24
|
if (_testing)
|
|
14
25
|
return _testing(request);
|
|
@@ -102,9 +113,11 @@ function meter(client, entry, request) {
|
|
|
102
113
|
}));
|
|
103
114
|
/* Outside the caller's transaction: a rolled-back write still spent the tokens. By name, so the decorated entity
|
|
104
115
|
stays out of this module's imports. */
|
|
105
|
-
(0, shell_1.getRepository)('AiUsage')
|
|
116
|
+
const written = (0, shell_1.getRepository)('AiUsage')
|
|
106
117
|
.save(rows)
|
|
107
|
-
.catch(error => console.error('[ai-client-base] could not record AI usage:', error?.message ?? error))
|
|
118
|
+
.catch(error => console.error('[ai-client-base] could not record AI usage:', error?.message ?? error))
|
|
119
|
+
.finally(() => recording.delete(written));
|
|
120
|
+
recording.add(written);
|
|
108
121
|
}
|
|
109
122
|
const metered = {
|
|
110
123
|
id: `${spec.provider}:${spec.model}`,
|
|
@@ -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\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 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 }\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.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"]}
|