mohdel 1.0.0 → 1.0.1

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 CHANGED
@@ -107,11 +107,11 @@ groq/llama-4-scout-17b-16e-instruct
107
107
 
108
108
  ## Attack surface
109
109
 
110
- Anthropic's 2026 threat report describes actors compromising AI wrapper
111
- services built on LiteLLM, using prompt injection to exfiltrate the production
112
- API keys held in their cloud containers. That attack needs two things: keys
113
- sitting where a process can read them, and a component that injected content
114
- can steer into reading them. Mohdel is built so neither is present.
110
+ Anthropic's 2026 threat report describes actors compromising LLM wrapper
111
+ services through prompt injection, exfiltrating the production API keys held
112
+ in their cloud containers. That attack needs two things: keys sitting where a
113
+ process can read them, and a component that injected content can steer into
114
+ reading them. Mohdel is built so neither is present.
115
115
 
116
116
  - **Nothing executes.** No `eval`, no `new Function`, no `child_process`
117
117
  anywhere in the session, factory or library, and no automatic tool loop. A
@@ -76,14 +76,6 @@ import { createRealtimeDeltaBuffer } from '../../src/lib/utils.js'
76
76
  export async function runAnswer ({ provider, model, modelKey, configuration, prompt, options = {} }, deps = {}) {
77
77
  const envelope = markTrustedMedia(toEnvelope({ modelKey, configuration, prompt, options }))
78
78
 
79
- // If the caller passed a `realtimeHandler`, feed every `delta`
80
- // event into a buffer that invokes the handler on batches matching
81
- // `bufferOpts` cadence. Without this, streaming callbacks silently
82
- // never fire — `mo ask --stream` and any integration that relies
83
- // on streaming callbacks stops working.
84
- //
85
- // Skip the buffer allocation entirely when no handler was
86
- // supplied — the common case.
87
79
  const deltaBuffer = options.realtimeHandler
88
80
  ? createRealtimeDeltaBuffer(options.realtimeHandler, options.bufferOpts)
89
81
  : null
@@ -99,9 +91,6 @@ export async function runAnswer ({ provider, model, modelKey, configuration, pro
99
91
  }
100
92
  }
101
93
  } finally {
102
- // Flush any pending buffered content regardless of terminal
103
- // path (success, error, or exception) so the handler sees the
104
- // tail of the stream.
105
94
  deltaBuffer?.flush()
106
95
  }
107
96
 
