@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.
- package/CHANGELOG.md +16 -0
- package/README.md +33 -15
- package/dist/catalogue/catalogue.d.ts.map +1 -1
- package/dist/catalogue/catalogue.js +13 -1
- package/dist/catalogue/catalogue.js.map +1 -1
- package/dist/catalogue/derive.d.ts +7 -1
- package/dist/catalogue/derive.d.ts.map +1 -1
- package/dist/catalogue/derive.js +69 -6
- package/dist/catalogue/derive.js.map +1 -1
- package/dist/catalogue/experimental-free.d.ts +3 -0
- package/dist/catalogue/experimental-free.d.ts.map +1 -0
- package/dist/catalogue/experimental-free.js +24 -0
- package/dist/catalogue/experimental-free.js.map +1 -0
- package/dist/catalogue/index.d.ts +1 -0
- package/dist/catalogue/index.d.ts.map +1 -1
- package/dist/catalogue/index.js +1 -0
- package/dist/catalogue/index.js.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +275 -18
- package/dist/client.js.map +1 -1
- package/dist/experimental-identity.d.ts +7 -0
- package/dist/experimental-identity.d.ts.map +1 -0
- package/dist/experimental-identity.js +28 -0
- package/dist/experimental-identity.js.map +1 -0
- package/dist/models.d.ts +1 -1
- package/dist/models.d.ts.map +1 -1
- package/dist/models.js +130 -53
- package/dist/models.js.map +1 -1
- package/dist/options.d.ts +2 -2
- package/dist/options.d.ts.map +1 -1
- package/dist/options.js +35 -10
- package/dist/options.js.map +1 -1
- package/dist/prompt.d.ts +4 -0
- package/dist/prompt.d.ts.map +1 -1
- package/dist/prompt.js +37 -4
- package/dist/prompt.js.map +1 -1
- package/dist/types.d.ts +8 -7
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/catalogue/catalogue.ts +15 -0
- package/src/catalogue/derive.ts +78 -6
- package/src/catalogue/experimental-free.ts +31 -0
- package/src/catalogue/index.ts +1 -0
- package/src/client.ts +314 -20
- package/src/experimental-identity.ts +34 -0
- package/src/models.review.json +5 -2
- package/src/models.ts +131 -54
- package/src/options.ts +39 -9
- package/src/prompt.ts +43 -4
- 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 {
|
|
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:
|
|
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
|
-
|
|
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
|
|
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 ?? {
|
|
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
|
|
451
|
+
return modelFetch(input, {
|
|
452
|
+
...init,
|
|
453
|
+
body: JSON.stringify(body),
|
|
454
|
+
})
|
|
197
455
|
}
|
|
198
|
-
|
|
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
|
-
{
|
|
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 {
|
|
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: {
|
|
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 = {
|
|
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: {
|
|
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 {
|
|
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 ||
|
|
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(
|
|
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
|
+
}
|
package/src/models.review.json
CHANGED
|
@@ -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.
|
|
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.
|
|
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.",
|