@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.
- package/CHANGELOG.md +28 -0
- package/README.md +30 -11
- package/dist/catalogue/catalogue.d.ts +106 -0
- package/dist/catalogue/catalogue.d.ts.map +1 -0
- package/dist/catalogue/catalogue.js +325 -0
- package/dist/catalogue/catalogue.js.map +1 -0
- package/dist/catalogue/derive.d.ts +203 -0
- package/dist/catalogue/derive.d.ts.map +1 -0
- package/dist/catalogue/derive.js +485 -0
- package/dist/catalogue/derive.js.map +1 -0
- package/dist/catalogue/fetch.d.ts +63 -0
- package/dist/catalogue/fetch.d.ts.map +1 -0
- package/dist/catalogue/fetch.js +180 -0
- package/dist/catalogue/fetch.js.map +1 -0
- package/dist/catalogue/index.d.ts +17 -0
- package/dist/catalogue/index.d.ts.map +1 -0
- package/dist/catalogue/index.js +17 -0
- package/dist/catalogue/index.js.map +1 -0
- package/dist/client.d.ts +5 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +59 -8
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/models.d.ts +6 -0
- package/dist/models.d.ts.map +1 -1
- package/dist/models.js +49 -9
- package/dist/models.js.map +1 -1
- package/dist/prompt.d.ts.map +1 -1
- package/dist/prompt.js +7 -0
- package/dist/prompt.js.map +1 -1
- package/dist/types.d.ts +11 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -2
- package/src/catalogue/catalogue.ts +462 -0
- package/src/catalogue/derive.ts +638 -0
- package/src/catalogue/fetch.ts +229 -0
- package/src/catalogue/index.ts +37 -0
- package/src/client.ts +68 -9
- package/src/index.ts +2 -1
- package/src/models.ts +50 -9
- package/src/prompt.ts +7 -0
- 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 {
|
|
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
|
|
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
|
-
'
|
|
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 =
|
|
460
|
-
if (model
|
|
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
|
|
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
|
|
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
|
|
36
|
-
*
|
|
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
|
|
57
|
-
* is neither carried nor omitted
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
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
|
|
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. */
|