@@ -233,9 +222,7 @@ function toEnvelope ({ modelKey, configuration, prompt, options }) {
233
222
  if (options.identifier) envelope.identifier = options.identifier
234
223
  if (options.idleHeartbeatMs !== undefined) envelope.idleHeartbeatMs = options.idleHeartbeatMs
235
224
 
236
- // OpenRouter routing prefs ride in their own bag to keep the flat
237
- // envelope clean. The openrouter adapter reads this via
238
- // `config.mutateArgs`.
225
+ // OpenRouter routing prefs ride in their own bag; the adapter reads them via `config.mutateArgs`.
239
226
  if (options.providerOrder || options.providerAllow || options.providerDeny) {
240
227
  envelope.providerOptions = {
241
228
  openrouter: {
@@ -361,10 +348,7 @@ function toEnvelopePrompt (prompt) {
361
348
  return out
362
349
  }
363
350
 
364
- // Unknown shape — reject early with a clear error. Letting this
365
- // fall through would land a raw non-iterable in the envelope and
366
- // produce a confusing `prompt.map is not a function` deep inside
367
- // the adapter.
351
+ // Rejected here; falling through lands a non-iterable in the envelope and fails as `prompt.map is not a function` inside the adapter.
368
352
  throw new MohdelError('invalid prompt shape', {
369
353
  type: 'SESSION_INVALID_PROMPT',
370
354
  retryable: false,
@@ -104,9 +104,6 @@ export function createCooldownTracker (
104
104
  return { check, recordFailure, reset, coolingDownError, threshold, durationMs }
105
105
  }
106
106
 
107
- // Single session-local tracker. Re-exported as named members so
108
- // callers can `import * as cooldown from './_cooldown.js'` and use
109
- // `cooldown.reset(key)` / `cooldown.coolingDownError(key)`.
110
107
  const defaultTracker = createCooldownTracker()
111
108
  export const check = defaultTracker.check
112
109
  export const recordFailure = defaultTracker.recordFailure
@@ -26,10 +26,7 @@ const LEVELS = {
26
26
  silent: 100
27
27
  }
28
28
 
29
- // Real sessions want "warn" (anomalies visible) but tests would
30
- // otherwise flood stderr with the lines we deliberately induce.
31
- // Under vitest, default to silent unless MOHDEL_LOG_LEVEL is set
32
- // explicitly.
29
+ // Under vitest, default to silent unless MOHDEL_LOG_LEVEL is set: induced anomalies would otherwise flood stderr.
33
30
  const DEFAULT_LEVEL = process.env.VITEST ? 'silent' : 'warn'
34
31
  const DEFAULT_VERBOSITY = 1
35
32
 
@@ -131,8 +128,4 @@ function mergeFields (target, source, includeStack) {
131
128
  }
132
129
  }
133
130
 
134
- // Module-level default — used when a caller doesn't pass their own.
135
- // Keeping the stream open lets adapters import the singleton
136
- // directly; `withContext` spawns scoped children per call without
137
- // allocating a new writer.
138
131
  export const logger = createLogger()
@@ -48,9 +48,6 @@ export async function ensureOtelInitialized () {
48
48
  sdk.start()
49
49
  otelInitialized = true
50
50
  } catch (e) {
51
- // Leave the flag false so a retry is possible. The current caller
52
- // (bin.js::main) runs this once, so "retry" in practice means a
53
- // process restart — but the semantics should match the flag name.
54
51
  process.stderr.write(
55
52
  `${JSON.stringify({ level: 'warn', time: Date.now(), msg: '[mohdel:tracing] OTel SDK init failed', err: { message: e.message } })}\n`
56
53
  )
@@ -148,8 +148,7 @@ async function * runStreaming (envelope, client, args, config, start, deps) {
148
148
  args.stream = true
149
149
  args.stream_options = { include_usage: true }
150
150
 
151
- // Accumulate via array + join to avoid per-delta V8 cons-string
152
- // churn on long streams.
151
+ // Array + join, not `+=`: per-delta cons-strings are the cost on a long stream.
153
152
  const contentParts = []
154
153
  const reasoningParts = []
155
154
  let first = null
@@ -199,12 +198,7 @@ async function * runStreaming (envelope, client, args, config, start, deps) {
199
198
  }
200
199
  if (choice?.delta?.tool_calls) {
201
200
  for (const tc of choice.delta.tool_calls) {
202
- // Resolve which slot this chunk belongs to:
203
- // 1. explicit `tc.index` always wins (normal case);
204
- // 2. missing `tc.index` but a `tc.id` we've seen before
205
- // → use the cross-ref we recorded on the opener;
206
- // 3. new `tc.id` with no index → allocate next slot;
207
- // 4. neither id nor index → can't correlate; drop + warn.
201
+ // Precedence: explicit `tc.index`, then a known `tc.id`, then a new slot; neither means the chunk cannot be correlated.
208
202
  let idx = tc.index
209
203
  if (idx == null && tc.id != null && idToIndex[tc.id] != null) {
210
204
  idx = idToIndex[tc.id]
@@ -8,10 +8,7 @@
8
8
 
9
9
  const argsObj = (args) => args || {}
10
10
 
11
- // Tool argument parse failures are expected (models routinely send
12
- // malformed JSON before retrying with corrections). Fall back to
13
- // returning the raw string — downstream adapter code handles the
14
- // type mismatch. Logging here would create warn-level noise.
11
+ // Malformed tool-call JSON is routine, not an anomaly: return the raw string and let the adapter handle the mismatch.
15
12
  const parseArgs = (_name, args) => {
16
13
  if (typeof args !== 'string') return argsObj(args)
17
14
  try {
@@ -110,8 +110,7 @@ export async function * anthropic (envelope, deps = {}) {
110
110
 
111
111
  const request = buildRequest(envelope, conversation, system, conversationCacheTtl)
112
112
 
113
- // Accumulate via array + join to avoid per-delta V8 cons-string
114
- // churn. Materialized at each exit point.
113
+ // Array + join, not `+=`: per-delta cons-strings are the cost on a long stream.
115
114
  const outputParts = []
116
115
  const currentOutput = () => outputParts.join('')
117
116
  let inputTokens = 0
@@ -62,8 +62,6 @@ export async function * fake (envelope, deps = {}) {
62
62
  return
63
63
 
64
64
  case 'hang':
65
- // Wait for abort. If the signal is already aborted, yield
66
- // cancelled immediately. Otherwise block until it fires.
67
65
  await waitForAbort(signal)
68
66
  yield cancelledDone(start, first, envelope, '', 0, 0)
69
67
  return
@@ -101,8 +101,7 @@ export async function * gemini (envelope, deps = {}) {
101
101
  request.config = { ...(request.config ?? {}), abortSignal: signal }
102
102
  }
103
103
 
104
- // Accumulate via array + join to avoid per-delta V8 cons-string
105
- // churn. Materialized at each exit point.
104
+ // Array + join, not `+=`: per-delta cons-strings are the cost on a long stream.
106
105
  const outputParts = []
107
106
  const currentOutput = () => outputParts.join('')
108
107
  let inputTokens = 0
@@ -71,8 +71,7 @@ export async function * openai (envelope, deps = {}) {
71
71
 
72
72
  const request = buildRequest(envelope, input, instructions)
73
73
 
74
- // Accumulate via array + join to avoid per-delta V8 cons-string
75
- // churn. Materialized at each exit point.
74
+ // Array + join, not `+=`: per-delta cons-strings are the cost on a long stream.
76
75
  const outputParts = []
77
76
  const currentOutput = () => outputParts.join('')
78
77
  let inputTokens = 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mohdel",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "license": "MIT",
5
5
  "author": {
6
6
  "name": "Christophe Le Bars",
@@ -136,7 +136,7 @@
136
136
  "@opentelemetry/exporter-trace-otlp-grpc": "^0.222.0",
137
137
  "@opentelemetry/sdk-node": "^0.222.0",
138
138
  "chalk": "^6.0.0",
139
- "mohdel-thin-gate-linux-x64-gnu": "1.0.0"
139
+ "mohdel-thin-gate-linux-x64-gnu": "1.0.1"
140
140
  },
141
141
  "dependencies": {
142
142
  "@anthropic-ai/sdk": "^0.125.0",
@@ -3,9 +3,7 @@ import { fieldDefs } from '../lib/schema.js'
3
3
  import { getCuratedModels, catalogEntries } from '../lib/common.js'
4
4
  import { ALIASES } from './aliases.js'
5
5
 
6
- // Completion must never import the factory (`src/lib/index.js`), which pulls
7
- // every adapter, provider SDK and the OTel stack — 350ms that would be paid on
8
- // every press of the tab key. Reading the catalog costs about 30ms.
6
+ // Never import the factory here: it loads every provider SDK, on every tab press.
9
7
 
10
8
  const NOUNS = ['model', 'provider', 'creator', 'tag', 'ratelimit', 'ask', 'transcribe', 'default', 'doctor']
11
9
 
@@ -18,9 +16,6 @@ const VERBS = {
18
16
  ratelimit: ['show', 'set', 'rm', 'provider']
19
17
  }
20
18
 
21
- // What the argument at a given offset after `<noun> <verb>` names. `model` is
22
- // an id already in the catalog, so `model add` — which takes a new one — is
23
- // deliberately absent.
24
19
  const ARGUMENT = {
25
20
  ask: ['model'],
26
21
  transcribe: ['model'],
@@ -79,16 +74,12 @@ export const candidates = async (cword, words) => {
79
74
  return offer([...NOUNS, ...Object.keys(ALIASES)])
80
75
  }
81
76
 
82
- // Resolve the alias exactly as the router does, so completion and dispatch
83
- // never disagree about what a word means.
84
77
  const alias = ALIASES[before[0]]
85
78
  const chain = alias ? [alias.noun, ...alias.inject, ...before.slice(1)] : before
86
79
  const [noun, verb, ...rest] = chain
87
80
 
88
81
  if (chain.length === 1 && VERBS[noun]) return offer(VERBS[noun])
89
82
 
90
- // `model show <id>` keys on two words; `ask <id>` on one. Whichever matched
91
- // decides where the argument list starts.
92
83
  const paired = ARGUMENT[`${noun} ${verb}`]
93
84
  const shape = paired ?? ARGUMENT[noun]
94
85
  if (!shape) return []
@@ -109,8 +100,7 @@ complete -o default -F _mo_completion mo
109
100
  `
110
101
 
111
102
  export async function runComplete (args) {
112
- // A crash here would break the user's tab key, so an unreadable or corrupt
113
- // catalog offers nothing rather than an error.
103
+ // An unreadable catalog offers nothing rather than breaking the tab key.
114
104
  try {
115
105
  const cword = Number.parseInt(args[0], 10)
116
106
  if (!Number.isFinite(cword)) return
@@ -1,10 +1,6 @@
1
1
  import { intro, outro, select, isCancel, cancel } from '@clack/prompts'
2
2
  import { getConfig, getCuratedModels, saveConfig, catalogEntries } from '../lib/common.js'
3
3
 
4
- // CLI-only. The default model is a convenience for someone typing `mo ask`;
5
- // the library and the gate never read it. A program picks a model by id or by
6
- // tag, where the choice is explicit and reviewable — a machine inheriting a
7
- // human's terminal preference is how a call ends up on a model nobody chose.
8
4
  export async function runDefault (args = []) {
9
5
  if (args.includes('-h') || args.includes('--help')) {
10
6
  console.log(`mohdel default — the model "mo ask" uses when you name none
@@ -49,8 +45,7 @@ read it: a program names its model, or selects one by tag.`)
49
45
  }
50
46
 
51
47
  try {
52
- // Merge: the file also holds the chosen coding agent, and replacing the
53
- // object wholesale would drop it.
48
+ // The file also holds the chosen coding agent; replacing it wholesale drops that.
54
49
  await saveConfig({ ...(await getConfig()), defaultModel: selectedModelId })
55
50
  outro(`Default set to ${selectedModelId} — "mo ask" uses it when you give no model.`)
56
51
  } catch (err) {
package/src/cli/entry.js CHANGED
@@ -155,9 +155,6 @@ Write an entry with your coding agent: mo model instructions <provider>`)
155
155
  console.log(`${meta('Undo:')} mo model backup diff prev ${meta('│')} mo model backup restore prev`)
156
156
  }
157
157
 
158
- // The candidate was written by the agent, not by mohdel, so it is never
159
- // removed unasked. It has served its purpose though, and the catalog's own
160
- // backups are what an undo uses — not this file.
161
158
  if (path === '-') return
162
159
  if (args.includes('--rm')) {
163
160
  await rm(path, { force: true })
@@ -14,8 +14,6 @@ const CATALOG_DOC = 'https://github.com/clbrge/mohdel/blob/main/docs/CATALOG.md'
14
14
  export const BRIEF_FILE = 'mohdel-brief.md'
15
15
  export const BRIEF_HEADING = '# mohdel catalog entry'
16
16
 
17
- // A brief mohdel wrote before is stale, not precious — replace it. Anything
18
- // else owning the name is the user's, so write alongside rather than over it.
19
17
  export const writeBrief = async (provider) => {
20
18
  const brief = await buildBrief(provider)
21
19
  let name = BRIEF_FILE
@@ -163,9 +161,6 @@ One-shot instead of a session, where the agent supports it:
163
161
 
164
162
  const assistant = (await getConfig()).assistant || null
165
163
 
166
- // 150 lines of agent-facing markdown is not what someone at a terminal
167
- // wants. Redirected or piped — how the recipe and agents use it — stdout
168
- // still carries the brief.
169
164
  if (process.stdout.isTTY && !args.includes('--print')) {
170
165
  const { preferredAgent, briefPrompt } = await import('../lib/assistants.js')
171
166
  const { ok, meta, id: cmd } = await import('./colors.js')
package/src/cli/model.js CHANGED
@@ -6,8 +6,6 @@ import { providerOf } from '#core/model-id.js'
6
6
  import { parseJsonFlag, printAvailableFields, jsonOutput, jsonOutputOne } from './json-output.js'
7
7
  import { id, label, tag, price, meta, err, warn, ok } from './colors.js'
8
8
 
9
- // An empty catalog is the fresh-install state, not an error — but printing
10
- // nothing at all reads as a broken command.
11
9
  export const EMPTY_CATALOG = 'Catalog is empty. "mo curate <provider>" adds a provider\'s models, ' +
12
10
  'then "mo model instructions <provider>" fills in the prices.'
13
11
 
@@ -30,10 +30,7 @@ async function appendToEnvFile (key, value) {
30
30
  let content = ''
31
31
  if (existsSync(ENV_PATH)) {
32
32
  content = await readFile(ENV_PATH, 'utf8')
33
- // Replace existing line if present. The replacement is a function
34
- // because `$&`, `` $` ``, `$'` and `$1` are expanded inside a
35
- // replacement *string* — a pasted key containing any of them would
36
- // be silently rewritten before it hit disk.
33
+ // Function form: `$&`, `` $` ``, `$'` and `$1` are expanded inside a replacement string, which would rewrite a pasted key.
37
34
  const re = new RegExp(`^${escapeRegExp(key)}=.*$`, 'm')
38
35
  if (re.test(content)) {
39
36
  content = content.replace(re, () => `${key}=${value}`)
@@ -52,8 +49,6 @@ export async function runOnboard () {
52
49
  loadDefaultEnv()
53
50
  const { configured, unconfigured } = getConfiguredProviders()
54
51
 
55
- // Already set up: a status line and the step that follows from it, not a
56
- // menu. `mo --help` is the menu.
57
52
  if (configured.length > 0) {
58
53
  const curated = await getCuratedModels()
59
54
  const active = catalogEntries(curated).filter(([, s]) => !s.deprecated)
@@ -72,8 +67,6 @@ export async function runOnboard () {
72
67
  let handOver = null
73
68
  if (active.length === 0) {
74
69
  next.push([`mo curate ${first}`, 'add the models this key can reach'])
75
- // An interrupted run leaves the brief behind. Telling someone to write
76
- // one that is already sitting in front of them is the wrong step.
77
70
  const { BRIEF_FILE, BRIEF_HEADING } = await import('./instructions.js')
78
71
  const briefPath = resolve(process.cwd(), BRIEF_FILE)
79
72
  const briefReady = existsSync(briefPath) &&
@@ -166,13 +159,8 @@ export async function runOnboard () {
166
159
  // Reload env so the new key is visible, then fill the catalog.
167
160
  loadDefaultEnv()
168
161
 
169
- // OpenRouter's model list carries prices, so picking by hand is complete
170
- // there and needs no agent at all.
171
162
  const selfPricing = !!providers[selected]?.pricesFromApi
172
163
 
173
- // Where the prices are published, the models that cost nothing are the whole
174
- // offer for someone with no coding agent. The count belongs on the menu: put
175
- // it behind a search over hundreds of ids and nobody finds it.
176
164
  let api = null
177
165
  let free = []
178
166
  if (selfPricing) {
@@ -244,9 +232,6 @@ export async function runOnboard () {
244
232
  await writeCatalogBrief(selected)
245
233
  }
246
234
 
247
- // The provider API returns model ids, never prices — entries land unpriced and
248
- // `cost` stays 0 on every result until the numbers are filled in. This is the
249
- // moment the user is still in the flow.
250
235
  async function writeCatalogBrief (selected) {
251
236
  const { AGENTS, detectAssistants, preferredAgent, briefPrompt } = await import('../lib/assistants.js')
252
237
  const installed = detectAssistants()
@@ -1,9 +1,6 @@
1
1
  import { statSync } from 'node:fs'
2
2
  import { join, delimiter } from 'node:path'
3
3
 
4
- // Launch forms are the vendor-documented ones for starting a session on an
5
- // initial prompt — not the one-shot/print flags, since picking a model is a
6
- // conversation.
7
4
  const AGENTS = [
8
5
  { bin: 'claude', label: 'Claude Code', start: p => `claude ${p}` },
9
6
  { bin: 'codex', label: 'Codex CLI', start: p => `codex ${p}` },
@@ -46,9 +43,6 @@ export const launchLines = (installed = detectAssistants(), chosen = null, inden
46
43
  // one argument is the form every agent CLI accepts.
47
44
  const custom = (bin) => ({ bin, label: bin, start: p => `${bin} ${p}` })
48
45
 
49
- // Which agent the recipe should name. A stated preference beats detection —
50
- // PATH cannot see which of two installed agents someone actually uses, nor an
51
- // agent that lives in an editor.
52
46
  export const preferredAgent = (chosen, installed = detectAssistants()) => {
53
47
  if (chosen) return AGENTS.find(a => a.bin === chosen) || custom(chosen)
54
48
  return TOP.find(a => installed.includes(a.bin)) || TOP[0]
@@ -60,8 +60,6 @@ export const reviewEntry = (key, spec, catalog, { strict = false, local = null }
60
60
  }
61
61
 
62
62
  for (const issue of validate(spec, key, { strict })) {
63
- // Custom fields are namespaced by convention (docs/CATALOG.md) and
64
- // round-trip untouched, so they are not drift.
65
63
  if (issue.message === 'unknown field' && issue.field.includes(':')) continue
66
64
  if (issue.message === 'unknown field' && local?.fields[issue.field]) continue
67
65
  if (issue.severity === 'error') errors.push(`${key}: ${issue.field} — ${issue.message}`)
package/src/lib/common.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { join, sep } from 'path'
2
2
  import { homedir } from 'os'
3
3
  import { existsSync } from 'fs'
4
- import { readFile, writeFile, mkdir, copyFile, stat } from 'fs/promises'
4
+ import { readFile, writeFile, mkdir, copyFile, stat, rename, unlink } from 'fs/promises'
5
5
  import envPaths from 'env-paths'
6
6
  import { validate, stripComputed } from './schema.js'
7
7
  import { silent } from './logger.js'
@@ -81,6 +81,17 @@ const getWeek = (ms) => {
81
81
  return `${d.getFullYear()}-W${String(Math.ceil((d.getDate() + new Date(d.getFullYear(), d.getMonth(), 1).getDay()) / 7)).padStart(2, '0')}-${d.getMonth()}`
82
82
  }
83
83
 
84
+ const writeAtomic = async (filePath, contents) => {
85
+ const tmp = `${filePath}.${process.pid}.tmp`
86
+ try {
87
+ await writeFile(tmp, contents)
88
+ await rename(tmp, filePath)
89
+ } catch (err) {
90
+ await unlink(tmp).catch(() => {})
91
+ throw err
92
+ }
93
+ }
94
+
84
95
  const rotateBackup = async (filePath) => {
85
96
  if (!existsSync(filePath)) return
86
97
 
@@ -214,7 +225,7 @@ const createFileOperation = (filePath, defaultValue = {}, operationType) => {
214
225
  }
215
226
 
216
227
  const sortedData = sortObjectKeys(dataToSave)
217
- await writeFile(filePath, JSON.stringify(sortedData, null, 2))
228
+ await writeAtomic(filePath, JSON.stringify(sortedData, null, 2))
218
229
  return true
219
230
  } catch (err) {
220
231
  moduleLogger.error(`[mohdel:common] failed to save ${operationType}: ${err.message}`)
@@ -79,8 +79,7 @@ export const parseLocalConventions = (text) => {
79
79
  return { fields, tags, adding, notes: doc.notes || '' }
80
80
  }
81
81
 
82
- // Absent is the default state, not an error. A file that exists but does not
83
- // parse is broken policy — it must not read as "no conventions".
82
+ // Absent is the default state; a file that exists but does not parse is not.
84
83
  export const loadLocalConventions = async (path = LOCAL_PATH) => {
85
84
  if (!existsSync(path)) return null
86
85
  try {
@@ -146,9 +146,9 @@ const providers = {
146
146
  baseURL: 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1',
147
147
  createConfiguration: apiKey => ({ apiKey }),
148
148
  references: {
149
- pricing: 'https://www.alibabacloud.com/help/en/model-studio/models',
150
- models: 'https://www.alibabacloud.com/help/en/model-studio/models',
151
- rateLimits: 'https://www.alibabacloud.com/help/en/model-studio/rate-limit'
149
+ pricing: 'https://docs.qwencloud.com/developer-guides/getting-started/pricing',
150
+ models: 'https://www.qwencloud.com/models',
151
+ rateLimits: 'https://docs.qwencloud.com/developer-guides/administration/rate-limits'
152
152
  },
153
153
  contextSemantics: 'shared',
154
154
  outputCapStrategy: 'accept'
package/src/lib/select.js CHANGED
@@ -94,21 +94,12 @@ const replaceModel = async (modelToReplace, newCuratedKey, newModelLabel, newMod
94
94
  }
95
95
  delete curated[modelToReplace.key]
96
96
 
97
- // Preserve custom properties from the old model entry,
98
- // but explicitly exclude 'model' and 'models' fields from the old entry.
99
- // This prevents old model identifiers from polluting the new entry if the
100
- // new model's details (newModelDetails) don't specify them, ensuring consistent `upstreamIds`.
101
97
  const {
102
98
  model: _discardedOldModelIdentifier, // eslint-disable-line no-unused-vars
103
99
  models: _discardedOldModelIdentifiers, // eslint-disable-line no-unused-vars
104
100
  ...restOfOldModelDataProperties
105
101
  } = oldModelDataFromCurated || {}
106
102
 
107
- // Add new model to curated.
108
- // Merge properties: Start with applicable old properties (restOfOldModelDataProperties),
109
- // then layer new model details (newModelDetails), which includes new provider & sdk.
110
- // Finally, ensure the new label (newModelLabel) is set.
111
- // Properties in newModelDetails and newModelLabel will override any from restOfOldModelDataProperties.
112
103
  curated[newCuratedKey] = {
113
104
  ...restOfOldModelDataProperties,
114
105
  ...newModelDetails,
@@ -165,9 +156,6 @@ const isModelTrackedInCollection = (collection, providerName, modelId) => {
165
156
  return false
166
157
  }
167
158
 
168
- // Which creators a provider serves is a fact of the catalog, not a property of
169
- // the provider: a router's line-up changes week to week. Offer the ones this
170
- // provider already serves first, then every creator mohdel has a label for.
171
159
  const creatorOptions = async (providerName, guess) => {
172
160
  const catalog = await getCuratedModels()
173
161
  const served = new Set()
@@ -262,10 +250,6 @@ const filterUncurated = (models, providerName, curated, excluded) => {
262
250
  })
263
251
  }
264
252
 
265
- // A model list only carries prices when the provider publishes them
266
- // (`pricesFromApi`). Where it does, the ones that cost nothing are the whole
267
- // no-agent story, and a free-text search over hundreds of ids is no way to
268
- // find them.
269
253
  export const freeModels = (models) =>
270
254
  models.filter(m => m.inputPrice === 0 && m.outputPrice === 0)
271
255
 
@@ -278,9 +262,7 @@ const buildEntry = async (providerName, model, providerInstance) => {
278
262
  label: model.label || model.id,
279
263
  ...(info || {})
280
264
  })
281
- // Upstream lists name the provider, never the creator. An OpenRouter id is
282
- // `<vendor>/<model>`, so the vendor segment is the answer when the creator
283
- // table does not recognize the name.
265
+ // An upstream list names the provider, never the creator; an OpenRouter id carries the vendor in its first segment.
284
266
  if (!entry.creator) entry.creator = creatorFromModelId(model.id) || model.id.split('/')[0]
285
267
  return entry
286
268
  }