@namzu/zen 3.1.0 → 4.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +33 -14
  3. package/dist/catalogue/catalogue.d.ts.map +1 -1
  4. package/dist/catalogue/catalogue.js +6 -0
  5. package/dist/catalogue/catalogue.js.map +1 -1
  6. package/dist/catalogue/derive.d.ts +15 -1
  7. package/dist/catalogue/derive.d.ts.map +1 -1
  8. package/dist/catalogue/derive.js +73 -10
  9. package/dist/catalogue/derive.js.map +1 -1
  10. package/dist/catalogue/experimental-free.d.ts +5 -0
  11. package/dist/catalogue/experimental-free.d.ts.map +1 -1
  12. package/dist/catalogue/experimental-free.js +4 -19
  13. package/dist/catalogue/experimental-free.js.map +1 -1
  14. package/dist/catalogue/fetch.d.ts +2 -0
  15. package/dist/catalogue/fetch.d.ts.map +1 -1
  16. package/dist/catalogue/fetch.js +14 -5
  17. package/dist/catalogue/fetch.js.map +1 -1
  18. package/dist/client.d.ts.map +1 -1
  19. package/dist/client.js +22 -13
  20. package/dist/client.js.map +1 -1
  21. package/dist/experimental-identity.d.ts +35 -3
  22. package/dist/experimental-identity.d.ts.map +1 -1
  23. package/dist/experimental-identity.js +69 -4
  24. package/dist/experimental-identity.js.map +1 -1
  25. package/dist/models.d.ts +2 -0
  26. package/dist/models.d.ts.map +1 -1
  27. package/dist/models.js +202 -29
  28. package/dist/models.js.map +1 -1
  29. package/dist/options.d.ts +14 -0
  30. package/dist/options.d.ts.map +1 -1
  31. package/dist/options.js +110 -9
  32. package/dist/options.js.map +1 -1
  33. package/dist/prompt.d.ts.map +1 -1
  34. package/dist/prompt.js +50 -1
  35. package/dist/prompt.js.map +1 -1
  36. package/dist/types.d.ts +2 -2
  37. package/package.json +2 -2
  38. package/src/catalogue/catalogue.ts +6 -0
  39. package/src/catalogue/derive.ts +80 -9
  40. package/src/catalogue/experimental-free.ts +4 -20
  41. package/src/catalogue/fetch.ts +16 -5
  42. package/src/client.ts +27 -20
  43. package/src/experimental-identity.ts +73 -5
  44. package/src/models.review.json +6 -15
  45. package/src/models.ts +204 -29
  46. package/src/options.ts +125 -9
  47. package/src/prompt.ts +56 -1
  48. package/src/types.ts +2 -2
@@ -1,31 +1,15 @@
1
1
  import type { ZenModel, ZenService } from '../models.js'
2
2
 
3
3
  /**
4
- * Free models with a documented Zen route that operators may try anonymously.
5
- * This is an experimental request path, separate from verified direct access.
6
- * Keep the list explicit: a zero price in an injected catalogue cannot admit
7
- * an arbitrary model or opt a caller into Zen Go.
4
+ * Free models: any model with zero input and output price
5
+ * is treated as experimentally available through the anonymous path.
6
+ * No explicit list; the runtime catalogue's price determines availability.
8
7
  */
9
- const EXPERIMENTAL_FREE_ZEN_IDS: ReadonlySet<string> = new Set([
10
- 'big-pickle',
11
- 'ling-3.0-flash-fin-free',
12
- 'longcat-2.5-preview-free',
13
- 'mimo-v2.5-free',
14
- 'muse-spark-1.3-contributor-free',
15
- 'nemotron-3-ultra-free',
16
- 'nemotron-3.5-lightning-free',
17
- 'space-bunny-free',
18
- ])
19
-
20
8
  export function isExperimentalFreeZenModel(
21
9
  service: ZenService,
22
10
  model: ZenModel | undefined,
23
11
  ): boolean {
24
12
  return (
25
- service === 'zen' &&
26
- model !== undefined &&
27
- EXPERIMENTAL_FREE_ZEN_IDS.has(model.id) &&
28
- model.inputPrice === 0 &&
29
- model.outputPrice === 0
13
+ service === 'zen' && model !== undefined && model.inputPrice === 0 && model.outputPrice === 0
30
14
  )
31
15
  }
@@ -11,7 +11,7 @@ import {
11
11
  type ZenCatalogueSources,
12
12
  buildZenCatalogue,
13
13
  } from './catalogue.js'
14
- import { SERVICE_PREFIX, ZenCatalogueSourceError } from './derive.js'
14
+ import { SERVICE_PREFIX, ZEN_SERVICES, ZenCatalogueSourceError } from './derive.js'
15
15
 
16
16
  /** The branch of OpenCode's repository the two documentation pages are read from. */
17
17
  export const ZEN_DOCS_REF = 'dev'
@@ -50,7 +50,18 @@ const DEFAULT_TIMEOUT_MS = 15_000
50
50
  const MIB = 1024 * 1024
51
51
 
