@namzu/zen 2.4.0 → 2.5.2

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +30 -11
  3. package/dist/catalogue/catalogue.d.ts +106 -0
  4. package/dist/catalogue/catalogue.d.ts.map +1 -0
  5. package/dist/catalogue/catalogue.js +325 -0
  6. package/dist/catalogue/catalogue.js.map +1 -0
  7. package/dist/catalogue/derive.d.ts +203 -0
  8. package/dist/catalogue/derive.d.ts.map +1 -0
  9. package/dist/catalogue/derive.js +485 -0
  10. package/dist/catalogue/derive.js.map +1 -0
  11. package/dist/catalogue/fetch.d.ts +63 -0
  12. package/dist/catalogue/fetch.d.ts.map +1 -0
  13. package/dist/catalogue/fetch.js +180 -0
  14. package/dist/catalogue/fetch.js.map +1 -0
  15. package/dist/catalogue/index.d.ts +17 -0
  16. package/dist/catalogue/index.d.ts.map +1 -0
  17. package/dist/catalogue/index.js +17 -0
  18. package/dist/catalogue/index.js.map +1 -0
  19. package/dist/client.d.ts +5 -1
  20. package/dist/client.d.ts.map +1 -1
  21. package/dist/client.js +59 -8
  22. package/dist/client.js.map +1 -1
  23. package/dist/index.d.ts +2 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +1 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/models.d.ts +6 -0
  28. package/dist/models.d.ts.map +1 -1
  29. package/dist/models.js +49 -9
  30. package/dist/models.js.map +1 -1
  31. package/dist/prompt.d.ts.map +1 -1
  32. package/dist/prompt.js +7 -0
  33. package/dist/prompt.js.map +1 -1
  34. package/dist/types.d.ts +11 -1
  35. package/dist/types.d.ts.map +1 -1
  36. package/package.json +6 -2
  37. package/src/catalogue/catalogue.ts +462 -0
  38. package/src/catalogue/derive.ts +638 -0
  39. package/src/catalogue/fetch.ts +229 -0
  40. package/src/catalogue/index.ts +37 -0
  41. package/src/client.ts +68 -9
  42. package/src/index.ts +2 -1
  43. package/src/models.ts +50 -9
  44. package/src/prompt.ts +7 -0
  45. package/src/types.ts +11 -1
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Reading the Zen catalogue's sources over the network — the one module in
3
+ * this package that does, and only when a caller invokes it. Importing it,
4
+ * constructing a provider or reading the bundled snapshot fetches nothing.
5
+ */
6
+
7
+ import type { ZenService } from '../models.js'
8
+ import {
9
+ type BuildZenCatalogueOptions,
10
+ type ZenCatalogueResult,
11
+ type ZenCatalogueSources,
12
+ buildZenCatalogue,
13
+ } from './catalogue.js'
14
+ import { SERVICE_PREFIX, ZenCatalogueSourceError } from './derive.js'
15
+
16
+ /** The branch of OpenCode's repository the two documentation pages are read from. */
17
+ export const ZEN_DOCS_REF = 'dev'
18
+ export const ZEN_MODELS_DEV_URL = 'https://models.dev/api.json'
19
+ /** The host both services' own `/models` endpoints hang off. */
20
+ export const ZEN_SERVICE_BASE = 'https://opencode.ai'
21
+
22
+ /** Byte ceilings per source. A source past its ceiling is refused, never truncated. */
23
+ export interface ZenCatalogueLimits {
24
+ /** Each documentation page. Default 1 MiB (the live pages are ~32 KB). */
25
+ readonly pageBytes?: number
26
+ /** models.dev's whole document. Default 32 MiB (measured 4.8 MB on 2026-09-22). */
27
+ readonly modelsDevBytes?: number
28
+ /** Each `/models` answer. Default 1 MiB, the driver's own `listModels` ceiling. */
29
+ readonly servedBytes?: number
30
+ }
31
+
32
+ export interface FetchZenCatalogueOptions {
33
+ /** Defaults to the global `fetch`. */
34
+ readonly fetch?: typeof fetch
35
+ /** Aborts every outstanding read; the call then rejects with the signal's reason. */
36
+ readonly signal?: AbortSignal
37
+ /** Deadline for each source read, in milliseconds. Default 15,000. */
38
+ readonly timeoutMs?: number
39
+ /** Tries per source; a 404 is never retried. Default 1. */
40
+ readonly attempts?: number
41
+ readonly limits?: ZenCatalogueLimits
42
+ /** OpenCode branch for the two pages. Default `dev`. */
43
+ readonly docsRef?: string
44
+ readonly modelsDevUrl?: string
45
+ /** Host for the two `/models` answers. Default `https://opencode.ai`. */
46
+ readonly serviceBase?: string
47
+ }
48
+
49
+ const DEFAULT_TIMEOUT_MS = 15_000
50
+ const MIB = 1024 * 1024
51
+
52
+ function docsBase(ref: string): string {
53
+ return `https://raw.githubusercontent.com/anomalyco/opencode/${encodeURIComponent(ref)}/packages/web/src/content/docs`
54
+ }
55
+
56
+ /** The models-list endpoint of one service. */
57
+ export function zenServedUrl(service: ZenService, serviceBase = ZEN_SERVICE_BASE): string {
58
+ return `${serviceBase.replace(/\/+$/, '')}${SERVICE_PREFIX[service]}models`
59
+ }
60
+
61
+ function positive(value: number | undefined, fallback: number, name: string): number {
62
+ if (value === undefined) return fallback
63
+ if (!Number.isSafeInteger(value) || value <= 0)
64
+ throw new RangeError(`${name} must be a positive integer.`)
65
+ return value
66
+ }
67
+
68
+ async function readBounded(response: Response, limit: number, url: string): Promise<string> {
69
+ const declared = Number(response.headers.get('content-length'))
70
+ if (Number.isFinite(declared) && declared > limit) {
71
+ await response.body?.cancel().catch(() => {})
72
+ throw new ZenCatalogueSourceError(
73
+ `${url} declares ${declared} bytes, past its ${limit}-byte limit.`,
74
+ )
75
+ }
76
+ const reader = response.body?.getReader()
77
+ if (!reader) throw new ZenCatalogueSourceError(`${url} answered with no body.`)
78
+ const chunks: Uint8Array[] = []
79
+ let bytes = 0
80
+ try {
81
+ while (true) {
82
+ const next = await reader.read()
83
+ if (next.done) break
84
+ bytes += next.value.byteLength
85
+ if (bytes > limit) {
86
+ throw new ZenCatalogueSourceError(`${url} exceeds its ${limit}-byte limit.`)
87
+ }
88
+ chunks.push(next.value)
89
+ }
90
+ } finally {
91
+ await reader.cancel().catch(() => {})
92
+ reader.releaseLock()
93
+ }
94
+ const body = new Uint8Array(bytes)
95
+ let offset = 0
96
+ for (const chunk of chunks) {
97
+ body.set(chunk, offset)
98
+ offset += chunk.byteLength
99
+ }
100
+ try {
101
+ return new TextDecoder('utf-8', { fatal: true }).decode(body)
102
+ } catch {
103
+ throw new ZenCatalogueSourceError(`${url} is not UTF-8 text.`)
104
+ }
105
+ }
106
+
107
+ /**
108
+ * One source, as text, inside its deadline and its byte ceiling.
109
+ *
110
+ * Redirects are refused: every source is a fixed URL that answers directly, and
111
+ * a source that starts redirecting has moved. A 404 is a branch or a path that
112
+ * moved and is not retried; a caller's abort is not retried either.
113
+ */
114
+ export async function readZenSource(
115
+ url: string,
116
+ limit: number,
117
+ options: Pick<FetchZenCatalogueOptions, 'fetch' | 'signal' | 'timeoutMs' | 'attempts'> = {},
118
+ ): Promise<string> {
119
+ const fetchFn = options.fetch ?? globalThis.fetch
120
+ const timeoutMs = positive(options.timeoutMs, DEFAULT_TIMEOUT_MS, 'timeoutMs')
121
+ const attempts = positive(options.attempts, 1, 'attempts')
122
+ let lastError: unknown
123
+ for (let attempt = 1; attempt <= attempts; attempt++) {
124
+ options.signal?.throwIfAborted()
125
+ try {
126
+ const signal = AbortSignal.any([
127
+ AbortSignal.timeout(timeoutMs),
128
+ ...(options.signal ? [options.signal] : []),
129
+ ])
130
+ const response = await fetchFn(url, { signal, redirect: 'error' })
131
+ if (response.ok) return await readBounded(response, limit, url)
132
+ await response.body?.cancel().catch(() => {})
133
+ if (response.status === 404) {
134
+ throw new ZenCatalogueSourceError(`${url} answered 404 — the branch or the path moved.`)
135
+ }
136
+ throw new Error(`${url} answered ${response.status}.`)
137
+ } catch (error) {
138
+ if (options.signal?.aborted) throw options.signal.reason
139
+ if (error instanceof ZenCatalogueSourceError) throw error
140
+ lastError = error
141
+ if (attempt < attempts) await delay(500 * attempt, options.signal)
142
+ }
143
+ }
144
+ throw new ZenCatalogueSourceError(
145
+ `${url} could not be read after ${attempts} attempt${attempts === 1 ? '' : 's'}: ${
146
+ lastError instanceof Error ? lastError.message : String(lastError)
147
+ }`,
148
+ )
149
+ }
150
+
151
+ function delay(ms: number, signal: AbortSignal | undefined): Promise<void> {
152
+ return new Promise((resolve, reject) => {
153
+ const timer = setTimeout(done, ms)
154
+ function done() {
155
+ signal?.removeEventListener('abort', onAbort)
156
+ resolve()
157
+ }
158
+ function onAbort() {
159
+ clearTimeout(timer)
160
+ reject(signal?.reason)
161
+ }
162
+ signal?.addEventListener('abort', onAbort, { once: true })
163
+ })
164
+ }
165
+
166
+ /** The two `/models` answers, as text. */
167
+ export async function fetchZenServedRosters(
168
+ options: FetchZenCatalogueOptions = {},
169
+ ): Promise<Record<ZenService, string>> {
170
+ const limit = positive(options.limits?.servedBytes, MIB, 'limits.servedBytes')
171
+ const [zen, go] = await Promise.all([
172
+ readZenSource(zenServedUrl('zen', options.serviceBase), limit, options),
173
+ readZenSource(zenServedUrl('go', options.serviceBase), limit, options),
174
+ ])
175
+ return { zen, go }
176
+ }
177
+
178
+ /**
179
+ * All five sources, as text: the two documentation pages, models.dev and the
180
+ * two `/models` answers. Rejects on the first source that cannot be read.
181
+ */
182
+ export async function fetchZenCatalogueSources(
183
+ options: FetchZenCatalogueOptions = {},
184
+ ): Promise<ZenCatalogueSources> {
185
+ const pageLimit = positive(options.limits?.pageBytes, MIB, 'limits.pageBytes')
186
+ const modelsDevLimit = positive(options.limits?.modelsDevBytes, 32 * MIB, 'limits.modelsDevBytes')
187
+ const base = docsBase(options.docsRef ?? ZEN_DOCS_REF)
188
+ const failFast = new AbortController()
189
+ const scoped: FetchZenCatalogueOptions = {
190
+ ...options,
191
+ signal: AbortSignal.any([failFast.signal, ...(options.signal ? [options.signal] : [])]),
192
+ }
193
+ const read = <T>(promise: Promise<T>): Promise<T> =>
194
+ promise.catch((error) => {
195
+ // One unreadable source decides the result, so the others stop
196
+ // downloading rather than finishing for nothing.
197
+ failFast.abort(error)
198
+ throw error
199
+ })
200
+ try {
201
+ const [zen, go, modelsDev, served] = await Promise.all([
202
+ read(readZenSource(`${base}/zen.mdx`, pageLimit, scoped)),
203
+ read(readZenSource(`${base}/go.mdx`, pageLimit, scoped)),
204
+ read(readZenSource(options.modelsDevUrl ?? ZEN_MODELS_DEV_URL, modelsDevLimit, scoped)),
205
+ read(fetchZenServedRosters(scoped)),
206
+ ])
207
+ return { docs: { zen, go }, modelsDev, served }
208
+ } catch (error) {
209
+ if (options.signal?.aborted) throw options.signal.reason
210
+ // A sibling aborted by `failFast` rejects with the first failure as its
211
+ // reason, which is the error worth reporting.
212
+ throw failFast.signal.aborted ? failFast.signal.reason : error
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Read every source and derive a catalogue from them, or reject.
218
+ *
219
+ * Opt-in by construction: nothing in this package calls it. A rejection is a
220
+ * `ZenCatalogueSourceError` for a source that could not be read or no longer
221
+ * parses, or the caller's abort reason; there is no partial result.
222
+ */
223
+ export async function fetchZenCatalogue(
224
+ options: FetchZenCatalogueOptions & BuildZenCatalogueOptions = {},
225
+ ): Promise<ZenCatalogueResult> {
226
+ const sources = await fetchZenCatalogueSources(options)
227
+ options.signal?.throwIfAborted()
228
+ return buildZenCatalogue(sources, options)
229
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * `@namzu/zen/catalogue`: derive the Zen and Go roster at run time.
3
+ *
4
+ * Opt-in. Nothing here runs unless called, and only `fetchZenCatalogue` and
5
+ * `fetchZenCatalogueSources` touch the network. A host that wants a fresher
6
+ * roster than the bundled snapshot calls `fetchZenCatalogue()`, stores the
7
+ * result if it likes, and hands it to `ZenProvider` through
8
+ * `ZenConfig.catalogue`.
9
+ *
10
+ * The parsing rules themselves (`./derive.js`) are not part of this subpath:
11
+ * they are shared with the repository's snapshot generator, which imports the
12
+ * built module directly, and are free to change shape between releases.
13
+ */
14
+
15
+ export {
16
+ type BuildZenCatalogueOptions,
17
+ ZEN_CATALOGUE_VERSION,
18
+ type ZenCatalogue,
19
+ ZenCatalogueFormatError,
20
+ type ZenCatalogueReport,
21
+ type ZenCatalogueResult,
22
+ type ZenCatalogueSources,
23
+ buildZenCatalogue,
24
+ findZenCatalogueModel,
25
+ isUnroutedZenModel,
26
+ parseZenCatalogue,
27
+ } from './catalogue.js'
28
+ export { ZenCatalogueSourceError, type ZenOmissions } from './derive.js'
29
+ export {
30
+ type FetchZenCatalogueOptions,
31
+ ZEN_DOCS_REF,
32
+ ZEN_MODELS_DEV_URL,
33
+ ZEN_SERVICE_BASE,
34
+ type ZenCatalogueLimits,
35
+ fetchZenCatalogue,
36
+ fetchZenCatalogueSources,
37
+ } from './fetch.js'
package/src/client.ts CHANGED
@@ -26,7 +26,12 @@ import {
26
26
  providerHttpError,
27
27
  providerVendorError,
28
28
  } from '@namzu/sdk'
29
- import { type ZenService, findZenModel } from './models.js'
29
+ import {
30
+ type ZenCatalogue,
31
+ findZenCatalogueModel,
32
+ isUnroutedZenModel,
33
+ } from './catalogue/catalogue.js'
34
+ import type { ZenModel, ZenService } from './models.js'
30
35
  import { createCallOptions } from './options.js'
31
36
  import { createReplayState, toReasoningBlocks } from './prompt.js'
32
37
  import type { ZenConfig, ZenGoConfig } from './types.js'
@@ -45,6 +50,22 @@ export const ZEN_CAPABILITIES: ProviderCapabilities = {
45
50
  export const ZEN_BASE_URL = 'https://opencode.ai/zen/v1'
46
51
  export const ZEN_GO_BASE_URL = 'https://opencode.ai/zen/go/v1'
47
52
 
53
+ /**
54
+ * The words a backend's own finish reason uses, in `LanguageModelV3FinishReason.raw`,
55
+ * for "the model's context window, not its output-token budget, is what ran
56
+ * out" — Anthropic's `model_context_window_exceeded` (folded, with
57
+ * `max_tokens`, into `unified: 'length'` by `@ai-sdk/anthropic`'s
58
+ * `mapAnthropicStopReason`) and the words other OpenAI-compatible backends
59
+ * use (see `@namzu/http`'s `mapOpenAIFinish`), since Zen also routes through
60
+ * `@ai-sdk/openai-compatible`.
61
+ */
62
+ const CONTEXT_WINDOW_RAW_FINISH_REASONS = new Set([
63
+ 'model_context_window_exceeded',
64
+ 'model_length',
65
+ 'context_length',
66
+ 'context_length_exceeded',
67
+ ])
68
+
48
69
  /** A provider instance belongs to one conversation; it never owns an agent loop. */
49
70
  export class ZenProvider implements LLMProvider {
50
71
  readonly id: 'zen' | 'zen-go'
@@ -64,6 +85,12 @@ export class ZenProvider implements LLMProvider {
64
85
  if (service !== 'zen' && service !== 'go') throw new Error('Unknown service.')
65
86
  if (config.protocol && !['chat', 'responses', 'messages', 'google'].includes(config.protocol))
66
87
  throw new Error('Unknown model protocol.')
88
+ if (
89
+ config.catalogue !== undefined &&
90
+ typeof config.catalogue !== 'function' &&
91
+ (typeof config.catalogue !== 'object' || config.catalogue === null)
92
+ )
93
+ throw new Error('catalogue must be a Zen catalogue or a function returning one.')
67
94
  this.id = service === 'go' ? 'zen-go' : 'zen'
68
95
  this.name = service === 'go' ? 'Zen Go' : 'Zen'
69
96
  if (config.apiKey !== undefined && typeof config.apiKey !== 'string')
@@ -111,7 +138,8 @@ export class ZenProvider implements LLMProvider {
111
138
  params.model ||
112
139
  this.config.model ||
113
140
  (this.anonymous ? 'muse-spark-1.3-contributor-free' : 'glm-5.3-flash')
114
- const info = findZenModel(this.service, model)
141
+ const catalogue = this.runtimeCatalogue()
142
+ const info = findZenCatalogueModel(catalogue, this.service, model)
115
143
  if (this.anonymous && info?.supportsAnonymousAccess !== true)
116
144
  throw new ProviderRequestError({
117
145
  providerId: this.id,
@@ -124,8 +152,9 @@ export class ZenProvider implements LLMProvider {
124
152
  throw new ProviderRequestError({
125
153
  providerId: this.id,
126
154
  kind: 'bad_request',
127
- detail:
128
- 'This model has no known wire format. Update the provider or configure protocol explicitly.',
155
+ detail: isUnroutedZenModel(catalogue, this.service, model)
156
+ ? 'The service serves this model, but no source states its wire format. Configure protocol explicitly.'
157
+ : 'This model has no known wire format. Update the provider or configure protocol explicitly.',
129
158
  })
130
159
  const route = params.providerRoute ?? { providerId: this.id, model, chainIndex: 0 }
131
160
  const controller = new AbortController()
@@ -410,6 +439,11 @@ export class ZenProvider implements LLMProvider {
410
439
  id,
411
440
  delta: {},
412
441
  finishReason,
442
+ ...(finishReason === 'length' &&
443
+ finish.finishReason.raw &&
444
+ CONTEXT_WINDOW_RAW_FINISH_REASONS.has(finish.finishReason.raw)
445
+ ? { finishDetail: 'context_window' as const }
446
+ : {}),
413
447
  usage: mapUsage(finish.usage),
414
448
  replayState: createReplayState(
415
449
  { ...params, model },
@@ -452,13 +486,27 @@ export class ZenProvider implements LLMProvider {
452
486
  throw new Error('The model catalogue returned an invalid response.')
453
487
  const result: ModelInfo[] = []
454
488
  const seen = new Set<string>()
489
+ const catalogue = this.runtimeCatalogue()
455
490
  for (const item of body.data) {
456
491
  if (!item || typeof item !== 'object' || typeof item.id !== 'string' || seen.has(item.id))
457
492
  continue
458
493
  seen.add(item.id)
459
- const model = findZenModel(this.service, item.id)
460
- if (model && (!this.anonymous || model.supportsAnonymousAccess === true))
461
- result.push({ ...model })
494
+ const model = findZenCatalogueModel(catalogue, this.service, item.id)
495
+ if (model) {
496
+ if (!this.anonymous || model.supportsAnonymousAccess === true) result.push({ ...model })
497
+ continue
498
+ }
499
+ // Served, and named by the runtime catalogue as having no known wire:
500
+ // listed as exactly that, so an operator can see it exists, and
501
+ // callable only with an explicit protocol. Never anonymous, and
502
+ // nothing about it — price, limits, tools — is invented.
503
+ if (!this.anonymous && isUnroutedZenModel(catalogue, this.service, item.id))
504
+ result.push({
505
+ id: item.id,
506
+ name: `${item.id} (no known wire format)`,
507
+ supportsToolUse: false,
508
+ supportsStreaming: true,
509
+ })
462
510
  }
463
511
  return result
464
512
  } catch (error) {
@@ -472,12 +520,23 @@ export class ZenProvider implements LLMProvider {
472
520
  model: string,
473
521
  _thinking?: ThinkingConfig,
474
522
  ): readonly ReasoningEffort[] | undefined {
475
- return findZenModel(this.service, model)?.effortLevels
523
+ return this.model(model)?.effortLevels
476
524
  }
477
525
 
478
526
  async resolveContextWindow(model: string, signal?: AbortSignal): Promise<number | undefined> {
479
527
  signal?.throwIfAborted()
480
- return findZenModel(this.service, model)?.contextWindow
528
+ return this.model(model)?.contextWindow
529
+ }
530
+
531
+ /** The injected runtime catalogue as it stands now, or undefined for the bundled one alone. */
532
+ private runtimeCatalogue(): ZenCatalogue | undefined {
533
+ const source = this.config.catalogue
534
+ return typeof source === 'function' ? source() : source
535
+ }
536
+
537
+ /** Exact lookup: the runtime catalogue first, then the bundled snapshot. */
538
+ private model(id: string): ZenModel | undefined {
539
+ return findZenCatalogueModel(this.runtimeCatalogue(), this.service, id)
481
540
  }
482
541
 
483
542
  /**
package/src/index.ts CHANGED
@@ -25,8 +25,9 @@ export {
25
25
  ZenGoProvider,
26
26
  } from './client.js'
27
27
  export type { ZenConfig, ZenGoConfig, ZenProviderConfig, ZenGoProviderConfig } from './types.js'
28
- export { getZenModels, findZenModel } from './models.js'
28
+ export { ZEN_OMITTED_MODELS, getZenModels, findZenModel } from './models.js'
29
29
  export type { ZenModel, ZenProtocol, ZenService } from './models.js'
30
+ export type { ZenCatalogue } from './catalogue/catalogue.js'
30
31
  export type {
31
32
  ChatCompletionParams,
32
33
  ModelInfo,
package/src/models.ts CHANGED
@@ -29,14 +29,19 @@ export interface ZenModel {
29
29
  /*
30
30
  * GENERATED FILE — do not edit. Regenerate with:
31
31
  *
32
- * node scripts/generate-zen-models.mjs
32
+ * pnpm --filter @namzu/zen build && node scripts/generate-zen-models.mjs
33
33
  *
34
34
  * A curation decision is an edit to src/models.review.json, and how a field is
35
- * derived is an edit to the script. Both survive regeneration; a hand edit here
36
- * does not, and the CI gate "Zen catalogue matches its source" fails on one.
35
+ * derived is an edit to src/catalogue/derive.ts. Both survive regeneration; a
36
+ * hand edit here does not.
37
37
  *
38
38
  * Refreshed: 2026-09-21
39
39
  *
40
+ * This is the BUNDLED snapshot: what the driver knows with no network. A host
41
+ * can derive a fresher roster at run time from the same sources, by the same
42
+ * rules, through `@namzu/zen/catalogue` and hand it to the provider as
43
+ * `ZenConfig.catalogue`; the CLI does that in the background on every launch.
44
+ *
40
45
  * Routes come from each service's own documentation page, as the pair
41
46
  * (endpoint, AI SDK package) that page states per model; both halves must agree
42
47
  * or the run stops. Prices come from that page's per-1M-token table at the base
@@ -53,12 +58,11 @@ export interface ZenModel {
53
58
  *
54
59
  * Not every model either service serves is carried here. A model that is
55
60
  * documented and not carried is omitted by name, with a reason, in
56
- * src/models.review.json — and the CI gate fails on any documented model that
57
- * is neither carried nor omitted, so a new upstream model is a decision someone
58
- * makes rather than a row that arrives by itself. The gate also reads what the
59
- * two services' own `/models` answers say they serve: an id served and
60
- * documented nowhere has no derivable wire, so it is reported as a decision
61
- * too, rather than dropped by the driver in silence.
61
+ * src/models.review.json, and the generator refuses to write while a
62
+ * documented model is neither carried nor omitted. It also reads what the two
63
+ * services' own `/models` answers say they serve: an id served and documented
64
+ * nowhere has no derivable wire, so it is reported as a decision too, rather
65
+ * than dropped by the driver in silence.
62
66
  *
63
67
  * Prices are estimates, not invoices: context tiers, cache, Go peak/off-peak
64
68
  * rates, subscription allowances and promotions can change the effective cost.
@@ -1253,6 +1257,43 @@ const GO_MODELS = freezeModels([
1253
1257
  },
1254
1258
  ])
1255
1259
 
1260
+ /**
1261
+ * Reviewed omissions, as `service/id`: models a source names that this
1262
+ * snapshot deliberately does not carry. A runtime refresh through
1263
+ * `@namzu/zen/catalogue` honours the same decisions by default.
1264
+ */
1265
+ export const ZEN_OMITTED_MODELS: readonly string[] = Object.freeze([
1266
+ 'go/deepseek-flash',
1267
+ 'go/glm-5',
1268
+ 'go/grok-4.5',
1269
+ 'go/hy3-preview',
1270
+ 'go/kimi-k2.5',
1271
+ 'go/mimo-v2-omni',
1272
+ 'go/mimo-v2-pro',
1273
+ 'go/mimo-v2.6-flash',
1274
+ 'go/mimo-v2.6-pro',
1275
+ 'go/minimax-m2.5',
1276
+ 'go/omen-alpha',
1277
+ 'go/qwen3.5-plus',
1278
+ 'zen/claude-sonnet-4',
1279
+ 'zen/deepseek-v4-flash-free',
1280
+ 'zen/glm-5',
1281
+ 'zen/gpt-5-codex',
1282
+ 'zen/gpt-5.1-codex',
1283
+ 'zen/gpt-5.1-codex-max',
1284
+ 'zen/gpt-5.1-codex-mini',
1285
+ 'zen/gpt-5.2-codex',
1286
+ 'zen/grok-4.7',
1287
+ 'zen/jev-1.13',
1288
+ 'zen/jev-1.13-free',
1289
+ 'zen/kimi-k2.5',
1290
+ 'zen/mimo-v2.6-flash-free',
1291
+ 'zen/minimax-m2.5',
1292
+ 'zen/muse-spark-1.2-contributor-free',
1293
+ 'zen/qwen3.7-max',
1294
+ 'zen/qwen3.7-plus',
1295
+ ])
1296
+
1256
1297
  /** Supported metadata; actual account availability is established by live discovery. */
1257
1298
  export function getZenModels(service: ZenService): readonly ZenModel[] {
1258
1299
  return service === 'zen' ? ZEN_MODELS : GO_MODELS
package/src/prompt.ts CHANGED
@@ -137,7 +137,14 @@ function prefixDigest(messages: readonly Message[]): string {
137
137
  chainIndex: message.source.chainIndex,
138
138
  },
139
139
  }),
140
+ // Bookkeeping the kernel stamps, not content the prefix is defined
141
+ // by: `id` names which durable record a message came from, and two
142
+ // requests built from the exact same prefix disagree on it whenever
143
+ // one side is a fresh, never-recorded copy of the other (or a
144
+ // fork's copy, carrying a different session's id for identical
145
+ // content).
140
146
  timestamp: undefined,
147
+ id: undefined,
141
148
  })),
142
149
  )
143
150
  }
package/src/types.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { ZenCatalogue } from './catalogue/catalogue.js'
1
2
  import type { ZenProtocol } from './models.js'
2
3
 
3
4
  export interface ZenConfig {
@@ -11,8 +12,17 @@ export interface ZenConfig {
11
12
  baseURL?: string
12
13
  /** Whole request timeout in milliseconds. Defaults to 120000. */
13
14
  timeout?: number
14
- /** Explicit wire format for a model absent from the bundled catalogue. */
15
+ /** Explicit wire format for a model absent from the catalogue. */
15
16
  protocol?: ZenProtocol
17
+ /**
18
+ * A runtime catalogue, from `@namzu/zen/catalogue`, consulted before the
19
+ * bundled snapshot for every lookup: routing, anonymous admission, listing,
20
+ * context windows and effort levels. A function is called at each lookup, so
21
+ * a host can swap in a fresher catalogue for providers it already built;
22
+ * returning `undefined` means the bundled snapshot alone. Omitted, the
23
+ * provider uses the bundled snapshot and nothing is ever fetched for it.
24
+ */
25
+ catalogue?: ZenCatalogue | (() => ZenCatalogue | undefined)
16
26
  }
17
27
 
18
28
  /** Go subscriptions require a real API key; anonymous Zen access does not apply. */