@startsimpli/llm 0.4.27 → 0.4.29
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/LICENSE +21 -0
- package/README.md +8 -3
- package/package.json +14 -3
- 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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 StartSimpli
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
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));
|
|
@@ -215,3 +214,9 @@ registerSpeechProvider('mine', () => (process.env.MINE_KEY ? new MyProvider(proc
|
|
|
215
214
|
|
|
216
215
|
`registerLLMProvider(name, factory)` does the same for text. Both sit on `ProviderRegistry`,
|
|
217
216
|
so a new modality (TTS, embeddings) reuses it rather than hard-coding a provider list.
|
|
217
|
+
|
|
218
|
+
## Links
|
|
219
|
+
|
|
220
|
+
- Source: https://github.com/qosha1/start-simpli/tree/main/packages/llm
|
|
221
|
+
- Homepage: https://startsimpli.com
|
|
222
|
+
- License: MIT, see [LICENSE](./LICENSE)
|
package/package.json
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@startsimpli/llm",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.29",
|
|
4
4
|
"description": "Generic LLM service infrastructure — provider abstraction, retry logic, JSON extraction",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://startsimpli.com",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/qosha1/start-simpli.git",
|
|
10
|
+
"directory": "packages/llm"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/qosha1/start-simpli/issues"
|
|
14
|
+
},
|
|
5
15
|
"main": "./src/index.ts",
|
|
6
16
|
"types": "./src/index.ts",
|
|
7
17
|
"exports": {
|
|
@@ -11,14 +21,15 @@
|
|
|
11
21
|
},
|
|
12
22
|
"files": [
|
|
13
23
|
"src",
|
|
14
|
-
"README.md"
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE"
|
|
15
26
|
],
|
|
16
27
|
"publishConfig": {
|
|
17
28
|
"access": "public"
|
|
18
29
|
},
|
|
19
30
|
"dependencies": {
|
|
20
31
|
"zod": "^4.3.6",
|
|
21
|
-
"@startsimpli/i18n": "^0.1.
|
|
32
|
+
"@startsimpli/i18n": "^0.1.4"
|
|
22
33
|
},
|
|
23
34
|
"devDependencies": {
|
|
24
35
|
"@types/node": "^20.19.39",
|
|
@@ -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
|
-
}
|