mohdel 0.123.0 → 0.125.0
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 +4 -2
- package/js/client/call.js +76 -19
- package/js/factory/bridge.js +2 -2
- package/js/session/adapters/_cancelled.js +0 -6
- package/js/session/adapters/_chat_completions.js +22 -0
- package/package.json +5 -5
- package/src/cli/ask.js +1 -1
- package/src/lib/common.js +3 -4
- package/src/lib/curated-cache.js +5 -0
- package/src/lib/index.js +25 -20
package/README.md
CHANGED
|
@@ -154,7 +154,9 @@ const result = await mo.use('anthropic/claude-sonnet-4-6').answer('Hello')
|
|
|
154
154
|
console.log(result.output, result.cost)
|
|
155
155
|
```
|
|
156
156
|
|
|
157
|
-
No subprocess, no setup beyond your API key. Right for CLI tools (`mo ask`), scripts, tests, and single-process services — which is most projects.
|
|
157
|
+
No subprocess, no setup beyond your API key. Right for CLI tools (`mo ask`), scripts, tests, and single-process services — which is most projects. Pass an `AbortSignal` as `answer(prompt, { signal })` to cancel in flight.
|
|
158
|
+
|
|
159
|
+
The factory reads the catalog from `~/.config/mohdel/curated.json` and keys from the environment, the same defaults a gate session starts from. `mohdel({ models })` replaces the catalog for the process, factory and session runtime alike, the way `set_catalog` replaces it in a gate session. `mohdel({ configurations: { openai: { apiKey } } })` overrides the key per provider.
|
|
158
160
|
|
|
159
161
|
### Client — cross-process (the production gateway)
|
|
160
162
|
|
|
@@ -231,7 +233,7 @@ With no session-bin configured, thin-gate runs in demo mode: `POST /v1/call` ret
|
|
|
231
233
|
|
|
232
234
|
### Calling from JS
|
|
233
235
|
|
|
234
|
-
The client snippet under [Library Usage](#library-usage) above is the full surface: `call(envelope, { socketPath, signal? })` returns an async iterable of events. Pass an `AbortSignal` to cancel in flight; thin-gate forwards a cancel control message to the session and reuses it on the pool. The envelope is the flat `answer(prompt, options)` surface plus transport metadata (`callId`, `authId`, `auth.key`, optional `traceparent`); see [`js/core/envelope.js`](js/core/envelope.js) for the full field list.
|
|
236
|
+
The client snippet under [Library Usage](#library-usage) above is the full surface: `call(envelope, { socketPath, signal? })` returns an async iterable of events. Pass an `AbortSignal` to cancel in flight; thin-gate forwards a cancel control message to the session and reuses it on the pool, and the client ends the stream with the same cancelled `done` the in-process path returns (status `incomplete`, warning `cancelled`, the partial output it relayed, zero tokens). The envelope is the flat `answer(prompt, options)` surface plus transport metadata (`callId`, `authId`, `auth.key`, optional `traceparent`); see [`js/core/envelope.js`](js/core/envelope.js) for the full field list.
|
|
235
237
|
|
|
236
238
|
### Other languages
|
|
237
239
|
|
package/js/client/call.js
CHANGED
|
@@ -1,17 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Send a CallEnvelope to thin-gate; returns an async iterable of Events.
|
|
3
3
|
*
|
|
4
|
-
* Cancellation: pass an AbortSignal. Aborting
|
|
5
|
-
* thin-gate infers cancel from connection close and
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Cancellation: pass an AbortSignal. Aborting closes the HTTP request;
|
|
5
|
+
* thin-gate infers cancel from connection close and sends the session a
|
|
6
|
+
* cancel control message. The session's own cancelled terminal is drained
|
|
7
|
+
* gate-side, so the client synthesizes the same cancelled `done` here —
|
|
8
|
+
* partial output from the deltas it relayed, zero tokens, zero cost —
|
|
9
|
+
* giving a caller the same terminal on this path as on the in-process one.
|
|
8
10
|
* @module client/call
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
import { requestUnix } from './transport.js'
|
|
12
14
|
import { readAll, parseErrorBody } from './response.js'
|
|
13
15
|
import { parseNDJSON } from './ndjson.js'
|
|
14
|
-
import { isEvent, MohdelError } from '#core'
|
|
16
|
+
import { isEvent, MohdelError, STATUS_INCOMPLETE, WARNING_CANCELLED } from '#core'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {string} start
|
|
20
|
+
* @param {string | null} first
|
|
21
|
+
* @param {string} output
|
|
22
|
+
* @returns {import('#core/events.js').DoneEvent}
|
|
23
|
+
*/
|
|
24
|
+
function cancelledDone (start, first, output) {
|
|
25
|
+
const end = String(process.hrtime.bigint())
|
|
26
|
+
return {
|
|
27
|
+
type: 'done',
|
|
28
|
+
result: {
|
|
29
|
+
status: STATUS_INCOMPLETE,
|
|
30
|
+
output: output || null,
|
|
31
|
+
inputTokens: 0,
|
|
32
|
+
outputTokens: 0,
|
|
33
|
+
thinkingTokens: 0,
|
|
34
|
+
cost: 0,
|
|
35
|
+
timestamps: { start, first: first ?? end, end },
|
|
36
|
+
warning: WARNING_CANCELLED
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
15
40
|
|
|
16
41
|
/**
|
|
17
42
|
* @param {import('#core/envelope.js').CallEnvelope} envelope
|
|
@@ -22,26 +47,58 @@ import { isEvent, MohdelError } from '#core'
|
|
|
22
47
|
* @returns {AsyncGenerator<import('#core/events.js').Event>}
|
|
23
48
|
*/
|
|
24
49
|
export async function * call (envelope, { socketPath, signal, path = '/v1/call' }) {
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
50
|
+
const start = String(process.hrtime.bigint())
|
|
51
|
+
if (signal?.aborted) {
|
|
52
|
+
yield cancelledDone(start, null, '')
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let res
|
|
57
|
+
try {
|
|
58
|
+
res = await requestUnix({
|
|
59
|
+
socketPath,
|
|
60
|
+
path,
|
|
61
|
+
method: 'POST',
|
|
62
|
+
body: envelope,
|
|
63
|
+
signal
|
|
64
|
+
})
|
|
65
|
+
} catch (e) {
|
|
66
|
+
if (signal?.aborted) {
|
|
67
|
+
yield cancelledDone(start, null, '')
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
throw e
|
|
71
|
+
}
|
|
32
72
|
|
|
33
73
|
if (res.statusCode !== 200) {
|
|
34
74
|
const body = await readAll(res)
|
|
35
75
|
throw MohdelError.fromJSON(parseErrorBody(body, res.statusCode ?? 0))
|
|
36
76
|
}
|
|
37
77
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
)
|
|
78
|
+
const outputParts = []
|
|
79
|
+
let first = null
|
|
80
|
+
let sawTerminal = false
|
|
81
|
+
try {
|
|
82
|
+
for await (const obj of parseNDJSON(res)) {
|
|
83
|
+
if (!isEvent(obj)) {
|
|
84
|
+
throw new MohdelError(
|
|
85
|
+
'received non-Event object from thin-gate',
|
|
86
|
+
{ type: 'PROTOCOL_INVALID_EVENT', retryable: false }
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
if (obj.type === 'delta') {
|
|
90
|
+
if (first === null) first = String(process.hrtime.bigint())
|
|
91
|
+
if (obj.delta?.type === 'message') outputParts.push(obj.delta.delta)
|
|
92
|
+
} else if (obj.type === 'done' || obj.type === 'error') {
|
|
93
|
+
sawTerminal = true
|
|
94
|
+
}
|
|
95
|
+
yield /** @type {import('#core/events.js').Event} */(obj)
|
|
44
96
|
}
|
|
45
|
-
|
|
97
|
+
} catch (e) {
|
|
98
|
+
if (!signal?.aborted) throw e
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (signal?.aborted && !sawTerminal) {
|
|
102
|
+
yield cancelledDone(start, first, outputParts.join(''))
|
|
46
103
|
}
|
|
47
104
|
}
|
package/js/factory/bridge.js
CHANGED
|
@@ -35,7 +35,6 @@ import { createRealtimeDeltaBuffer } from '../../src/lib/utils.js'
|
|
|
35
35
|
* Hook that returns `{rpmLimit, tpmLimit}` for a given provider —
|
|
36
36
|
* lets the factory keep its own `providersConfig` as source of
|
|
37
37
|
* truth instead of the module-level `providers.json` loader.
|
|
38
|
-
* @property {AbortSignal} [signal]
|
|
39
38
|
*/
|
|
40
39
|
|
|
41
40
|
/**
|
|
@@ -58,6 +57,7 @@ import { createRealtimeDeltaBuffer } from '../../src/lib/utils.js'
|
|
|
58
57
|
* | `providerOrder` / `providerAllow` / `providerDeny`| envelope.providerOptions.openrouter |
|
|
59
58
|
* | `traceparent` / `baggage` | envelope transport metadata |
|
|
60
59
|
* | `callId` / `authId` | envelope transport metadata |
|
|
60
|
+
* | `signal` | not on the wire — `run()` deps, aborts the call |
|
|
61
61
|
* | `configuration.apiKey` | envelope.auth.key |
|
|
62
62
|
* | **`parentSpan`** | **dropped** — use `traceparent` instead |
|
|
63
63
|
* | **`maybeThrowHandler`** | **dropped** — no factory-side validation hook |
|
|
@@ -90,7 +90,7 @@ export async function runAnswer ({ provider, model, modelKey, configuration, pro
|
|
|
90
90
|
|
|
91
91
|
let terminal
|
|
92
92
|
try {
|
|
93
|
-
for await (const ev of run(envelope, deps)) {
|
|
93
|
+
for await (const ev of run(envelope, { ...deps, signal: options.signal })) {
|
|
94
94
|
if (ev.type === 'delta' && ev.delta) {
|
|
95
95
|
if (deltaBuffer) deltaBuffer.push(ev.delta.type, ev.delta.delta)
|
|
96
96
|
} else if (ev.type === 'done' || ev.type === 'error') {
|
|
@@ -2,12 +2,6 @@
|
|
|
2
2
|
* Shared `cancelledDone` helper for adapters that need to synthesize
|
|
3
3
|
* a terminal `done` event on `signal.aborted` mid-stream.
|
|
4
4
|
*
|
|
5
|
-
* Three adapters (openai, anthropic, gemini) had byte-identical
|
|
6
|
-
* copies of this; consolidated here. `_chat_completions.js`
|
|
7
|
-
* and `run.js` have their own cancel paths — don't migrate them
|
|
8
|
-
* here unless you're certain the shape matches (thinkingTokens,
|
|
9
|
-
* cost, tool_calls semantics can all differ).
|
|
10
|
-
*
|
|
11
5
|
* @module session/adapters/_cancelled
|
|
12
6
|
*/
|
|
13
7
|
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { getSpec } from './_catalog.js'
|
|
20
20
|
import { classifyProviderError } from './_errors.js'
|
|
21
21
|
import { costFor } from './_pricing.js'
|
|
22
|
+
import { cancelledDone } from './_cancelled.js'
|
|
22
23
|
import { catalogKey, bareOf } from '#core/model-id.js'
|
|
23
24
|
import {
|
|
24
25
|
STATUS_COMPLETED,
|
|
@@ -98,6 +99,10 @@ export async function * runChatCompletions (envelope, client, config, deps = {})
|
|
|
98
99
|
try {
|
|
99
100
|
response = await client.chat.completions.create(args, { signal: deps.signal })
|
|
100
101
|
} catch (e) {
|
|
102
|
+
if (deps.signal?.aborted) {
|
|
103
|
+
yield cancelledDone(start, null, envelope, '', 0, 0)
|
|
104
|
+
return
|
|
105
|
+
}
|
|
101
106
|
deps.log?.warn({ err: e }, `[mohdel:${config.provider}] request failed`)
|
|
102
107
|
yield { type: 'error', error: classifyProviderError(e, envelope.auth?.key, { provider: config.provider }) }
|
|
103
108
|
return
|
|
@@ -161,6 +166,10 @@ async function * runStreaming (envelope, client, args, config, start, deps) {
|
|
|
161
166
|
try {
|
|
162
167
|
stream = await client.chat.completions.create(args, { signal: deps.signal })
|
|
163
168
|
} catch (e) {
|
|
169
|
+
if (deps.signal?.aborted) {
|
|
170
|
+
yield cancelledDone(start, null, envelope, '', 0, 0)
|
|
171
|
+
return
|
|
172
|
+
}
|
|
164
173
|
deps.log?.warn({ err: e }, `[mohdel:${config.provider}] request failed`)
|
|
165
174
|
yield { type: 'error', error: classifyProviderError(e, envelope.auth?.key, { provider: config.provider }) }
|
|
166
175
|
return
|
|
@@ -168,6 +177,10 @@ async function * runStreaming (envelope, client, args, config, start, deps) {
|
|
|
168
177
|
|
|
169
178
|
try {
|
|
170
179
|
for await (const chunk of stream) {
|
|
180
|
+
if (deps.signal?.aborted) {
|
|
181
|
+
yield cancelledDone(start, first, envelope, contentParts.join(''), 0, 0)
|
|
182
|
+
return
|
|
183
|
+
}
|
|
171
184
|
const choice = chunk.choices?.[0]
|
|
172
185
|
// DeepSeek V4 / deepseek-reasoner / Cerebras reasoning models emit
|
|
173
186
|
// `delta.reasoning_content` chunks before visible content. Capture
|
|
@@ -225,11 +238,20 @@ async function * runStreaming (envelope, client, args, config, start, deps) {
|
|
|
225
238
|
if (chunk.usage) usage = chunk.usage
|
|
226
239
|
}
|
|
227
240
|
} catch (e) {
|
|
241
|
+
if (deps.signal?.aborted) {
|
|
242
|
+
yield cancelledDone(start, first, envelope, contentParts.join(''), 0, 0)
|
|
243
|
+
return
|
|
244
|
+
}
|
|
228
245
|
deps.log?.warn({ err: e }, `[mohdel:${config.provider}] stream failed`)
|
|
229
246
|
yield { type: 'error', error: classifyProviderError(e, envelope.auth?.key, { provider: config.provider }) }
|
|
230
247
|
return
|
|
231
248
|
}
|
|
232
249
|
|
|
250
|
+
if (deps.signal?.aborted) {
|
|
251
|
+
yield cancelledDone(start, first, envelope, contentParts.join(''), 0, 0)
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
|
|
233
255
|
const collectedToolCalls = Object.values(toolCallAccum).map(tc => ({
|
|
234
256
|
id: tc.id,
|
|
235
257
|
function: { name: tc.name, arguments: tc.arguments }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mohdel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.125.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Christophe Le Bars",
|
|
@@ -108,16 +108,16 @@
|
|
|
108
108
|
"@opentelemetry/exporter-trace-otlp-grpc": "^0.222.0",
|
|
109
109
|
"@opentelemetry/sdk-node": "^0.222.0",
|
|
110
110
|
"chalk": "^6.0.0",
|
|
111
|
-
"mohdel-thin-gate-linux-x64-gnu": "0.
|
|
111
|
+
"mohdel-thin-gate-linux-x64-gnu": "0.125.0"
|
|
112
112
|
},
|
|
113
113
|
"dependencies": {
|
|
114
|
-
"@anthropic-ai/sdk": "^0.
|
|
114
|
+
"@anthropic-ai/sdk": "^0.123.0",
|
|
115
115
|
"@cerebras/cerebras_cloud_sdk": "^1.91.0",
|
|
116
|
-
"@google/genai": "^2.
|
|
116
|
+
"@google/genai": "^2.21.0",
|
|
117
117
|
"@opentelemetry/api": "^1.9.1",
|
|
118
118
|
"env-paths": "^4.0.0",
|
|
119
119
|
"groq-sdk": "^1.6.0",
|
|
120
|
-
"openai": "^7.
|
|
120
|
+
"openai": "^7.9.0",
|
|
121
121
|
"undici": "^7.29.0"
|
|
122
122
|
},
|
|
123
123
|
"lint-staged": {
|
package/src/cli/ask.js
CHANGED
|
@@ -13,7 +13,7 @@ export const hintsForError = (err, modelId) => {
|
|
|
13
13
|
const provider = modelId.includes('/') ? modelId.split('/')[0] : null
|
|
14
14
|
const hints = []
|
|
15
15
|
|
|
16
|
-
if (/not found in
|
|
16
|
+
if (/not found in catalog/i.test(both)) {
|
|
17
17
|
if (provider) {
|
|
18
18
|
hints.push(`→ run: mo curate ${provider} # add upstream models from this provider`)
|
|
19
19
|
hints.push(`→ or: mo model add ${modelId} # add this one manually`)
|
package/src/lib/common.js
CHANGED
|
@@ -112,12 +112,11 @@ const createFileOperation = (filePath, defaultValue = {}, operationType) => {
|
|
|
112
112
|
const loadHandler = async () => {
|
|
113
113
|
let loadedData
|
|
114
114
|
try {
|
|
115
|
-
if (!existsSync(CONFIG_DIR)) {
|
|
116
|
-
await mkdir(CONFIG_DIR, { recursive: true })
|
|
117
|
-
}
|
|
118
|
-
|
|
119
115
|
if (!existsSync(filePath)) {
|
|
120
116
|
if (defaultValue && Object.keys(defaultValue).length > 0) {
|
|
117
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
118
|
+
await mkdir(CONFIG_DIR, { recursive: true })
|
|
119
|
+
}
|
|
121
120
|
await writeFile(filePath, JSON.stringify(defaultValue, null, 2))
|
|
122
121
|
loadedData = JSON.parse(JSON.stringify(defaultValue))
|
|
123
122
|
} else {
|
package/src/lib/curated-cache.js
CHANGED
|
@@ -69,6 +69,11 @@ const rebuildAliasMap = () => {
|
|
|
69
69
|
|
|
70
70
|
export const loadCuratedCache = ensureCuratedCache
|
|
71
71
|
|
|
72
|
+
export const setCuratedCache = (table) => {
|
|
73
|
+
curatedCache = { ...table }
|
|
74
|
+
aliasMapCache = buildAliasMap(curatedCache)
|
|
75
|
+
}
|
|
76
|
+
|
|
72
77
|
export const getCuratedCacheSnapshot = () => curatedCache
|
|
73
78
|
|
|
74
79
|
export const getAliasMapSnapshot = () => aliasMapCache
|
package/src/lib/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import providers from './providers.js'
|
|
|
7
7
|
import { getAPIKey, loadDefaultEnv, getProvidersConfig, saveProvidersConfig, setLogger as setCommonLogger } from './common.js'
|
|
8
8
|
import {
|
|
9
9
|
loadCuratedCache,
|
|
10
|
+
setCuratedCache,
|
|
10
11
|
getCuratedCacheSnapshot,
|
|
11
12
|
expandModelAliasSync,
|
|
12
13
|
suggestModels,
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
} from './curated-cache.js'
|
|
15
16
|
import { createRateLimiter } from '../../js/session/_rate_limiter.js'
|
|
16
17
|
import { createCooldownTracker } from '../../js/session/_cooldown.js'
|
|
18
|
+
import { setCatalog } from '../../js/session/adapters/_catalog.js'
|
|
17
19
|
import { runAnswer, runAnswerImage, runAnswerTranscription } from '../../js/factory/bridge.js'
|
|
18
20
|
import { startSpan, endSpanOk, endSpanError } from './tracing.js'
|
|
19
21
|
import { isValidTag } from './schema.js'
|
|
@@ -200,21 +202,27 @@ export { buildHandlers as _buildHandlersForTests }
|
|
|
200
202
|
* @param {Function} [opts.onFailure] — fired after every failed answer call
|
|
201
203
|
* @param {number} [opts.cooldownThreshold=3] — consecutive provider failures before cooldown
|
|
202
204
|
* @param {number} [opts.cooldownDuration=60000] — cooldown duration in ms
|
|
203
|
-
* @param {object} [opts.models] —
|
|
204
|
-
*
|
|
205
|
+
* @param {object} [opts.models] — catalog `{ [provider/model]: spec }`. Replaces
|
|
206
|
+
* `~/.config/mohdel/curated.json` for this process,
|
|
207
|
+
* factory and session runtime alike.
|
|
208
|
+
* @param {object} [opts.configurations] — `{ [provider]: { apiKey } }`. Overrides the env
|
|
209
|
+
* key per provider; env for the rest.
|
|
205
210
|
*/
|
|
206
211
|
const mohdel = async ({ logger, verbosity: verbosityOpt, onSuccess, onFailure, cooldownThreshold, cooldownDuration, models, configurations } = {}) => {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
212
|
+
loadDefaultEnv()
|
|
213
|
+
|
|
214
|
+
// The session runtime keeps its own catalog cache (the seam thin-gate's
|
|
215
|
+
// `set_catalog` uses) and would otherwise lazy-load curated.json, so a
|
|
216
|
+
// replacement has to reach both caches.
|
|
217
|
+
if (models) {
|
|
218
|
+
setCuratedCache(models)
|
|
219
|
+
setCatalog(models)
|
|
220
|
+
} else {
|
|
213
221
|
await loadCuratedCache()
|
|
214
222
|
}
|
|
215
223
|
|
|
216
224
|
// Provider config: user-specific rate limits per provider (~/.config/mohdel/providers.json)
|
|
217
|
-
const providersConfig =
|
|
225
|
+
const providersConfig = await getProvidersConfig()
|
|
218
226
|
|
|
219
227
|
// Resolve verbosity tier once at init. Factory opt > env var > default.
|
|
220
228
|
// Captured in the answer() closure below to gate per-call log lines.
|
|
@@ -248,7 +256,7 @@ const mohdel = async ({ logger, verbosity: verbosityOpt, onSuccess, onFailure, c
|
|
|
248
256
|
get: (target, prop) => {
|
|
249
257
|
if (prop === 'list') {
|
|
250
258
|
return (tag) => { // Sync
|
|
251
|
-
const catalog =
|
|
259
|
+
const catalog = getCuratedCacheSnapshot()
|
|
252
260
|
let modelEntries = Object.entries(catalog)
|
|
253
261
|
.filter(([, metadata]) => !metadata.deprecated)
|
|
254
262
|
|
|
@@ -267,7 +275,7 @@ const mohdel = async ({ logger, verbosity: verbosityOpt, onSuccess, onFailure, c
|
|
|
267
275
|
|
|
268
276
|
if (prop === 'use') {
|
|
269
277
|
return (modelId) => { // Sync
|
|
270
|
-
const catalog =
|
|
278
|
+
const catalog = getCuratedCacheSnapshot()
|
|
271
279
|
|
|
272
280
|
// Parse optional :outputEffort suffix (e.g. "claude-opus:max").
|
|
273
281
|
// Effort levels vary per spec (`thinkingEffortLevels` keys —
|
|
@@ -284,7 +292,7 @@ const mohdel = async ({ logger, verbosity: verbosityOpt, onSuccess, onFailure, c
|
|
|
284
292
|
// Fallback specs are excluded from that test on purpose:
|
|
285
293
|
// providers that synthesize them resolve any string, which
|
|
286
294
|
// would disable suffix parsing for them entirely.
|
|
287
|
-
const exactId =
|
|
295
|
+
const exactId = expandModelAliasSync(modelId)
|
|
288
296
|
const isCuratedId = !!catalog[exactId]
|
|
289
297
|
|
|
290
298
|
// `@speed` is parsed off first so the two suffixes are read
|
|
@@ -299,7 +307,7 @@ const mohdel = async ({ logger, verbosity: verbosityOpt, onSuccess, onFailure, c
|
|
|
299
307
|
// resolves the same as `x@fast`.
|
|
300
308
|
const colon = base.lastIndexOf(':')
|
|
301
309
|
const probe = colon > 0 ? base.slice(0, colon) : base
|
|
302
|
-
const probeResolved =
|
|
310
|
+
const probeResolved = expandModelAliasSync(probe)
|
|
303
311
|
if (catalog[probeResolved] || createFallbackModelSpec(probeResolved)) {
|
|
304
312
|
aliasSpeed = candidate
|
|
305
313
|
modelId = base
|
|
@@ -311,7 +319,7 @@ const mohdel = async ({ logger, verbosity: verbosityOpt, onSuccess, onFailure, c
|
|
|
311
319
|
if (colonIdx > 0) {
|
|
312
320
|
const candidate = modelId.slice(colonIdx + 1)
|
|
313
321
|
const base = modelId.slice(0, colonIdx)
|
|
314
|
-
const baseResolved =
|
|
322
|
+
const baseResolved = expandModelAliasSync(base)
|
|
315
323
|
const baseSpec = catalog[baseResolved] || createFallbackModelSpec(baseResolved)
|
|
316
324
|
if (baseSpec) {
|
|
317
325
|
aliasOutputEffort = candidate
|
|
@@ -319,17 +327,14 @@ const mohdel = async ({ logger, verbosity: verbosityOpt, onSuccess, onFailure, c
|
|
|
319
327
|
}
|
|
320
328
|
}
|
|
321
329
|
|
|
322
|
-
let resolvedModelId =
|
|
330
|
+
let resolvedModelId = expandModelAliasSync(modelId)
|
|
323
331
|
let modelSpec = catalog[resolvedModelId]
|
|
324
332
|
|
|
325
333
|
if (!modelSpec) {
|
|
326
334
|
modelSpec = createFallbackModelSpec(resolvedModelId)
|
|
327
335
|
if (!modelSpec) {
|
|
328
|
-
if (libraryMode) {
|
|
329
|
-
throw new Error(`Model '${modelId}' not found in provided models.`)
|
|
330
|
-
}
|
|
331
336
|
const suggestions = suggestModels(modelId)
|
|
332
|
-
let msg = `Model '${modelId}' not found in
|
|
337
|
+
let msg = `Model '${modelId}' not found in catalog.`
|
|
333
338
|
if (suggestions.length) {
|
|
334
339
|
msg += ' Did you mean?\n' + suggestions.map(s => ` ${s.id} ${s.label}`).join('\n')
|
|
335
340
|
}
|
|
@@ -777,7 +782,7 @@ const createModelProxy = (resolvedModelId, modelSpec, handlers, aliasOutputEffor
|
|
|
777
782
|
|
|
778
783
|
if (prop === 'info') {
|
|
779
784
|
return () => { // Sync
|
|
780
|
-
const catalog =
|
|
785
|
+
const catalog = getCuratedCacheSnapshot()
|
|
781
786
|
return catalog?.[resolvedModelId] ? { ...catalog[resolvedModelId] } : { ...modelSpec }
|
|
782
787
|
}
|
|
783
788
|
}
|