52
52
  function docsBase(ref: string): string {
53
- return `https://raw.githubusercontent.com/anomalyco/opencode/${encodeURIComponent(ref)}/packages/web/src/content/docs`
53
+ // The docs site moved under `services/www`; the two pages are read from
54
+ // there now. `ZEN_SERVICES` carries each page's path under this root.
55
+ return `https://raw.githubusercontent.com/anomalyco/opencode/${encodeURIComponent(
56
+ ref,
57
+ )}/services/www/src/docs/content`
58
+ }
59
+
60
+ /** The URL of one service's documentation page. */
61
+ export function zenDocsUrl(service: ZenService, ref = ZEN_DOCS_REF): string {
62
+ const page = ZEN_SERVICES.find((entry) => entry.service === service)?.page
63
+ if (page === undefined) throw new Error(`Unknown Zen service ${String(service)}.`)
64
+ return `${docsBase(ref)}/${page}`
54
65
  }
55
66
 
56
67
  /** The models-list endpoint of one service. */
@@ -184,7 +195,7 @@ export async function fetchZenCatalogueSources(
184
195
  ): Promise<ZenCatalogueSources> {
185
196
  const pageLimit = positive(options.limits?.pageBytes, MIB, 'limits.pageBytes')
186
197
  const modelsDevLimit = positive(options.limits?.modelsDevBytes, 32 * MIB, 'limits.modelsDevBytes')
187
- const base = docsBase(options.docsRef ?? ZEN_DOCS_REF)
198
+ const ref = options.docsRef ?? ZEN_DOCS_REF
188
199
  const failFast = new AbortController()
189
200
  const scoped: FetchZenCatalogueOptions = {
190
201
  ...options,
@@ -199,8 +210,8 @@ export async function fetchZenCatalogueSources(
199
210
  })
200
211
  try {
201
212
  const [zen, go, modelsDev, served] = await Promise.all([
202
- read(readZenSource(`${base}/zen.mdx`, pageLimit, scoped)),
203
- read(readZenSource(`${base}/go.mdx`, pageLimit, scoped)),
213
+ read(readZenSource(zenDocsUrl('zen', ref), pageLimit, scoped)),
214
+ read(readZenSource(zenDocsUrl('go', ref), pageLimit, scoped)),
204
215
  read(readZenSource(options.modelsDevUrl ?? ZEN_MODELS_DEV_URL, modelsDevLimit, scoped)),
205
216
  read(fetchZenServedRosters(scoped)),
206
217
  ])
package/src/client.ts CHANGED
@@ -32,15 +32,14 @@ import {
32
32
  findZenCatalogueModel,
33
33
  isUnroutedZenModel,
34
34
  } from './catalogue/catalogue.js'
35
- import { isDirectAnonymousZenId } from './catalogue/derive.js'
36
35
  import { isExperimentalFreeZenModel } from './catalogue/experimental-free.js'
37
36
  import {
38
- EXPERIMENTAL_OPENCODE_USER_AGENT,
39
37
  experimentalOpenCodeRequestId,
40
38
  experimentalOpenCodeSessionId,
39
+ experimentalOpenCodeUserAgent,
41
40
  } from './experimental-identity.js'
42
41
  import type { ZenModel, ZenService } from './models.js'
43
- import { createCallOptions } from './options.js'
42
+ import { createCallOptions, withZenFreeTierTools } from './options.js'
44
43
  import {
45
44
  createReplayState,
46
45
  isGoDeepSeekV4ResponsesModel,
@@ -369,12 +368,10 @@ export class ZenProvider implements LLMProvider {
369
368
  params.model || this.config.model || (this.anonymous ? 'space-bunny-free' : 'glm-5.3-flash')
370
369
  const catalogue = this.runtimeCatalogue()
371
370
  const info = findZenCatalogueModel(catalogue, this.service, model)
372
- const verifiedAnonymous =
373
- isDirectAnonymousZenId(model) &&
374
- info?.supportsAnonymousAccess === true &&
375
- isExperimentalFreeZenModel(this.service, info)
376
- const experimentalAnonymous = isExperimentalFreeZenModel(this.service, info)
377
- if (this.anonymous && !verifiedAnonymous && !experimentalAnonymous)
371
+ // Every zero-price Zen model is admitted anonymously; the free-tier
372
+ // gateway then decides per request. `supportsAnonymousAccess` still
373
+ // marks the one model whose keyless path was verified directly.
374
+ if (this.anonymous && !isExperimentalFreeZenModel(this.service, info))
378
375
  throw new ProviderRequestError({
379
376
  providerId: this.id,
380
377
  kind: 'auth',
@@ -402,21 +399,30 @@ export class ZenProvider implements LLMProvider {
402
399
  ...(params.signal ? [params.signal] : []),
403
400
  ])
404
401
  let reader: ReadableStreamDefaultReader<LanguageModelV3StreamPart> | undefined
405
- const experimentalHeaders =
406
- this.anonymous && experimentalAnonymous && !verifiedAnonymous && this.baseURL === ZEN_BASE_URL
407
- ? {
408
- 'User-Agent': EXPERIMENTAL_OPENCODE_USER_AGENT,
409
- 'x-opencode-client': 'cli',
410
- 'x-opencode-project': 'global',
411
- 'x-opencode-session': experimentalOpenCodeSessionId(this.sessionId),
412
- }
413
- : undefined
402
+ // The free-tier gateway admits an anonymous request only when it is
403
+ // shaped as the OpenCode client shapes it: an OpenCode User-Agent, a
404
+ // descending `ses_` session id, and the four tool names the client
405
+ // always registers. A caller-owned base URL is not the gateway, so the
406
+ // identity stays off it.
407
+ const freeTierGate = this.anonymous && this.baseURL === ZEN_BASE_URL
408
+ const gateSession = freeTierGate
409
+ ? experimentalOpenCodeSessionId(this.sessionId)
410
+ : this.sessionId
411
+ const gateHeaders = freeTierGate
412
+ ? {
413
+ 'User-Agent': experimentalOpenCodeUserAgent(),
414
+ 'x-opencode-client': 'cli',
415
+ 'x-opencode-project': 'global',
416
+ 'x-opencode-session': gateSession,
417
+ 'x-opencode-session-id': gateSession,
418
+ }
419
+ : undefined
414
420
  const modelFetch: typeof fetch = (input, init) =>
415
421
  this.requestFetch(
416
422
  input,
417
423
  init,
418
- experimentalHeaders
419
- ? { ...experimentalHeaders, 'x-opencode-request': experimentalOpenCodeRequestId() }
424
+ gateHeaders
425
+ ? { ...gateHeaders, 'x-opencode-request': experimentalOpenCodeRequestId() }
420
426
  : undefined,
421
427
  )
422
428
  try {
@@ -432,6 +438,7 @@ export class ZenProvider implements LLMProvider {
432
438
  detail: 'The message history contains content this model protocol cannot encode.',
433
439
  })
434
440
  }
441
+ options = withZenFreeTierTools(options, freeTierGate, protocol !== 'responses')
435
442
  options.abortSignal = signal
436
443
  const settings = {
437
444
  apiKey: this.apiKey,
@@ -1,10 +1,73 @@
1
+ import { execFileSync } from 'node:child_process'
1
2
  import { createHash, randomBytes } from 'node:crypto'
2
3
 
3
4
  const ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
4
5
 
5
- /** Pinned to the OpenCode request shape observed in the 2026-09-28 trial. */
6
- export const EXPERIMENTAL_OPENCODE_USER_AGENT =
7
- 'opencode/1.18.32 ai-sdk/provider-utils/4.0.23 runtime/bun/1.3.14'
6
+ /**
7
+ * The floor the free-tier gate enforces: `OpenCode 1.18.0 or newer is required
8
+ * to use the free tier`. Anything older answers HTTP 426 `UpgradeRequired`.
9
+ * This pin is only a fallback for a host with no installed OpenCode — the
10
+ * version actually advertised is resolved from the installed CLI when one can
11
+ * be found, so this constant is never the reason a request is refused.
12
+ */
13
+ export const FALLBACK_OPENCODE_VERSION = '1.18.35'
14
+
15
+ /**
16
+ * The four tool names the free-tier gate requires in every anonymous request
17
+ * body. A request missing any one of them — or carrying an empty or absent
18
+ * `tools` — is refused with HTTP 403 `FreeTierError`. The names are the ones
19
+ * OpenCode itself always registers, so a host that already offers tools under
20
+ * these names needs nothing injected.
21
+ */
22
+ export const ZEN_FREE_TIER_TOOL_NAMES = Object.freeze(['bash', 'glob', 'grep', 'read'] as const)
23
+
24
+ /** `OpenCode <version> (…)` as the installed CLI reports it. */
25
+ export function parseOpenCodeVersion(stdout: string): string | undefined {
26
+ const match = /(\d+\.\d+\.\d+)/.exec(stdout)
27
+ return match?.[1]
28
+ }
29
+
30
+ let resolvedUserAgent: string | undefined
31
+
32
+ function readInstalledOpenCodeVersion(): string | undefined {
33
+ // A provider must not spawn a process per request, so the lookup runs once
34
+ // per process and every failure — no binary, no permission, a version that
35
+ // does not parse — falls through to the pinned floor. The stdin is closed
36
+ // so a CLI that would prompt cannot hang the caller.
37
+ try {
38
+ const output = execFileSync('opencode', ['--version'], {
39
+ timeout: 2_000,
40
+ encoding: 'utf8',
41
+ stdio: ['ignore', 'pipe', 'ignore'],
42
+ })
43
+ return parseOpenCodeVersion(output)
44
+ } catch {
45
+ return undefined
46
+ }
47
+ }
48
+
49
+ /**
50
+ * The User-Agent an anonymous Zen request advertises.
51
+ *
52
+ * Resolution order: `OPENCODE_UA` verbatim, then the installed OpenCode CLI's
53
+ * version, then {@link FALLBACK_OPENCODE_VERSION}. The free-tier gate reads the
54
+ * version out of this header and refuses anything below 1.18, so a host that
55
+ * has OpenCode installed stands in for the version it actually has.
56
+ */
57
+ export function experimentalOpenCodeUserAgent(): string {
58
+ if (resolvedUserAgent !== undefined) return resolvedUserAgent
59
+ const override = process.env.OPENCODE_UA?.trim()
60
+ const version = override || readInstalledOpenCodeVersion() || FALLBACK_OPENCODE_VERSION
61
+ resolvedUserAgent = override
62
+ ? override
63
+ : `opencode/${version} ai-sdk/provider-utils/4.0.23 runtime/bun/1.3.14`
64
+ return resolvedUserAgent
65
+ }
66
+
67
+ /** Test seam: forget the memoized User-Agent so the next call re-resolves it. */
68
+ export function resetExperimentalOpenCodeUserAgent(): void {
69
+ resolvedUserAgent = undefined
70
+ }
8
71
 
9
72
  function timeHex(value: bigint): string {
10
73
  const time = Buffer.alloc(6)
@@ -15,10 +78,15 @@ function timeHex(value: bigint): string {
15
78
  }
16
79
 
17
80
  function base62(bytes: Uint8Array): string {
18
- return Array.from(bytes, (byte) => ALPHABET[byte % ALPHABET.length]).join('')
81
+ return Array.from(bytes, (byte) => ALPHABET[byte % 62]).join('')
19
82
  }
20
83
 
21
- /** Stable when the CLI constructs a new provider for each turn of one conversation. */
84
+ /**
85
+ * A session id in the exact descending shape OpenCode emits, which is the
86
+ * shape the free-tier gate parses: `ses_`, 12 lowercase hex digits carrying
87
+ * the inverted timestamp with its low 12 bits as `0xfff − counter`, then 14
88
+ * base62 characters. The counter is 1, well inside the gate's 0–63 window.
89
+ */
22
90
  export function experimentalOpenCodeSessionId(namzuSessionId: string): string {
23
91
  const hash = createHash('sha256').update(namzuSessionId).digest()
24
92
  const uuidTime = /^([0-9a-f]{8})-([0-9a-f]{4})-7[0-9a-f]{3}-/i.exec(namzuSessionId)
@@ -1,36 +1,27 @@
1
1
  {
2
2
  "omissions": {
3
3
  "go/deepseek-flash": "Served by the Go service and named by no route row on either page; models.dev carries no entry for it either, so neither a wire nor its limits can be derived.",
4
- "go/glm-5": "Served by Go and no longer routed by the Go page, which routes GLM 5.2, 5.3 and 5.3 Flash. The Zen page still routes this id, and one service's route is not the other's, so none is taken from it.",
5
4
  "go/glm-5.1": "Still served by Go, but the current Go page states neither a route nor a price for it. The /models answer states no wire, so the old bundled route is not carried without current evidence.",
6
- "go/grok-4.5": "Served by Go and absent from the Go page, which routes Grok 4.6. models.dev states an npm package for it, and that field is deliberately not used as a route, so none is carried.",
7
- "go/hy3-preview": "Served by Go and named by no route row on either page; models.dev carries no entry for it, so neither a wire nor its limits can be derived.",
8
- "go/kimi-k2.5": "Served by Go and absent from the Go page, which routes Kimi K2.6, K2.7 Code and K3. The Zen page still routes this id, and one service's route is not the other's, so none is taken from it.",
9
- "go/mimo-v2-omni": "Served by Go and absent from the Go page, which routes MiMo V2.5 and V2.5 Pro. models.dev states a name and limits for it and no wire, so none is carried.",
10
- "go/mimo-v2-pro": "Served by Go and absent from the Go page, which routes MiMo V2.5 and V2.5 Pro. models.dev states a name and limits for it and no wire, so none is carried.",
11
- "go/mimo-v2.6-flash": "Routed by the Go page since 2026-09-21, and models.dev carries no `opencode-go` entry for it yet, so its limits cannot be derived and it is not carried.",
12
- "go/mimo-v2.6-pro": "Routed by the Go page since 2026-09-21, and models.dev carries no `opencode-go` entry for it yet, so its limits cannot be derived and it is not carried.",
13
5
  "go/minimax-m2.5": "Superseded on Go by MiniMax M2.7 and M3, which are both carried. The page still lists it.",
14
6
  "go/omen-alpha": "Served by Go and no longer routed by the Go page, which is why this release dropped it from the catalogue. No source states a wire for it any more, so a caller that needs it must name the protocol itself.",
15
- "go/qwen3.5-plus": "Served by Go and absent from the Go page, which routes Qwen 3.7 Plus, 3.8 Flash and 3.8 Max. The Zen page routes this id on Messages, and one service's route is not the other's, so none is taken from it.",
16
7
  "go/qwen3.6-plus": "Still served by Go, but the current Go page states neither a route nor a price for it. The /models answer states no wire, and the old bundled Messages route conflicts with the reference client's Chat route, so it is not carried by guessing.",
17
8
  "go/qwen3.7-max": "Still served by Go, but the current Go page states neither a route nor a price for it. The /models answer states no wire, so the old bundled route is not carried without current evidence.",
18
- "zen/claude-sonnet-4": "Served by Zen and named by no route row on either page: the Zen page routes Claude Sonnet 4.5 and 4.6. models.dev states an npm package for it, and that field is deliberately not used as a route, so none is carried.",
19
- "zen/deepseek-v4-flash-free": "Served by Zen and absent from the Zen page, which routes DeepSeek V4 Flash. models.dev states a name and limits for this free id and no wire, so none is carried.",
9
+ "zen/claude-opus-5-5-backup": "Served by the Zen /models answer as a failover twin of a routed model and named by no route row on either page. A backup id carries no route of its own, so no wire can be derived and a caller that needs it must name the protocol explicitly.",
10
+ "zen/gemini-4-argon": "Routed by the Zen page, which states neither a price row nor a free-list entry for it. A price that cannot be read is never defaulted to zero, so it is not carried — least of all anonymously.",
20
11
  "zen/glm-5": "Superseded by GLM 5.1, 5.2 and 5.3, which are all carried. The page still lists it.",
21
12
  "zen/gpt-5-codex": "Superseded Codex generation; gpt-5.3-codex and gpt-5.3-codex-spark are carried.",
22
13
  "zen/gpt-5.1-codex": "Superseded Codex generation; gpt-5.3-codex and gpt-5.3-codex-spark are carried.",
23
14
  "zen/gpt-5.1-codex-max": "Superseded Codex generation; gpt-5.3-codex and gpt-5.3-codex-spark are carried.",
24
15
  "zen/gpt-5.1-codex-mini": "Superseded Codex generation; gpt-5.3-codex and gpt-5.3-codex-spark are carried.",
25
16
  "zen/gpt-5.2-codex": "Superseded Codex generation; gpt-5.3-codex and gpt-5.3-codex-spark are carried.",
26
- "zen/grok-4.7": "Routed by the Zen page since 2026-09-21, and models.dev carries no `opencode` entry for it yet, so its limits cannot be derived and it is not carried.",
27
17
  "zen/jev-1.13": "Routed by the Zen page on an endpoint whose AI SDK package column carries a dash, the marker that page uses for a cell with no value: no wire is stated for it, so none can be carried. The service serves the id, and models.dev carries a name and limits for it and no package either.",
28
18
  "zen/jev-1.13-free": "Routed by the Zen page on an endpoint whose AI SDK package column carries a dash, the marker that page uses for a cell with no value: no wire is stated for it, so none can be carried. The service serves the id and the page's free-model list names it, and models.dev carries a name and limits for it and no package either.",
29
19
  "zen/kimi-k2.5": "Superseded by Kimi K2.6, K2.7 Code and K3, which are all carried.",
30
- "zen/mimo-v2.6-flash-free": "Routed by the Zen page since 2026-09-21, and models.dev carries no `opencode` entry for it yet, so its limits cannot be derived and it is not carried.",
31
20
  "zen/minimax-m2.5": "Superseded by MiniMax M2.7 and M3, which are both carried.",
21
+ "zen/mistral-large-4": "Routed by the Zen page, which states neither a price row nor a free-list entry for it. A price that cannot be read is never defaulted to zero, so it is not carried.",
32
22
  "zen/muse-spark-1.2-contributor-free": "Served by Zen under an id no page routes: the Zen page's free-model list names the 1.3 contributor model, and Go's 1.2 contributor row is a different id on the other service. models.dev states an npm package for it, and that field is deliberately not used as a route, so none is carried.",
33
- "zen/qwen3.7-max": "models.dev carries no entry for it, so its context window, limits and tool support cannot be derived and are never invented.",
34
- "zen/qwen3.7-plus": "models.dev carries no entry for it, so its context window, limits and tool support cannot be derived and are never invented."
23
+ "zen/qwen3.7-max": "Routed by the Zen page, but models.dev carries no entry for it and the Zen service's /models answer does not serve it, so nothing states that it can be called and it is not carried. It is carried with conservative limits as soon as the service serves it.",
24
+ "zen/qwen3.7-plus": "Routed by the Zen page, but models.dev carries no entry for it and the Zen service's /models answer does not serve it, so nothing states that it can be called and it is not carried. It is carried with conservative limits as soon as the service serves it.",
25
+ "zen/test": "Served by the Zen /models answer and named by no route row on either page, and its id reads as an endpoint fixture rather than a model. No wire can be derived, and nothing about it should ever be carried or admitted anonymously."
35
26
  }
36
27
  }
package/src/models.ts CHANGED
@@ -22,6 +22,8 @@ export interface ZenModel {
22
22
  readonly supportsStreaming: boolean
23
23
  /** True only for verified direct-anonymous Zen models; zero price does not grant access. */
24
24
  readonly supportsAnonymousAccess?: boolean
25
+ /** False: the limits are conservative placeholders models.dev has not published. */
26
+ readonly limitsVerified?: false
25
27
  /** Exact advertised selectable effort levels; empty means no effort selector. */
26
28
  readonly effortLevels?: readonly ReasoningEffort[]
27
29
  }
@@ -35,7 +37,7 @@ export interface ZenModel {
35
37
  * derived is an edit to src/catalogue/derive.ts. Both survive regeneration; a
36
38
  * hand edit here does not.
37
39
  *
38
- * Refreshed: 2026-09-28
40
+ * Refreshed: 2026-10-13
39
41
  *
40
42
  * This is the BUNDLED snapshot: what the driver knows with no network. A host
41
43
  * can derive a fresher roster at run time from the same sources, by the same
@@ -116,6 +118,19 @@ const ZEN_MODELS = freezeModels([
116
118
  supportsStreaming: true,
117
119
  effortLevels: ['none', 'low', 'medium', 'high', 'xhigh', 'max'],
118
120
  },
121
+ {
122
+ id: 'gpt-6.1-sol',
123
+ name: 'GPT 6.1 Sol',
124
+ protocol: 'responses',
125
+ contextWindow: 1050000,
126
+ maxOutputTokens: 128000,
127
+ inputModalities: ['text', 'image', 'document'],
128
+ inputPrice: 2.0,
129
+ outputPrice: 10.0,
130
+ supportsToolUse: true,
131
+ supportsStreaming: true,
132
+ effortLevels: ['low', 'medium', 'high', 'xhigh', 'max'],
133
+ },
119
134
  {
120
135
  id: 'gpt-6-luna',
121
136
  name: 'GPT 6 Luna',
@@ -428,6 +443,19 @@ const ZEN_MODELS = freezeModels([
428
443
  supportsStreaming: true,
429
444
  effortLevels: ['low', 'medium', 'high'],
430
445
  },
446
+ {
447
+ id: 'claude-sonnet-5-5',
448
+ name: 'Claude Sonnet 5.5',
449
+ protocol: 'messages',
450
+ contextWindow: 1000000,
451
+ maxOutputTokens: 128000,
452
+ inputModalities: ['text', 'image', 'document'],
453
+ inputPrice: 2.0,
454
+ outputPrice: 10.0,
455
+ supportsToolUse: true,
456
+ supportsStreaming: true,
457
+ effortLevels: ['low', 'medium', 'high', 'xhigh', 'max'],
458
+ },
431
459
  {
432
460
  id: 'claude-sonnet-5',
433
461
  name: 'Claude Sonnet 5',
@@ -467,6 +495,19 @@ const ZEN_MODELS = freezeModels([
467
495
  supportsStreaming: true,
468
496
  effortLevels: [],
469
497
  },
498
+ {
499
+ id: 'claude-haiku-5-5',
500
+ name: 'Claude Haiku 5.5',
501
+ protocol: 'messages',
502
+ contextWindow: 1000000,
503
+ maxOutputTokens: 128000,
504
+ inputModalities: ['text', 'image', 'document'],
505
+ inputPrice: 0.1,
506
+ outputPrice: 0.5,
507
+ supportsToolUse: true,
508
+ supportsStreaming: true,
509
+ effortLevels: ['low', 'medium', 'high', 'xhigh', 'max'],
510
+ },
470
511
  {
471
512
  id: 'claude-haiku-4-5',
472
513
  name: 'Claude Haiku 4.5',
@@ -571,6 +612,19 @@ const ZEN_MODELS = freezeModels([
571
612
  supportsStreaming: true,
572
613
  effortLevels: ['minimal', 'low', 'medium', 'high'],
573
614
  },
615
+ {
616
+ id: 'grok-4.7',
617
+ name: 'Grok 4.7',
618
+ protocol: 'responses',
619
+ contextWindow: 500000,
620
+ maxOutputTokens: 500000,
621
+ inputModalities: ['text', 'image', 'document'],
622
+ inputPrice: 2.0,
623
+ outputPrice: 6.0,
624
+ supportsToolUse: true,
625
+ supportsStreaming: true,
626
+ effortLevels: ['low', 'medium', 'high', 'xhigh'],
627
+ },
574
628
  {
575
629
  id: 'grok-4.6',
576
630
  name: 'Grok 4.6',
@@ -884,6 +938,19 @@ const ZEN_MODELS = freezeModels([
884
938
  supportsStreaming: true,
885
939
  effortLevels: ['low', 'medium', 'high', 'xhigh', 'max'],
886
940
  },
941
+ {
942
+ id: 'cat-jam-free',
943
+ name: 'Cat Jam Free',
944
+ protocol: 'chat',
945
+ contextWindow: 262144,
946
+ maxOutputTokens: 131072,
947
+ inputModalities: ['text'],
948
+ inputPrice: 0,
949
+ outputPrice: 0,
950
+ supportsToolUse: true,
951
+ supportsStreaming: true,
952
+ effortLevels: ['low', 'medium', 'high', 'xhigh', 'max'],
953
+ },
887
954
  {
888
955
  id: 'longcat-2.5-preview-free',
889
956
  name: 'LongCat 2.5 Preview Free',
@@ -897,6 +964,45 @@ const ZEN_MODELS = freezeModels([
897
964
  supportsStreaming: true,
898
965
  effortLevels: [],
899
966
  },
967
+ {
968
+ id: 'step-5-preview-free',
969
+ name: 'Step 5 Preview Free',
970
+ protocol: 'chat',
971
+ contextWindow: 1000000,
972
+ maxOutputTokens: 65536,
973
+ inputModalities: ['text', 'image'],
974
+ inputPrice: 0,
975
+ outputPrice: 0,
976
+ supportsToolUse: true,
977
+ supportsStreaming: true,
978
+ effortLevels: ['low', 'medium', 'high'],
979
+ },
980
+ {
981
+ id: 'exo-free',
982
+ name: 'Exo Free',
983
+ protocol: 'chat',
984
+ contextWindow: 1048576,
985
+ maxOutputTokens: 131072,
986
+ inputModalities: ['text', 'image'],
987
+ inputPrice: 0,
988
+ outputPrice: 0,
989
+ supportsToolUse: true,
990
+ supportsStreaming: true,
991
+ effortLevels: ['high'],
992
+ },
993
+ {
994
+ id: 'mimo-v2.6-flash-free',
995
+ name: 'MiMo-V2.6-Flash Free',
996
+ protocol: 'chat',
997
+ contextWindow: 200000,
998
+ maxOutputTokens: 32000,
999
+ inputModalities: ['text', 'image'],
1000
+ inputPrice: 0,
1001
+ outputPrice: 0,
1002
+ supportsToolUse: true,
1003
+ supportsStreaming: true,
1004
+ effortLevels: [],
1005
+ },
900
1006
  {
901
1007
  id: 'mimo-v2.5-free',
902
1008
  name: 'MiMo-V2.5 Free',
@@ -910,6 +1016,19 @@ const ZEN_MODELS = freezeModels([
910
1016
  supportsStreaming: true,
911
1017
  effortLevels: [],
912
1018
  },
1019
+ {
1020
+ id: 'ling-3.1-flash-free',
1021
+ name: 'Ling 3.1 Flash Free',
1022
+ protocol: 'chat',
1023
+ contextWindow: 262144,
1024
+ maxOutputTokens: 32768,
1025
+ inputModalities: ['text'],
1026
+ inputPrice: 0,
1027
+ outputPrice: 0,
1028
+ supportsToolUse: true,
1029
+ supportsStreaming: true,
1030
+ effortLevels: [],
1031
+ },
913
1032
  {
914
1033
  id: 'ling-3.0-flash-fin-free',
915
1034
  name: 'Ling 3.0 Flash Fin Free',
@@ -1017,6 +1136,19 @@ const GO_MODELS = freezeModels([
1017
1136
  supportsStreaming: true,
1018
1137
  effortLevels: ['none', 'low', 'medium', 'high', 'xhigh', 'max'],
1019
1138
  },
1139
+ {
1140
+ id: 'claude-haiku-5-5',
1141
+ name: 'Claude Haiku 5.5',
1142
+ protocol: 'messages',
1143
+ contextWindow: 1000000,
1144
+ maxOutputTokens: 128000,
1145
+ inputModalities: ['text', 'image', 'document'],
1146
+ inputPrice: 0.1,
1147
+ outputPrice: 0.5,
1148
+ supportsToolUse: true,
1149
+ supportsStreaming: true,
1150
+ effortLevels: ['low', 'medium', 'high', 'xhigh', 'max'],
1151
+ },
1020
1152
  {
1021
1153
  id: 'glm-5.3-flash',
1022
1154
  name: 'GLM-5.3-Flash',
@@ -1108,19 +1240,6 @@ const GO_MODELS = freezeModels([
1108
1240
  supportsStreaming: true,
1109
1241
  effortLevels: [],
1110
1242
  },
1111
- {
1112
- id: 'longcat-2.5-preview-free',
1113
- name: 'LongCat 2.5 Preview Free',
1114
- protocol: 'chat',
1115
- contextWindow: 1000000,
1116
- maxOutputTokens: 131072,
1117
- inputModalities: ['text', 'image'],
1118
- inputPrice: 0,
1119
- outputPrice: 0,
1120
- supportsToolUse: true,
1121
- supportsStreaming: true,
1122
- effortLevels: [],
1123
- },
1124
1243
  {
1125
1244
  id: 'deepseek-v4.1-flash',
1126
1245
  name: 'DeepSeek V4.1 Flash',
@@ -1173,6 +1292,32 @@ const GO_MODELS = freezeModels([
1173
1292
  supportsStreaming: true,
1174
1293
  effortLevels: ['low', 'high', 'max'],
1175
1294
  },
1295
+ {
1296
+ id: 'mimo-v2.6-flash',
1297
+ name: 'MiMo-V2.6-Flash',
1298
+ protocol: 'chat',
1299
+ contextWindow: 1048576,
1300
+ maxOutputTokens: 131072,
1301
+ inputModalities: ['text', 'image'],
1302
+ inputPrice: 0.14,
1303
+ outputPrice: 0.28,
1304
+ supportsToolUse: true,
1305
+ supportsStreaming: true,
1306
+ effortLevels: [],
1307
+ },
1308
+ {
1309
+ id: 'mimo-v2.6-pro',
1310
+ name: 'MiMo-V2.6-Pro',
1311
+ protocol: 'chat',
1312
+ contextWindow: 1048576,
1313
+ maxOutputTokens: 131072,
1314
+ inputModalities: ['text', 'image'],
1315
+ inputPrice: 0.435,
1316
+ outputPrice: 0.87,
1317
+ supportsToolUse: true,
1318
+ supportsStreaming: true,
1319
+ effortLevels: [],
1320
+ },
1176
1321
  {
1177
1322
  id: 'mimo-v2.5',
1178
1323
  name: 'MiMo-V2.5',
@@ -1317,12 +1462,51 @@ const GO_MODELS = freezeModels([
1317
1462
  effortLevels: ['none', 'low', 'high'],
1318
1463
  },
1319
1464
  {
1320
- id: 'space-bunny-free',
1321
- name: 'Space Bunny Free',
1465
+ id: 'space-bunny',
1466
+ name: 'Space Bunny',
1322
1467
  protocol: 'chat',
1323
1468
  contextWindow: 1048576,
1324
1469
  maxOutputTokens: 524288,
1325
1470
  inputModalities: ['text', 'image'],
1471
+ inputPrice: 0.15,
1472
+ outputPrice: 0.6,
1473
+ supportsToolUse: true,
1474
+ supportsStreaming: true,
1475
+ effortLevels: ['low', 'medium', 'high', 'xhigh', 'max'],
1476
+ },
1477
+ {
1478
+ id: 'longcat-2.5-preview-free',
1479
+ name: 'LongCat 2.5 Preview Free',
1480
+ protocol: 'chat',
1481
+ contextWindow: 1000000,
1482
+ maxOutputTokens: 131072,
1483
+ inputModalities: ['text', 'image'],
1484
+ inputPrice: 0,
1485
+ outputPrice: 0,
1486
+ supportsToolUse: true,
1487
+ supportsStreaming: true,
1488
+ effortLevels: [],
1489
+ },
1490
+ {
1491
+ id: 'step-5-preview-free',
1492
+ name: 'Step 5 Preview Free',
1493
+ protocol: 'chat',
1494
+ contextWindow: 1000000,
1495
+ maxOutputTokens: 65536,
1496
+ inputModalities: ['text', 'image'],
1497
+ inputPrice: 0,
1498
+ outputPrice: 0,
1499
+ supportsToolUse: true,
1500
+ supportsStreaming: true,
1501
+ effortLevels: ['low', 'medium', 'high'],
1502
+ },
1503
+ {
1504
+ id: 'cat-jam-free',
1505
+ name: 'Cat Jam Free',
1506
+ protocol: 'chat',
1507
+ contextWindow: 262144,
1508
+ maxOutputTokens: 131072,
1509
+ inputModalities: ['text'],
1326
1510
  inputPrice: 0,
1327
1511
  outputPrice: 0,
1328
1512
  supportsToolUse: true,
@@ -1338,37 +1522,28 @@ const GO_MODELS = freezeModels([
1338
1522
  */
1339
1523
  export const ZEN_OMITTED_MODELS: readonly string[] = Object.freeze([
1340
1524
  'go/deepseek-flash',
1341
- 'go/glm-5',
1342
1525
  'go/glm-5.1',
1343
- 'go/grok-4.5',
1344
- 'go/hy3-preview',
1345
- 'go/kimi-k2.5',
1346
- 'go/mimo-v2-omni',
1347
- 'go/mimo-v2-pro',
1348
- 'go/mimo-v2.6-flash',
1349
- 'go/mimo-v2.6-pro',
1350
1526
  'go/minimax-m2.5',
1351
1527
  'go/omen-alpha',
1352
- 'go/qwen3.5-plus',
1353
1528
  'go/qwen3.6-plus',
1354
1529
  'go/qwen3.7-max',
1355
- 'zen/claude-sonnet-4',
1356
- 'zen/deepseek-v4-flash-free',
1530
+ 'zen/claude-opus-5-5-backup',
1531
+ 'zen/gemini-4-argon',
1357
1532
  'zen/glm-5',
1358
1533
  'zen/gpt-5-codex',
1359
1534
  'zen/gpt-5.1-codex',
1360
1535
  'zen/gpt-5.1-codex-max',
1361
1536
  'zen/gpt-5.1-codex-mini',
1362
1537
  'zen/gpt-5.2-codex',
1363
- 'zen/grok-4.7',
1364
1538
  'zen/jev-1.13',
1365
1539
  'zen/jev-1.13-free',
1366
1540
  'zen/kimi-k2.5',
1367
- 'zen/mimo-v2.6-flash-free',
1368
1541
  'zen/minimax-m2.5',
1542
+ 'zen/mistral-large-4',
1369
1543
  'zen/muse-spark-1.2-contributor-free',
1370
1544
  'zen/qwen3.7-max',
1371
1545
  'zen/qwen3.7-plus',
1546
+ 'zen/test',
1372
1547
  ])
1373
1548
 
1374
1549
  /** Supported metadata; actual account availability is established by live discovery. */