@namzu/zen 2.5.2 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +33 -15
  3. package/dist/catalogue/catalogue.d.ts.map +1 -1
  4. package/dist/catalogue/catalogue.js +13 -1
  5. package/dist/catalogue/catalogue.js.map +1 -1
  6. package/dist/catalogue/derive.d.ts +7 -1
  7. package/dist/catalogue/derive.d.ts.map +1 -1
  8. package/dist/catalogue/derive.js +69 -6
  9. package/dist/catalogue/derive.js.map +1 -1
  10. package/dist/catalogue/experimental-free.d.ts +3 -0
  11. package/dist/catalogue/experimental-free.d.ts.map +1 -0
  12. package/dist/catalogue/experimental-free.js +24 -0
  13. package/dist/catalogue/experimental-free.js.map +1 -0
  14. package/dist/catalogue/index.d.ts +1 -0
  15. package/dist/catalogue/index.d.ts.map +1 -1
  16. package/dist/catalogue/index.js +1 -0
  17. package/dist/catalogue/index.js.map +1 -1
  18. package/dist/client.d.ts.map +1 -1
  19. package/dist/client.js +275 -18
  20. package/dist/client.js.map +1 -1
  21. package/dist/experimental-identity.d.ts +7 -0
  22. package/dist/experimental-identity.d.ts.map +1 -0
  23. package/dist/experimental-identity.js +28 -0
  24. package/dist/experimental-identity.js.map +1 -0
  25. package/dist/models.d.ts +1 -1
  26. package/dist/models.d.ts.map +1 -1
  27. package/dist/models.js +130 -53
  28. package/dist/models.js.map +1 -1
  29. package/dist/options.d.ts +2 -2
  30. package/dist/options.d.ts.map +1 -1
  31. package/dist/options.js +35 -10
  32. package/dist/options.js.map +1 -1
  33. package/dist/prompt.d.ts +4 -0
  34. package/dist/prompt.d.ts.map +1 -1
  35. package/dist/prompt.js +37 -4
  36. package/dist/prompt.js.map +1 -1
  37. package/dist/types.d.ts +8 -7
  38. package/dist/types.d.ts.map +1 -1
  39. package/package.json +2 -2
  40. package/src/catalogue/catalogue.ts +15 -0
  41. package/src/catalogue/derive.ts +78 -6
  42. package/src/catalogue/experimental-free.ts +31 -0
  43. package/src/catalogue/index.ts +1 -0
  44. package/src/client.ts +314 -20
  45. package/src/experimental-identity.ts +34 -0
  46. package/src/models.review.json +5 -2
  47. package/src/models.ts +131 -54
  48. package/src/options.ts +39 -9
  49. package/src/prompt.ts +43 -4
  50. package/src/types.ts +8 -7
package/src/client.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  APICallError,
8
8
  type LanguageModelV3,
9
9
  type LanguageModelV3Content,
10
+ type LanguageModelV3Prompt,
10
11
  type LanguageModelV3StreamPart,
11
12
  type LanguageModelV3Usage,
12
13
  type SharedV3ProviderMetadata,
@@ -31,9 +32,21 @@ import {
31
32
  findZenCatalogueModel,
32
33
  isUnroutedZenModel,
33
34
  } from './catalogue/catalogue.js'
35
+ import { isDirectAnonymousZenId } from './catalogue/derive.js'
36
+ import { isExperimentalFreeZenModel } from './catalogue/experimental-free.js'
37
+ import {
38
+ EXPERIMENTAL_OPENCODE_USER_AGENT,
39
+ experimentalOpenCodeRequestId,
40
+ experimentalOpenCodeSessionId,
41
+ } from './experimental-identity.js'
34
42
  import type { ZenModel, ZenService } from './models.js'
35
43
  import { createCallOptions } from './options.js'
36
- import { createReplayState, toReasoningBlocks } from './prompt.js'
44
+ import {
45
+ createReplayState,
46
+ isGoDeepSeekV4ResponsesModel,
47
+ isMuseSparkResponsesModel,
48
+ toReasoningBlocks,
49
+ } from './prompt.js'
37
50
  import type { ZenConfig, ZenGoConfig } from './types.js'
38
51
 
