@things-factory/ai-client-base 10.1.259 → 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/ai-client-base",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.261",
|
|
4
4
|
"main": "dist-server/index.js",
|
|
5
5
|
"things-factory": true,
|
|
6
6
|
"author": "heartyoh",
|
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@anthropic-ai/sdk": "^0.40.0",
|
|
26
26
|
"@google/generative-ai": "^0.24.0",
|
|
27
|
-
"@things-factory/auth-base": "^10.1.
|
|
27
|
+
"@things-factory/auth-base": "^10.1.261",
|
|
28
28
|
"@things-factory/env": "^10.1.149",
|
|
29
|
-
"@things-factory/setting-base": "^10.1.
|
|
30
|
-
"@things-factory/shell": "^10.1.
|
|
29
|
+
"@things-factory/setting-base": "^10.1.261",
|
|
30
|
+
"@things-factory/shell": "^10.1.261",
|
|
31
31
|
"openai": "^4.0.0"
|
|
32
32
|
},
|
|
33
|
-
"gitHead": "
|
|
33
|
+
"gitHead": "06537757f8708e6a511c6232e8792f18fdd6a986"
|
|
34
34
|
}
|
|
@@ -48,6 +48,7 @@ registerAIProvider('fake', config => ({
|
|
|
48
48
|
options.onUsage?.({ model: 'fake-1-0607', inputTokens: 300, outputTokens: 5 })
|
|
49
49
|
return { toolCalls: [], stopReason: 'end_turn' }
|
|
50
50
|
},
|
|
51
|
+
transcribe: async (_audio, options: any) => { options.onUsage?.({ model: 'fake-speech' }); return { text: 'spoken', segments: [{ start: 0, end: 1, text: 'spoken' }] } },
|
|
51
52
|
listModels: async () => ['fake-1']
|
|
52
53
|
}))
|
|
53
54
|
|
|
@@ -157,3 +158,13 @@ it('a failure is named by what the provider said', () => {
|
|
|
157
158
|
it('a call is grouped by the UTC hour it started in', () => {
|
|
158
159
|
expect(hourOf(new Date('2026-10-12T05:47:13.120Z')).toISOString()).toBe('2026-10-12T05:00:00.000Z')
|
|
159
160
|
})
|
|
161
|
+
|
|
162
|
+
it('speech transcription uses the same tenant usage boundary without fabricated token counts', async () => {
|
|
163
|
+
;(readAiAgentDirectory as jest.Mock).mockResolvedValue({ entries: [entry('speech')], defaultAgentId: 'speech' })
|
|
164
|
+
const client = await aiClientFor({ domain: { id: 'office' } as any, user: { id: 'manager' }, modeKind: 'learning.material-transcription' })
|
|
165
|
+
expect(await client.transcribe!(Buffer.from('audio'))).toEqual({ text: 'spoken', segments: [{ start: 0, end: 1, text: 'spoken' }] })
|
|
166
|
+
await aiUsageRecorded()
|
|
167
|
+
expect(saved).toHaveLength(1)
|
|
168
|
+
expect(saved[0]).toMatchObject({ operation: 'transcribe', model: 'fake-speech', domain: { id: 'office' }, creator: { id: 'manager' } })
|
|
169
|
+
expect(saved[0].inputTokens).toBeUndefined()
|
|
170
|
+
})
|
|
@@ -156,6 +156,7 @@ function meter(client: AIClient, entry: AiAgentEntry, request: AiClientRequest):
|
|
|
156
156
|
agent: { id: spec.id, name: spec.name, provider: spec.provider, model: spec.model },
|
|
157
157
|
chat: (messages, options) => measured('chat', options, o => client.chat(messages, o)),
|
|
158
158
|
generateJSON: (prompt, options) => measured('generateJSON', options, o => client.generateJSON(prompt, o)),
|
|
159
|
+
...(client.transcribe && { transcribe: (audio, options) => measured('transcribe', options, o => client.transcribe!(audio, o)) }),
|
|
159
160
|
...(client.vision && {
|
|
160
161
|
vision: (image, prompt, options) => measured('vision', options, o => client.vision!(image, prompt, o))
|
|
161
162
|
}),
|
|
@@ -52,7 +52,7 @@ export class AiUsage {
|
|
|
52
52
|
modeKind?: string
|
|
53
53
|
|
|
54
54
|
@Column()
|
|
55
|
-
@Field(type => String, { description: 'chat, chatWithTools, generateJSON or
|
|
55
|
+
@Field(type => String, { description: 'chat, chatWithTools, generateJSON, vision or transcribe.' })
|
|
56
56
|
operation: string
|
|
57
57
|
|
|
58
58
|
@Column({ nullable: true })
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* organization 필드 지원.
|
|
8
8
|
*/
|
|
9
9
|
import OpenAI from 'openai'
|
|
10
|
+
import { toFile } from 'openai/uploads'
|
|
10
11
|
import type {
|
|
11
12
|
AIChatOptions,
|
|
12
13
|
AIClient,
|
|
@@ -42,6 +43,17 @@ export class OpenAIAIClient implements AIClient {
|
|
|
42
43
|
this.id = `openai:${this.defaultModel}`
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
/** Whisper accepts the small MP3 prepared by the material reader and returns speech timestamps. */
|
|
47
|
+
async transcribe(audio: Buffer, options: AIChatOptions = {}) {
|
|
48
|
+
const model = 'whisper-1'
|
|
49
|
+
const answer = await this.sdk.audio.transcriptions.create({
|
|
50
|
+
file: await toFile(audio, 'speech.mp3', { type: 'audio/mpeg' }),
|
|
51
|
+
model, response_format: 'verbose_json', timestamp_granularities: ['segment']
|
|
52
|
+
}, requestOptionsOf(options))
|
|
53
|
+
options.onUsage?.({ model }) // Speech billing does not report text token counts.
|
|
54
|
+
return { text: answer.text, segments: (answer.segments ?? []).map(s => ({ start: s.start, end: s.end, text: s.text })) }
|
|
55
|
+
}
|
|
56
|
+
|
|
45
57
|
async chat(messages: AIMessage[], options: AIChatOptions = {}): Promise<string> {
|
|
46
58
|
const sdkMessages = buildSDKMessages(messages, options.systemPrompt)
|
|
47
59
|
const response = await this.sdk.chat.completions.create({
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { OpenAIAIClient } from './openai'
|
|
2
|
+
|
|
3
|
+
it('transcribes speech with timestamps, cancellation and usage reporting through the configured SDK', async () => {
|
|
4
|
+
const client: any = new OpenAIAIClient({ provider: 'openai', apiKey: 'test-only' })
|
|
5
|
+
const calls: any[] = []
|
|
6
|
+
client.sdk = { audio: { transcriptions: { create: async (body: any, request: any) => {
|
|
7
|
+
calls.push({ body, request })
|
|
8
|
+
return { text: 'Hello', segments: [{ start: 0, end: 1.5, text: 'Hello' }] }
|
|
9
|
+
} } } }
|
|
10
|
+
const usage: any[] = []
|
|
11
|
+
const { signal } = new AbortController()
|
|
12
|
+
const answer = await client.transcribe(Buffer.from('test audio'), { signal, onUsage: (u: any) => usage.push(u) })
|
|
13
|
+
expect(answer).toEqual({ text: 'Hello', segments: [{ start: 0, end: 1.5, text: 'Hello' }] })
|
|
14
|
+
expect(calls[0].body.model).toBe('whisper-1')
|
|
15
|
+
expect(calls[0].body.response_format).toBe('verbose_json')
|
|
16
|
+
expect(calls[0].request.signal).toBe(signal)
|
|
17
|
+
expect(usage).toEqual([{ model: 'whisper-1' }])
|
|
18
|
+
})
|
package/server/service/types.ts
CHANGED
|
@@ -198,6 +198,9 @@ export interface AIClient {
|
|
|
198
198
|
options?: AIToolChatOptions
|
|
199
199
|
): Promise<AIToolChatResult>
|
|
200
200
|
|
|
201
|
+
/** Optional speech recognition. The provider returns timestamped speech, never inferred video visuals. */
|
|
202
|
+
transcribe?(audio: Buffer, options?: AIChatOptions): Promise<{ text: string; segments: { start: number; end: number; text: string }[] }>
|
|
203
|
+
|
|
201
204
|
/** The ids of the conversation models this key may call, sorted. Reading it also proves the key is accepted. */
|
|
202
205
|
listModels(): Promise<string[]>
|
|
203
206
|
}
|