@startsimpli/llm 0.4.27 → 0.4.28
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/README.md +2 -3
- package/package.json +1 -1
- package/src/__tests__/speech-cost.test.ts +12 -14
- package/src/__tests__/speech.test.ts +1 -1
- package/src/speech/index.ts +0 -1
- package/src/speech/providers/openai.ts +2 -2
- package/src/speech/service.ts +1 -3
- package/src/speech/providers/groq.ts +0 -35
package/README.md
CHANGED
|
@@ -196,7 +196,6 @@ const { text, words } = await speech.transcribe({
|
|
|
196
196
|
|
|
197
197
|
| Env | Provider | List price |
|
|
198
198
|
| --- | --- | --- |
|
|
199
|
-
| `GROQ_API_KEY` | Groq `whisper-large-v3-turbo` | $0.04 / audio hour |
|
|
200
199
|
| `OPENAI_API_KEY` | OpenAI `gpt-4o-mini-transcribe` (default; `gpt-4o-transcribe` / `whisper-1` cost 2x) | $0.18 / hour |
|
|
201
200
|
| `ELEVENLABS_API_KEY` | ElevenLabs Scribe (keyterm hints are a billed add-on, so off unless `sendKeyterms`) | $0.22 / hour |
|
|
202
201
|
| `SPEECH_PROVIDER=mock` | deterministic, for tests | — |
|
|
@@ -206,8 +205,8 @@ Retryable errors back off and retry; a failure that survives falls through to th
|
|
|
206
205
|
vendor (`fallback: false` to pin one). Bad audio (`INVALID_REQUEST`) never falls through.
|
|
207
206
|
|
|
208
207
|
**Add a vendor** — implement `ISpeechProvider` (state `costPerMinuteUsd`) and register it, no
|
|
209
|
-
service edit. An OpenAI-compatible host is
|
|
210
|
-
|
|
208
|
+
service edit. An OpenAI-compatible host is just `OpenAISpeechProvider` with a different
|
|
209
|
+
`name` / `baseUrl` / `defaultModel` / `prices`:
|
|
211
210
|
|
|
212
211
|
```ts
|
|
213
212
|
registerSpeechProvider('mine', () => (process.env.MINE_KEY ? new MyProvider(process.env.MINE_KEY) : null));
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
ElevenLabsSpeechProvider,
|
|
4
|
+
ElevenLabsSpeechProvider, OpenAISpeechProvider, SpeechService,
|
|
5
5
|
} from '../speech';
|
|
6
6
|
import type { ISpeechProvider, TranscribeRequest } from '../speech';
|
|
7
7
|
|
|
@@ -19,23 +19,21 @@ describe('what each vendor costs per audio minute (USD, list price)', () => {
|
|
|
19
19
|
it('elevenlabs Scribe is $0.22/hour', () => {
|
|
20
20
|
expect(new ElevenLabsSpeechProvider({ apiKey: 'k' }).costPerMinuteUsd()).toBeCloseTo(0.22 / 60);
|
|
21
21
|
});
|
|
22
|
-
it('groq whisper-large-v3-turbo is $0.04/hour', () => {
|
|
23
|
-
expect(new GroqSpeechProvider({ apiKey: 'k' }).costPerMinuteUsd()).toBeCloseTo(0.04 / 60);
|
|
24
|
-
});
|
|
25
22
|
});
|
|
26
23
|
|
|
27
|
-
describe('
|
|
28
|
-
it('
|
|
29
|
-
expect(createGroqSpeechProvider({})).toBeNull();
|
|
30
|
-
expect(createGroqSpeechProvider({ GROQ_API_KEY: 'g' })?.name).toBe('groq');
|
|
31
|
-
});
|
|
32
|
-
it('posts to Groq with its own default model and reports itself as groq', async () => {
|
|
24
|
+
describe('an OpenAI-compatible host is the OpenAI provider with different options', () => {
|
|
25
|
+
it('reports its own name, model and price and posts to its own base URL', async () => {
|
|
33
26
|
const f = okJson({ text: ' hi ' });
|
|
34
|
-
const
|
|
27
|
+
const p = new OpenAISpeechProvider({
|
|
28
|
+
apiKey: 'k', name: 'selfhosted', baseUrl: 'https://stt.example.com/openai', defaultModel: 'w',
|
|
29
|
+
prices: { w: 0.001 }, fetchImpl: f as never,
|
|
30
|
+
});
|
|
31
|
+
const out = await p.transcribe(req);
|
|
35
32
|
const [url, init] = f.mock.calls[0] as unknown as [string, RequestInit];
|
|
36
|
-
expect(url).toBe('https://
|
|
37
|
-
expect((init.body as FormData).get('model')).toBe('
|
|
38
|
-
expect(out).toMatchObject({ text: 'hi', provider: '
|
|
33
|
+
expect(url).toBe('https://stt.example.com/openai/v1/audio/transcriptions');
|
|
34
|
+
expect((init.body as FormData).get('model')).toBe('w');
|
|
35
|
+
expect(out).toMatchObject({ text: 'hi', provider: 'selfhosted' });
|
|
36
|
+
expect(p.costPerMinuteUsd()).toBeCloseTo(0.001);
|
|
39
37
|
});
|
|
40
38
|
});
|
|
41
39
|
|
|
@@ -153,7 +153,7 @@ describe('SpeechService', () => {
|
|
|
153
153
|
});
|
|
154
154
|
it('says what to set when nothing is configured', async () => {
|
|
155
155
|
const s = withProviders(new SpeechService());
|
|
156
|
-
await expect(s.transcribe(req)).rejects.toThrow(/
|
|
156
|
+
await expect(s.transcribe(req)).rejects.toThrow(/OPENAI_API_KEY or ELEVENLABS_API_KEY/);
|
|
157
157
|
expect(s.isAvailable()).toBe(false);
|
|
158
158
|
});
|
|
159
159
|
it('SPEECH_LANGUAGE is a default, and a per-call language wins', async () => {
|
package/src/speech/index.ts
CHANGED
|
@@ -14,6 +14,5 @@ export type {
|
|
|
14
14
|
export { ElevenLabsSpeechProvider, createElevenLabsSpeechProvider } from './providers/elevenlabs';
|
|
15
15
|
export { OpenAISpeechProvider, createOpenAISpeechProvider } from './providers/openai';
|
|
16
16
|
export type { OpenAISpeechOptions } from './providers/openai';
|
|
17
|
-
export { GroqSpeechProvider, createGroqSpeechProvider } from './providers/groq';
|
|
18
17
|
export { MockSpeechProvider, setMockSpeechBehavior, resetMockSpeechBehavior } from './providers/mock';
|
|
19
18
|
export { audioExtension, speechHttpError } from './http';
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* and equally accurate on our benchmark. Per-word timings are only offered by
|
|
6
6
|
* `whisper-1` (verbose_json), so asking for `timestamps: 'word'` switches model.
|
|
7
7
|
*
|
|
8
|
-
* ANY OpenAI-COMPATIBLE ENDPOINT IS THIS CLASS WITH DIFFERENT OPTIONS
|
|
9
|
-
*
|
|
8
|
+
* ANY OpenAI-COMPATIBLE ENDPOINT IS THIS CLASS WITH DIFFERENT OPTIONS: name, base
|
|
9
|
+
* URL, default model, word-timing model and price table.
|
|
10
10
|
*/
|
|
11
11
|
import type { ISpeechProvider } from '../interface';
|
|
12
12
|
import { assertAudio, audioBlob, speechFetch } from '../http';
|
package/src/speech/service.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { LLMProviderError } from '../errors';
|
|
|
2
2
|
import { ProviderRegistry } from '../providers/registry';
|
|
3
3
|
import type { ISpeechProvider } from './interface';
|
|
4
4
|
import { createElevenLabsSpeechProvider } from './providers/elevenlabs';
|
|
5
|
-
import { createGroqSpeechProvider } from './providers/groq';
|
|
6
5
|
import { MockSpeechProvider } from './providers/mock';
|
|
7
6
|
import { createOpenAISpeechProvider } from './providers/openai';
|
|
8
7
|
import type { SpeechServiceConfig, TranscribeRequest, TranscriptResult } from './types';
|
|
@@ -12,7 +11,6 @@ import type { SpeechServiceConfig, TranscribeRequest, TranscriptResult } from '.
|
|
|
12
11
|
* `registerSpeechProvider('name', factory)` — no edit to `SpeechService`.
|
|
13
12
|
*/
|
|
14
13
|
export const speechProviders = new ProviderRegistry<ISpeechProvider>()
|
|
15
|
-
.register('groq', () => createGroqSpeechProvider())
|
|
16
14
|
.register('openai', () => createOpenAISpeechProvider())
|
|
17
15
|
.register('elevenlabs', () => createElevenLabsSpeechProvider());
|
|
18
16
|
|
|
@@ -76,7 +74,7 @@ export class SpeechService {
|
|
|
76
74
|
const order = this.order();
|
|
77
75
|
if (!order.length) {
|
|
78
76
|
throw new LLMProviderError(
|
|
79
|
-
'No speech providers available. Set
|
|
77
|
+
'No speech providers available. Set OPENAI_API_KEY or ELEVENLABS_API_KEY.',
|
|
80
78
|
'AUTHENTICATION_ERROR', false,
|
|
81
79
|
);
|
|
82
80
|
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Groq-hosted Whisper — the cheapest option we benchmarked ($0.04 per audio hour,
|
|
3
|
-
* about a fifth of ElevenLabs Scribe). Groq speaks the OpenAI transcription API,
|
|
4
|
-
* so this is the OpenAI provider pointed at a different host: that is the whole
|
|
5
|
-
* file, and the template for any other OpenAI-compatible vendor.
|
|
6
|
-
* Env: GROQ_API_KEY.
|
|
7
|
-
*/
|
|
8
|
-
import { OpenAISpeechProvider } from './openai';
|
|
9
|
-
import type { OpenAISpeechOptions } from './openai';
|
|
10
|
-
|
|
11
|
-
const GROQ_PRICES: Record<string, number> = {
|
|
12
|
-
'whisper-large-v3-turbo': 0.04 / 60,
|
|
13
|
-
'whisper-large-v3': 0.111 / 60,
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export class GroqSpeechProvider extends OpenAISpeechProvider {
|
|
17
|
-
constructor(options: Pick<OpenAISpeechOptions, 'apiKey' | 'fetchImpl' | 'baseUrl'> = {}) {
|
|
18
|
-
super({
|
|
19
|
-
...options,
|
|
20
|
-
name: 'groq',
|
|
21
|
-
baseUrl: options.baseUrl ?? 'https://api.groq.com/openai',
|
|
22
|
-
defaultModel: 'whisper-large-v3-turbo',
|
|
23
|
-
// Groq's Whisper models return word timings from verbose_json themselves.
|
|
24
|
-
wordTimestampModel: 'whisper-large-v3-turbo',
|
|
25
|
-
prices: GROQ_PRICES,
|
|
26
|
-
fallbackCostPerMinuteUsd: 0.111 / 60,
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function createGroqSpeechProvider(
|
|
32
|
-
env: Record<string, string | undefined> = typeof process !== 'undefined' ? process.env : {},
|
|
33
|
-
): GroqSpeechProvider | null {
|
|
34
|
-
return env.GROQ_API_KEY ? new GroqSpeechProvider({ apiKey: env.GROQ_API_KEY }) : null;
|
|
35
|
-
}
|