39
52
  export const ZEN_CAPABILITIES: ProviderCapabilities = {
@@ -50,6 +63,219 @@ export const ZEN_CAPABILITIES: ProviderCapabilities = {
50
63
  export const ZEN_BASE_URL = 'https://opencode.ai/zen/v1'
51
64
  export const ZEN_GO_BASE_URL = 'https://opencode.ai/zen/go/v1'
52
65
 
66
+ function withoutMuseSparkEncryptedReasoning(init?: RequestInit): RequestInit {
67
+ // The pinned OpenAI adapter adds this include automatically whenever a
68
+ // reasoning model uses store:false, even if providerOptions omits it.
69
+ // Muse Spark cannot round-trip that gateway-issued ciphertext. Keep the
70
+ // request stateless and preserve every other include the adapter may add.
71
+ if (typeof init?.body !== 'string')
72
+ throw new Error('Zen Muse Spark adapter emitted a request body that cannot be checked.')
73
+ const body: unknown = JSON.parse(init.body)
74
+ if (typeof body !== 'object' || body === null || Array.isArray(body))
75
+ throw new Error('Zen Muse Spark adapter emitted an invalid request body.')
76
+ const request = body as Record<string, unknown>
77
+ if (request.include !== undefined) {
78
+ if (
79
+ !Array.isArray(request.include) ||
80
+ !request.include.every((item) => typeof item === 'string')
81
+ )
82
+ throw new Error('Zen Muse Spark adapter emitted an invalid include list.')
83
+ const include = request.include.filter((item) => item !== 'reasoning.encrypted_content')
84
+ if (include.length > 0) request.include = include
85
+ else request.include = undefined
86
+ }
87
+ return { ...init, body: JSON.stringify(request) }
88
+ }
89
+
90
+ function isGoDeepSeekV4ChatModel(service: ZenService, model: string, protocol: string): boolean {
91
+ return (
92
+ service === 'go' &&
93
+ protocol === 'chat' &&
94
+ (model === 'deepseek-v4-pro' ||
95
+ model === 'deepseek-v4.1-flash' ||
96
+ model === 'deepseek-v4-flash')
97
+ )
98
+ }
99
+
100
+ function adapterBody(init: RequestInit | undefined, model: string): Record<string, unknown> {
101
+ if (typeof init?.body !== 'string')
102
+ throw new Error(`Zen adapter emitted a non-JSON request body for ${model}.`)
103
+ const body: unknown = JSON.parse(init.body)
104
+ if (typeof body !== 'object' || body === null || Array.isArray(body))
105
+ throw new Error(`Zen adapter emitted an invalid request body for ${model}.`)
106
+ return body as Record<string, unknown>
107
+ }
108
+
109
+ function withGoDeepSeekV4ChatHistory(init: RequestInit | undefined, model: string): RequestInit {
110
+ const body = adapterBody(init, model)
111
+ if (!Array.isArray(body.messages)) throw new Error('Go DeepSeek chat request has no messages.')
112
+ for (const value of body.messages) {
113
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
114
+ throw new Error('Go DeepSeek chat request has an invalid message.')
115
+ const message = value as Record<string, unknown>
116
+ if (message.role !== 'assistant') continue
117
+ if (message.content === null) message.content = ''
118
+ if (message.reasoning_content === undefined || message.reasoning_content === null)
119
+ message.reasoning_content = ''
120
+ else if (typeof message.reasoning_content !== 'string')
121
+ throw new Error('Go DeepSeek chat request has invalid reasoning content.')
122
+ }
123
+ return { ...init, body: JSON.stringify(body) }
124
+ }
125
+
126
+ function withGoDeepSeekV4ResponsesHistory(
127
+ init: RequestInit | undefined,
128
+ model: string,
129
+ prompt: LanguageModelV3Prompt,
130
+ ): RequestInit {
131
+ const body = adapterBody(init, model)
132
+ if (!Array.isArray(body.input)) throw new Error('Go DeepSeek Responses request has no input.')
133
+ const input: Record<string, unknown>[] = []
134
+ let turn: Record<string, unknown>[] = []
135
+ const sourceTurns = prompt.filter((message) => message.role === 'assistant')
136
+ let sourceIndex = 0
137
+ const refuse = (): never => {
138
+ throw new ProviderRequestError({
139
+ providerId: 'zen-go',
140
+ kind: 'bad_request',
141
+ detail: `Go DeepSeek ${model} requires captured, nonempty reasoning_text for each previous assistant turn. This history cannot be replayed safely.`,
142
+ })
143
+ }
144
+ const flushTurn = () => {
145
+ if (turn.length === 0) return
146
+ const source = sourceTurns[sourceIndex++]
147
+ if (source?.role !== 'assistant') return refuse()
148
+ const messages = turn.filter((item) => item.role === 'assistant')
149
+ const calls = turn.filter((item) => item.type === 'function_call')
150
+ const sourceText = source.content
151
+ .filter((part) => part.type === 'text')
152
+ .map((part) => part.text)
153
+ const wireText = messages.flatMap((message) =>
154
+ Array.isArray(message.content)
155
+ ? message.content
156
+ .filter(
157
+ (part) => typeof part === 'object' && part !== null && part.type === 'output_text',
158
+ )
159
+ .map((part) => part.text)
160
+ : [],
161
+ )
162
+ const sourceCalls = source.content
163
+ .filter((part) => part.type === 'tool-call')
164
+ .map((part) => part.toolCallId)
165
+ const wireCalls = calls.map((call) => call.call_id)
166
+ if (
167
+ JSON.stringify(sourceText) !== JSON.stringify(wireText) ||
168
+ JSON.stringify(sourceCalls) !== JSON.stringify(wireCalls)
169
+ )
170
+ refuse()
171
+ const captured = source.content
172
+ .filter((part) => part.type === 'reasoning')
173
+ .map((part) => part.text)
174
+ .filter((text) => text.trim().length > 0)
175
+ .join('\n')
176
+ if (!captured) refuse()
177
+ const reasoning = {
178
+ type: 'reasoning',
179
+ summary: [],
180
+ content: [{ type: 'reasoning_text', text: captured }],
181
+ }
182
+ // The adapter can replay a native [tool-call, text] turn as a call then
183
+ // an assistant message. Go rejects a message between calls and outputs.
184
+ input.push(reasoning, ...messages, ...calls)
185
+ turn = []
186
+ }
187
+ for (const value of body.input) {
188
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
189
+ throw new Error('Go DeepSeek Responses request has an invalid input item.')
190
+ const item = value as Record<string, unknown>
191
+ if (item.type === 'reasoning' || item.type === 'function_call' || item.role === 'assistant') {
192
+ turn.push(item)
193
+ } else {
194
+ flushTurn()
195
+ input.push(item)
196
+ }
197
+ }
198
+ flushTurn()
199
+ if (sourceIndex !== sourceTurns.length) refuse()
200
+ body.input = input
201
+ return { ...init, body: JSON.stringify(body) }
202
+ }
203
+
204
+ function withGoDeepSeekReasoningTextEvents(response: Response): Response {
205
+ if (
206
+ !response.ok ||
207
+ !response.body ||
208
+ !response.headers.get('content-type')?.includes('text/event-stream')
209
+ )
210
+ return response
211
+ // @ai-sdk/openai 3.0.109 understands reasoning summary deltas but not the
212
+ // reasoning_text delta that Go DeepSeek streams. Translate that one event
213
+ // before the adapter parses SSE; the original text then enters durable replay.
214
+ const decoder = new TextDecoder()
215
+ const encoder = new TextEncoder()
216
+ let pending = ''
217
+ const rewrite = (event: string): string => {
218
+ const lines = event.split(/\r\n|\n|\r/)
219
+ const dataLines = lines.filter((line) => line.startsWith('data:'))
220
+ if (dataLines.length !== 1) return event
221
+ let value: unknown
222
+ try {
223
+ value = JSON.parse(dataLines[0]?.slice(5).trimStart() ?? '')
224
+ } catch {
225
+ return event
226
+ }
227
+ if (
228
+ typeof value !== 'object' ||
229
+ value === null ||
230
+ !('type' in value) ||
231
+ value.type !== 'response.reasoning_text.delta' ||
232
+ !('item_id' in value) ||
233
+ typeof value.item_id !== 'string' ||
234
+ !('delta' in value) ||
235
+ typeof value.delta !== 'string'
236
+ )
237
+ return event
238
+ const translated = {
239
+ ...value,
240
+ type: 'response.reasoning_summary_text.delta',
241
+ summary_index: 0,
242
+ }
243
+ return lines
244
+ .map((line) => {
245
+ if (line.startsWith('data:')) return `data: ${JSON.stringify(translated)}`
246
+ if (line.startsWith('event:')) return 'event: response.reasoning_summary_text.delta'
247
+ return line
248
+ })
249
+ .join('\n')
250
+ }
251
+ const body = response.body.pipeThrough(
252
+ new TransformStream<Uint8Array, Uint8Array>({
253
+ transform(chunk, controller) {
254
+ pending += decoder.decode(chunk, { stream: true })
255
+ while (true) {
256
+ const boundary = /\r\n\r\n|\n\n|\r\r/.exec(pending)
257
+ if (!boundary) break
258
+ const event = pending.slice(0, boundary.index)
259
+ pending = pending.slice(boundary.index + boundary[0].length)
260
+ controller.enqueue(encoder.encode(`${rewrite(event)}\n\n`))
261
+ }
262
+ },
263
+ flush(controller) {
264
+ pending += decoder.decode()
265
+ if (pending) controller.enqueue(encoder.encode(pending))
266
+ },
267
+ }),
268
+ )
269
+ const headers = new Headers(response.headers)
270
+ headers.delete('content-length')
271
+ headers.delete('content-encoding')
272
+ return new Response(body, {
273
+ status: response.status,
274
+ statusText: response.statusText,
275
+ headers,
276
+ })
277
+ }
278
+
53
279
  /**
54
280
  * The words a backend's own finish reason uses, in `LanguageModelV3FinishReason.raw`,
55
281
  * for "the model's context window, not its output-token budget, is what ran
@@ -74,7 +300,11 @@ export class ZenProvider implements LLMProvider {
74
300
  readonly sessionId: string
75
301
  private readonly baseURL: string
76
302
  private readonly timeout: number
77
- private readonly requestFetch: typeof fetch
303
+ private readonly requestFetch: (
304
+ input: Parameters<typeof fetch>[0],
305
+ init?: RequestInit,
306
+ overrides?: Readonly<Record<string, string>>,
307
+ ) => ReturnType<typeof fetch>
78
308
  private readonly apiKey: string
79
309
  private readonly anonymous: boolean
80
310
 
@@ -125,27 +355,31 @@ export class ZenProvider implements LLMProvider {
125
355
  this.timeout = config.timeout ?? 120_000
126
356
  if (!Number.isSafeInteger(this.timeout) || this.timeout <= 0)
127
357
  throw new Error('timeout must be a positive integer in milliseconds.')
128
- this.requestFetch = (input, init) => {
358
+ this.requestFetch = (input, init, overrides) => {
129
359
  const headers = new Headers(init?.headers)
130
360
  for (const [key, value] of Object.entries(attributionHeaders())) headers.set(key, value)
131
361
  headers.set('x-opencode-session', this.sessionId)
362
+ if (overrides) for (const [key, value] of Object.entries(overrides)) headers.set(key, value)
132
363
  return fetch(input, { ...init, headers, redirect: 'error' })
133
364
  }
134
365
  }
135
366
 
136
367
  async *chatStream(params: ChatCompletionParams): AsyncIterable<StreamChunk> {
137
368
  const model =
138
- params.model ||
139
- this.config.model ||
140
- (this.anonymous ? 'muse-spark-1.3-contributor-free' : 'glm-5.3-flash')
369
+ params.model || this.config.model || (this.anonymous ? 'space-bunny-free' : 'glm-5.3-flash')
141
370
  const catalogue = this.runtimeCatalogue()
142
371
  const info = findZenCatalogueModel(catalogue, this.service, model)
143
- if (this.anonymous && info?.supportsAnonymousAccess !== true)
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)
144
378
  throw new ProviderRequestError({
145
379
  providerId: this.id,
146
380
  kind: 'auth',
147
381
  detail:
148
- 'Anonymous Zen access requires a supported free model. Configure an API key for other models.',
382
+ 'Anonymous Zen access requires a listed free model with a known route. Configure an API key for other models.',
149
383
  })
150
384
  const protocol = this.config.protocol ?? info?.protocol
151
385
  if (!protocol)
@@ -156,7 +390,11 @@ export class ZenProvider implements LLMProvider {
156
390
  ? 'The service serves this model, but no source states its wire format. Configure protocol explicitly.'
157
391
  : 'This model has no known wire format. Update the provider or configure protocol explicitly.',
158
392
  })
159
- const route = params.providerRoute ?? { providerId: this.id, model, chainIndex: 0 }
393
+ const route = params.providerRoute ?? {
394
+ providerId: this.id,
395
+ model,
396
+ chainIndex: 0,
397
+ }
160
398
  const controller = new AbortController()
161
399
  const signal = AbortSignal.any([
162
400
  controller.signal,
@@ -164,11 +402,28 @@ export class ZenProvider implements LLMProvider {
164
402
  ...(params.signal ? [params.signal] : []),
165
403
  ])
166
404
  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
414
+ const modelFetch: typeof fetch = (input, init) =>
415
+ this.requestFetch(
416
+ input,
417
+ init,
418
+ experimentalHeaders
419
+ ? { ...experimentalHeaders, 'x-opencode-request': experimentalOpenCodeRequestId() }
420
+ : undefined,
421
+ )
167
422
  try {
168
423
  signal.throwIfAborted()
169
424
  let options: ReturnType<typeof createCallOptions>
170
425
  try {
171
- options = createCallOptions({ ...params, model }, route, this.service, protocol)
426
+ options = createCallOptions({ ...params, model }, route, this.service, protocol, info)
172
427
  } catch (error) {
173
428
  if (isProviderRequestError(error)) throw error
174
429
  throw new ProviderRequestError({
@@ -193,9 +448,21 @@ export class ZenProvider implements LLMProvider {
193
448
  body.generationConfig ??= {}
194
449
  delete body.generationConfig.responseSchema
195
450
  body.generationConfig.responseJsonSchema = params.responseFormat.json_schema.schema
196
- return this.requestFetch(input, { ...init, body: JSON.stringify(body) })
451
+ return modelFetch(input, {
452
+ ...init,
453
+ body: JSON.stringify(body),
454
+ })
197
455
  }
198
- return this.requestFetch(input, init)
456
+ if (isMuseSparkResponsesModel(model, protocol))
457
+ return modelFetch(input, withoutMuseSparkEncryptedReasoning(init))
458
+ if (isGoDeepSeekV4ChatModel(this.service, model, protocol))
459
+ return modelFetch(input, withGoDeepSeekV4ChatHistory(init, model))
460
+ if (isGoDeepSeekV4ResponsesModel(this.service, model, protocol))
461
+ return modelFetch(
462
+ input,
463
+ withGoDeepSeekV4ResponsesHistory(init, model, options.prompt),
464
+ ).then(withGoDeepSeekReasoningTextEvents)
465
+ return modelFetch(input, init)
199
466
  }) as typeof fetch,
200
467
  }
201
468
  let native: LanguageModelV3
@@ -225,7 +492,10 @@ export class ZenProvider implements LLMProvider {
225
492
  const texts = new Map<string, Extract<LanguageModelV3Content, { type: 'text' }>>()
226
493
  const thoughts = new Map<
227
494
  string,
228
- { index: number; value: Extract<LanguageModelV3Content, { type: 'reasoning' }> }
495
+ {
496
+ index: number
497
+ value: Extract<LanguageModelV3Content, { type: 'reasoning' }>
498
+ }
229
499
  >()
230
500
  const tools = new Map<
231
501
  string,
@@ -290,7 +560,10 @@ export class ZenProvider implements LLMProvider {
290
560
  const index = thoughts.size
291
561
  thoughts.set(part.id, { index, value })
292
562
  content.push(value)
293
- yield { id, delta: { reasoning: { index, type: 'thinking', text: '' } } }
563
+ yield {
564
+ id,
565
+ delta: { reasoning: { index, type: 'thinking', text: '' } },
566
+ }
294
567
  break
295
568
  }
296
569
  case 'reasoning-delta':
@@ -358,7 +631,9 @@ export class ZenProvider implements LLMProvider {
358
631
  )
359
632
  yield {
360
633
  id,
361
- delta: { toolCalls: [{ index: tool.index, function: { arguments: part.delta } }] },
634
+ delta: {
635
+ toolCalls: [{ index: tool.index, function: { arguments: part.delta } }],
636
+ },
362
637
  }
363
638
  break
364
639
  }
@@ -377,7 +652,12 @@ export class ZenProvider implements LLMProvider {
377
652
  throw new Error('Provider-executed tools are not configured.')
378
653
  let tool = tools.get(part.toolCallId)
379
654
  if (!tool) {
380
- tool = { index: tools.size, input: '', ended: false, value: { ...part } }
655
+ tool = {
656
+ index: tools.size,
657
+ input: '',
658
+ ended: false,
659
+ value: { ...part },
660
+ }
381
661
  tools.set(part.toolCallId, tool)
382
662
  content.push(tool.value)
383
663
  yield {
@@ -401,7 +681,9 @@ export class ZenProvider implements LLMProvider {
401
681
  if (suffix)
402
682
  yield {
403
683
  id,
404
- delta: { toolCalls: [{ index: tool.index, function: { arguments: suffix } }] },
684
+ delta: {
685
+ toolCalls: [{ index: tool.index, function: { arguments: suffix } }],
686
+ },
405
687
  }
406
688
  tool.value.input = part.input
407
689
  tool.value.providerMetadata = mergeMetadata(
@@ -409,7 +691,12 @@ export class ZenProvider implements LLMProvider {
409
691
  part.providerMetadata,
410
692
  )
411
693
  tool.ended = true
412
- yield { id, delta: { toolCallEnd: { index: tool.index, id: part.toolCallId } } }
694
+ yield {
695
+ id,
696
+ delta: {
697
+ toolCallEnd: { index: tool.index, id: part.toolCallId },
698
+ },
699
+ }
413
700
  break
414
701
  }
415
702
  case 'finish':
@@ -493,7 +780,8 @@ export class ZenProvider implements LLMProvider {
493
780
  seen.add(item.id)
494
781
  const model = findZenCatalogueModel(catalogue, this.service, item.id)
495
782
  if (model) {
496
- if (!this.anonymous || model.supportsAnonymousAccess === true) result.push({ ...model })
783
+ if (!this.anonymous || isExperimentalFreeZenModel(this.service, model))
784
+ result.push({ ...model })
497
785
  continue
498
786
  }
499
787
  // Served, and named by the runtime catalogue as having no known wire:
@@ -603,7 +891,13 @@ export class ZenProvider implements LLMProvider {
603
891
  const envelope = safeErrorEnvelope(error)
604
892
  const body = scrub(JSON.stringify(envelope))
605
893
  if (envelope.status !== undefined)
606
- return named(providerHttpError({ providerId: this.id, status: envelope.status, body }))
894
+ return named(
895
+ providerHttpError({
896
+ providerId: this.id,
897
+ status: envelope.status,
898
+ body,
899
+ }),
900
+ )
607
901
  return named(
608
902
  providerVendorError({
609
903
  providerId: this.id,
@@ -0,0 +1,34 @@
1
+ import { createHash, randomBytes } from 'node:crypto'
2
+
3
+ const ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
4
+
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'
8
+
9
+ function timeHex(value: bigint): string {
10
+ const time = Buffer.alloc(6)
11
+ for (let index = 0; index < time.length; index++) {
12
+ time[index] = Number((value >> BigInt(40 - index * 8)) & 0xffn)
13
+ }
14
+ return time.toString('hex')
15
+ }
16
+
17
+ function base62(bytes: Uint8Array): string {
18
+ return Array.from(bytes, (byte) => ALPHABET[byte % ALPHABET.length]).join('')
19
+ }
20
+
21
+ /** Stable when the CLI constructs a new provider for each turn of one conversation. */
22
+ export function experimentalOpenCodeSessionId(namzuSessionId: string): string {
23
+ const hash = createHash('sha256').update(namzuSessionId).digest()
24
+ const uuidTime = /^([0-9a-f]{8})-([0-9a-f]{4})-7[0-9a-f]{3}-/i.exec(namzuSessionId)
25
+ const milliseconds = uuidTime
26
+ ? BigInt(`0x${uuidTime[1]}${uuidTime[2]}`)
27
+ : BigInt(`0x${hash.subarray(0, 6).toString('hex')}`)
28
+ return `ses_${timeHex(~(milliseconds * 0x1000n + 1n))}${base62(hash.subarray(6, 20))}`
29
+ }
30
+
31
+ /** One OpenCode-shaped message id per outbound model request. */
32
+ export function experimentalOpenCodeRequestId(): string {
33
+ return `msg_${timeHex(BigInt(Date.now()) * 0x1000n + 1n)}${base62(randomBytes(14))}`
34
+ }
@@ -1,7 +1,8 @@
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.1, 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.",
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
+ "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.",
5
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.",
6
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.",
7
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.",
@@ -11,7 +12,9 @@
11
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.",
12
13
  "go/minimax-m2.5": "Superseded on Go by MiniMax M2.7 and M3, which are both carried. The page still lists it.",
13
14
  "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.",
14
- "go/qwen3.5-plus": "Served by Go and absent from the Go page, which routes Qwen 3.6 Plus and later. The Zen page routes this id on Messages, and one service's route is not the other's, so none is taken from it.",
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
+ "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
+ "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.",
15
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.",
16
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.",
17
20
  "zen/glm-5": "Superseded by GLM 5.1, 5.2 and 5.3, which are all carried. The page still lists it.",