@crossworks/client-types 0.232.175 → 0.232.182
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/package.json +2 -2
- package/src/model-pools.test.ts +64 -4
- package/src/model-pools.ts +64 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crossworks/client-types",
|
|
3
|
-
"version": "0.232.
|
|
3
|
+
"version": "0.232.182",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"./*": "./src/*.ts"
|
|
10
10
|
},
|
|
11
11
|
"devDependencies": {
|
|
12
|
-
"@types/node": "^26.4.
|
|
12
|
+
"@types/node": "^26.4.1"
|
|
13
13
|
},
|
|
14
14
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
15
15
|
"repository": {
|
package/src/model-pools.test.ts
CHANGED
|
@@ -6,12 +6,18 @@
|
|
|
6
6
|
* vision worker wanted text.
|
|
7
7
|
*/
|
|
8
8
|
import { describe, expect, it } from 'vitest';
|
|
9
|
-
import { MODEL_POOLS, poolModelIssue } from './model-pools';
|
|
9
|
+
import { MODEL_POOLS, poolModelIssue, kindFromModalities } from './model-pools';
|
|
10
10
|
import { CURATED_MODEL_POOLS } from './model-pools-data';
|
|
11
11
|
|
|
12
12
|
const READER = { input: ['text', 'image'], output: ['text'] };
|
|
13
13
|
const GENERATOR = { input: ['image', 'text'], output: ['image', 'text'] };
|
|
14
14
|
const TEXT_ONLY = { input: ['text'], output: ['text'] };
|
|
15
|
+
// The voice shapes, verbatim from OpenRouter's catalog once the fetch asks
|
|
16
|
+
// for `output_modalities=all`: dedicated engines on either side, plus the
|
|
17
|
+
// speech-capable chat model that legitimately serves BOTH voice pools.
|
|
18
|
+
const TTS_ENGINE = { input: ['text'], output: ['speech'] };
|
|
19
|
+
const STT_ENGINE = { input: ['audio'], output: ['transcription'] };
|
|
20
|
+
const AUDIO_CHAT = { input: ['text', 'audio'], output: ['text', 'audio'] };
|
|
15
21
|
|
|
16
22
|
describe('poolModelIssue', () => {
|
|
17
23
|
it('keeps an image generator out of the vision pool', () => {
|
|
@@ -37,9 +43,32 @@ describe('poolModelIssue', () => {
|
|
|
37
43
|
expect(poolModelIssue('image_gen', READER)).toMatch(/does not output images/);
|
|
38
44
|
});
|
|
39
45
|
|
|
40
|
-
it('
|
|
41
|
-
expect(poolModelIssue('
|
|
42
|
-
expect(poolModelIssue('
|
|
46
|
+
it('keeps every other non-text output out of a text-out pool', () => {
|
|
47
|
+
expect(poolModelIssue('summarizer', TTS_ENGINE)).toMatch(/outputs speech, not text/);
|
|
48
|
+
expect(poolModelIssue('agents', STT_ENGINE)).toMatch(/outputs transcription, not text/);
|
|
49
|
+
expect(poolModelIssue('extractor', { input: ['text'], output: ['embeddings'] })).toMatch(
|
|
50
|
+
/outputs embeddings, not text/,
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('checks the voice pools now that the catalog lists their engines', () => {
|
|
55
|
+
expect(poolModelIssue('tts', TTS_ENGINE)).toBeNull();
|
|
56
|
+
expect(poolModelIssue('stt', STT_ENGINE)).toBeNull();
|
|
57
|
+
// The classic swap, which used to sail through unchecked.
|
|
58
|
+
expect(poolModelIssue('tts', STT_ENGINE)).toMatch(/produces no audio/);
|
|
59
|
+
expect(poolModelIssue('stt', TTS_ENGINE)).toMatch(/does not turn audio into text/);
|
|
60
|
+
expect(poolModelIssue('tts', TEXT_ONLY)).toMatch(/produces no audio/);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('accepts a speech-capable chat model in both voice pools (gpt-audio)', () => {
|
|
64
|
+
expect(poolModelIssue('tts', AUDIO_CHAT)).toBeNull();
|
|
65
|
+
expect(poolModelIssue('stt', AUDIO_CHAT)).toBeNull();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('still fails open for the direct-provider voice slugs OpenRouter never lists', () => {
|
|
69
|
+
// ElevenLabs, Deepgram, Gemini voices: no catalog row, so no evidence.
|
|
70
|
+
expect(poolModelIssue('tts', { input: [], output: [] })).toBeNull();
|
|
71
|
+
expect(poolModelIssue('stt', null)).toBeNull();
|
|
43
72
|
});
|
|
44
73
|
|
|
45
74
|
it('fails OPEN on an unloaded catalog and on an unknown pool', () => {
|
|
@@ -49,6 +78,37 @@ describe('poolModelIssue', () => {
|
|
|
49
78
|
});
|
|
50
79
|
});
|
|
51
80
|
|
|
81
|
+
describe('kindFromModalities', () => {
|
|
82
|
+
it('reads the bucket off the catalog instead of guessing from the slug', () => {
|
|
83
|
+
expect(kindFromModalities(['transcription'])).toBe('stt');
|
|
84
|
+
expect(kindFromModalities(['speech'])).toBe('tts');
|
|
85
|
+
expect(kindFromModalities(['embeddings'])).toBe('embedding');
|
|
86
|
+
expect(kindFromModalities(['rerank'])).toBe('rerank');
|
|
87
|
+
expect(kindFromModalities(['video'])).toBe('video');
|
|
88
|
+
expect(kindFromModalities(['image', 'text'])).toBe('image');
|
|
89
|
+
expect(kindFromModalities(['text'])).toBe('chat');
|
|
90
|
+
expect(kindFromModalities([])).toBe('chat');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('separates a speech-capable chat model from a sound generator', () => {
|
|
94
|
+
// openai/gpt-audio: text+audio->text+audio, still a chat model.
|
|
95
|
+
expect(kindFromModalities(['text', 'audio'])).toBe('chat');
|
|
96
|
+
// google/lyria-*: audio out only.
|
|
97
|
+
expect(kindFromModalities(['audio'])).toBe('audio');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('agrees with the pool contract it sits beside', () => {
|
|
101
|
+
// A model this bucket calls 'tts' must be one the tts pool accepts, or
|
|
102
|
+
// the /models filter and the curation guard would tell different stories.
|
|
103
|
+
const tts = { input: ['text'], output: ['speech'] };
|
|
104
|
+
expect(kindFromModalities(tts.output)).toBe('tts');
|
|
105
|
+
expect(poolModelIssue('tts', tts)).toBeNull();
|
|
106
|
+
const stt = { input: ['audio'], output: ['transcription'] };
|
|
107
|
+
expect(kindFromModalities(stt.output)).toBe('stt');
|
|
108
|
+
expect(poolModelIssue('stt', stt)).toBeNull();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
52
112
|
describe('the shipped curated template', () => {
|
|
53
113
|
const slugs = (pool: string) =>
|
|
54
114
|
new Set(
|
package/src/model-pools.ts
CHANGED
|
@@ -37,9 +37,13 @@ export type ModelPoolDef = {
|
|
|
37
37
|
export type PoolModality = {
|
|
38
38
|
/** Modalities the model must ACCEPT. Empty = text-only is fine. */
|
|
39
39
|
input: readonly ('image' | 'file')[];
|
|
40
|
-
/** What the consumer reads back
|
|
41
|
-
*
|
|
42
|
-
|
|
40
|
+
/** What the consumer reads back, in OpenRouter's own output_modalities
|
|
41
|
+
* vocabulary — `speech` is a TTS engine, `transcription` an ASR one. Those
|
|
42
|
+
* two used to be uncheckable (the catalog fetch was the text-out slice, so
|
|
43
|
+
* a voice route was always "unknown"); since the fetch asks for
|
|
44
|
+
* `output_modalities=all` they carry positive evidence like everything
|
|
45
|
+
* else. */
|
|
46
|
+
output: 'text' | 'image' | 'speech' | 'transcription';
|
|
43
47
|
};
|
|
44
48
|
|
|
45
49
|
const TEXT_OUT: PoolModality = { input: [], output: 'text' };
|
|
@@ -103,7 +107,7 @@ export const MODEL_POOLS: readonly ModelPoolDef[] = [
|
|
|
103
107
|
description:
|
|
104
108
|
'Turns replies into speech. Provider-specific catalog (Grok voice, GPT-4o TTS voices, ElevenLabs).',
|
|
105
109
|
group: 'workers',
|
|
106
|
-
modality: { input: [], output: '
|
|
110
|
+
modality: { input: [], output: 'speech' },
|
|
107
111
|
},
|
|
108
112
|
{
|
|
109
113
|
id: 'stt',
|
|
@@ -111,7 +115,7 @@ export const MODEL_POOLS: readonly ModelPoolDef[] = [
|
|
|
111
115
|
description:
|
|
112
116
|
'Speech to text for voice notes and video ingest (Whisper family, grok-stt, gpt-4o-mini-transcribe).',
|
|
113
117
|
group: 'workers',
|
|
114
|
-
modality: { input: [], output: '
|
|
118
|
+
modality: { input: [], output: 'transcription' },
|
|
115
119
|
},
|
|
116
120
|
{
|
|
117
121
|
id: 'search',
|
|
@@ -156,6 +160,33 @@ export type ModelModalities = {
|
|
|
156
160
|
output: readonly string[];
|
|
157
161
|
};
|
|
158
162
|
|
|
163
|
+
/** The coarse bucket a catalog row falls into. `chat` is what a text-out row
|
|
164
|
+
* is called; the rest mirror OpenRouter's output-modality vocabulary. */
|
|
165
|
+
export type CatalogKind =
|
|
166
|
+
'chat' | 'image' | 'video' | 'tts' | 'stt' | 'embedding' | 'rerank' | 'audio';
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Bucket a model by what it PRODUCES. Lives here, beside `poolModelIssue`, so
|
|
170
|
+
* the /models type filter and the Curator's `model_catalog` filter cannot
|
|
171
|
+
* drift apart — they are answering the same question off the same field, and
|
|
172
|
+
* two copies of this ordering would eventually disagree about (say) a model
|
|
173
|
+
* that both generates images and returns text.
|
|
174
|
+
*
|
|
175
|
+
* Order matters: the dedicated buckets win over the generic ones, and audio
|
|
176
|
+
* beside text is a speech-capable CHAT model (`openai/gpt-audio`) rather than
|
|
177
|
+
* a sound generator.
|
|
178
|
+
*/
|
|
179
|
+
export function kindFromModalities(output: readonly string[]): CatalogKind {
|
|
180
|
+
if (output.includes('transcription')) return 'stt';
|
|
181
|
+
if (output.includes('speech')) return 'tts';
|
|
182
|
+
if (output.includes('embeddings')) return 'embedding';
|
|
183
|
+
if (output.includes('rerank')) return 'rerank';
|
|
184
|
+
if (output.includes('video')) return 'video';
|
|
185
|
+
if (output.includes('image')) return 'image';
|
|
186
|
+
if (output.includes('audio') && !output.includes('text')) return 'audio';
|
|
187
|
+
return 'chat';
|
|
188
|
+
}
|
|
189
|
+
|
|
159
190
|
/**
|
|
160
191
|
* Does this model belong in this pool? Returns the reason it does NOT, or
|
|
161
192
|
* null when it fits.
|
|
@@ -163,9 +194,9 @@ export type ModelModalities = {
|
|
|
163
194
|
* Fail-open by design (same rule as the worker-config catalog check): a
|
|
164
195
|
* `null` modalities argument means the catalog never loaded, and an outage
|
|
165
196
|
* must never block a curator from recording their judgment. Only positive
|
|
166
|
-
* catalog evidence rejects
|
|
167
|
-
*
|
|
168
|
-
*
|
|
197
|
+
* catalog evidence rejects — a route the catalog does not list (every
|
|
198
|
+
* direct-provider voice slug: ElevenLabs, Deepgram, Gemini voices) still
|
|
199
|
+
* arrives here as an empty pair and passes.
|
|
169
200
|
*/
|
|
170
201
|
export function poolModelIssue(
|
|
171
202
|
poolId: string,
|
|
@@ -174,7 +205,6 @@ export function poolModelIssue(
|
|
|
174
205
|
const pool = POOL_BY_ID.get(poolId);
|
|
175
206
|
if (!pool || !modalities) return null;
|
|
176
207
|
const want = pool.modality;
|
|
177
|
-
if (want.output === 'audio') return null;
|
|
178
208
|
const outputs = modalities.output ?? [];
|
|
179
209
|
const inputs = modalities.input ?? [];
|
|
180
210
|
if (outputs.length === 0 && inputs.length === 0) return null;
|
|
@@ -187,9 +217,34 @@ export function poolModelIssue(
|
|
|
187
217
|
`pool instead. Reading images is just a capable text-out model that accepts pictures.`
|
|
188
218
|
);
|
|
189
219
|
}
|
|
220
|
+
// Everything else non-text on a text-out pool: video, speech (a TTS engine),
|
|
221
|
+
// transcription (an ASR one), embeddings, rerank. Before the catalog was
|
|
222
|
+
// widened these could not reach a text pool because the fetch never listed
|
|
223
|
+
// them; now they can, so the guard has to name them.
|
|
224
|
+
if (want.output === 'text' && !outputs.includes('text') && outputs.length > 0) {
|
|
225
|
+
return `this model outputs ${outputs.join('+')}, not text — the ${pool.label} pool needs a text-out model.`;
|
|
226
|
+
}
|
|
190
227
|
if (want.output === 'image' && !makesImages && outputs.length > 0) {
|
|
191
228
|
return `this model does not output images (${outputs.join('+')}) — the ${pool.label} pool needs a generator.`;
|
|
192
229
|
}
|
|
230
|
+
// Voice pools. Two shapes qualify for each: the dedicated engine
|
|
231
|
+
// (`speech` / `transcription`) and the speech-capable chat model
|
|
232
|
+
// (`openai/gpt-audio` is `text+audio->text+audio` and legitimately serves
|
|
233
|
+
// BOTH pools — it is in the shipped template for both). What that still
|
|
234
|
+
// catches is the classic swap: a pure TTS engine parked in Transcribe emits
|
|
235
|
+
// no text and takes no audio, which is a positive contradiction.
|
|
236
|
+
if (want.output === 'speech' && outputs.length > 0) {
|
|
237
|
+
if (!outputs.includes('speech') && !outputs.includes('audio')) {
|
|
238
|
+
return `this model outputs ${outputs.join('+')} — it produces no audio, and the ${pool.label} pool needs a model that speaks.`;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (want.output === 'transcription' && outputs.length > 0) {
|
|
242
|
+
const transcribes =
|
|
243
|
+
outputs.includes('transcription') || (outputs.includes('text') && inputs.includes('audio'));
|
|
244
|
+
if (!transcribes) {
|
|
245
|
+
return `this model does not turn audio into text (${inputs.join('+') || '?'}->${outputs.join('+')}) — the ${pool.label} pool needs one that does.`;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
193
248
|
for (const need of want.input) {
|
|
194
249
|
if (inputs.length > 0 && !inputs.includes(need)) {
|
|
195
250
|
return `this model does not accept ${need} input (accepts ${inputs.join('+')}) — the ${pool.label} pool needs one that does.`;
|