@namzu/zen 1.0.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 +61 -0
- package/LICENSE.md +110 -0
- package/README.md +142 -0
- package/dist/client.d.ts +30 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +517 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/models.d.ts +29 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.js +1170 -0
- package/dist/models.js.map +1 -0
- package/dist/options.d.ts +5 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +221 -0
- package/dist/options.js.map +1 -0
- package/dist/prompt.d.ts +9 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +376 -0
- package/dist/prompt.js.map +1 -0
- package/dist/types.d.ts +26 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +80 -0
- package/src/client.ts +585 -0
- package/src/index.ts +36 -0
- package/src/models.ts +1199 -0
- package/src/options.ts +259 -0
- package/src/prompt.ts +456 -0
- package/src/types.ts +29 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import { createAnthropic } from '@ai-sdk/anthropic'
|
|
3
|
+
import { createGoogleGenerativeAI } from '@ai-sdk/google'
|
|
4
|
+
import { createOpenAI } from '@ai-sdk/openai'
|
|
5
|
+
import { createOpenAICompatible } from '@ai-sdk/openai-compatible'
|
|
6
|
+
import {
|
|
7
|
+
APICallError,
|
|
8
|
+
type LanguageModelV3,
|
|
9
|
+
type LanguageModelV3Content,
|
|
10
|
+
type LanguageModelV3StreamPart,
|
|
11
|
+
type LanguageModelV3Usage,
|
|
12
|
+
type SharedV3ProviderMetadata,
|
|
13
|
+
} from '@ai-sdk/provider'
|
|
14
|
+
import {
|
|
15
|
+
type ChatCompletionParams,
|
|
16
|
+
type LLMProvider,
|
|
17
|
+
type ModelInfo,
|
|
18
|
+
type ProviderCapabilities,
|
|
19
|
+
ProviderRequestError,
|
|
20
|
+
type ReasoningEffort,
|
|
21
|
+
type StreamChunk,
|
|
22
|
+
type ThinkingConfig,
|
|
23
|
+
type TokenUsage,
|
|
24
|
+
attributionHeaders,
|
|
25
|
+
isProviderRequestError,
|
|
26
|
+
providerHttpError,
|
|
27
|
+
providerVendorError,
|
|
28
|
+
} from '@namzu/sdk'
|
|
29
|
+
import { type ZenService, findZenModel } from './models.js'
|
|
30
|
+
import { createCallOptions } from './options.js'
|
|
31
|
+
import { createReplayState, toReasoningBlocks } from './prompt.js'
|
|
32
|
+
import type { ZenConfig, ZenGoConfig } from './types.js'
|
|
33
|
+
|
|
34
|
+
export const ZEN_CAPABILITIES: ProviderCapabilities = {
|
|
35
|
+
supportsTools: true,
|
|
36
|
+
supportsStreaming: true,
|
|
37
|
+
supportsFunctionCalling: true,
|
|
38
|
+
supportsNativeStructuredOutput: true,
|
|
39
|
+
supportsVision: true,
|
|
40
|
+
supportsDocuments: true,
|
|
41
|
+
supportsToolResultImages: true,
|
|
42
|
+
supportsToolResultDocuments: true,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const ZEN_BASE_URL = 'https://opencode.ai/zen/v1'
|
|
46
|
+
export const ZEN_GO_BASE_URL = 'https://opencode.ai/zen/go/v1'
|
|
47
|
+
|
|
48
|
+
/** A provider instance belongs to one conversation; it never owns an agent loop. */
|
|
49
|
+
export class ZenProvider implements LLMProvider {
|
|
50
|
+
readonly id: 'zen' | 'zen-go'
|
|
51
|
+
readonly name: string
|
|
52
|
+
readonly capabilities = ZEN_CAPABILITIES
|
|
53
|
+
readonly sessionId: string
|
|
54
|
+
private readonly baseURL: string
|
|
55
|
+
private readonly timeout: number
|
|
56
|
+
private readonly requestFetch: typeof fetch
|
|
57
|
+
private readonly apiKey: string
|
|
58
|
+
private readonly anonymous: boolean
|
|
59
|
+
|
|
60
|
+
constructor(
|
|
61
|
+
private readonly config: ZenConfig = {},
|
|
62
|
+
readonly service: ZenService = 'zen',
|
|
63
|
+
) {
|
|
64
|
+
if (service !== 'zen' && service !== 'go') throw new Error('Unknown service.')
|
|
65
|
+
if (config.protocol && !['chat', 'responses', 'messages', 'google'].includes(config.protocol))
|
|
66
|
+
throw new Error('Unknown model protocol.')
|
|
67
|
+
this.id = service === 'go' ? 'zen-go' : 'zen'
|
|
68
|
+
this.name = service === 'go' ? 'Zen Go' : 'Zen'
|
|
69
|
+
if (config.apiKey !== undefined && typeof config.apiKey !== 'string')
|
|
70
|
+
throw new Error('apiKey must be a string.')
|
|
71
|
+
const apiKey = config.apiKey?.trim()
|
|
72
|
+
this.anonymous = !apiKey || apiKey === 'public'
|
|
73
|
+
if (service === 'go' && this.anonymous) throw new Error('Zen Go requires an API key.')
|
|
74
|
+
// OpenCode uses this non-secret sentinel for its documented anonymous models.
|
|
75
|
+
this.apiKey = apiKey || 'public'
|
|
76
|
+
this.sessionId = config.sessionId ?? randomUUID()
|
|
77
|
+
if (
|
|
78
|
+
!this.sessionId.trim() ||
|
|
79
|
+
/[\r\n]/.test(this.sessionId) ||
|
|
80
|
+
Buffer.byteLength(this.sessionId) > 512
|
|
81
|
+
)
|
|
82
|
+
throw new Error('A nonempty, single-line conversation sessionId is required.')
|
|
83
|
+
this.baseURL = (config.baseURL ?? (service === 'go' ? ZEN_GO_BASE_URL : ZEN_BASE_URL)).replace(
|
|
84
|
+
/\/+$/,
|
|
85
|
+
'',
|
|
86
|
+
)
|
|
87
|
+
const url = new URL(this.baseURL)
|
|
88
|
+
if (
|
|
89
|
+
!['http:', 'https:'].includes(url.protocol) ||
|
|
90
|
+
url.username ||
|
|
91
|
+
url.password ||
|
|
92
|
+
url.search ||
|
|
93
|
+
url.hash
|
|
94
|
+
)
|
|
95
|
+
throw new Error(
|
|
96
|
+
'baseURL must be an HTTP(S) service URL without credentials, query or fragment.',
|
|
97
|
+
)
|
|
98
|
+
this.timeout = config.timeout ?? 120_000
|
|
99
|
+
if (!Number.isSafeInteger(this.timeout) || this.timeout <= 0)
|
|
100
|
+
throw new Error('timeout must be a positive integer in milliseconds.')
|
|
101
|
+
this.requestFetch = (input, init) => {
|
|
102
|
+
const headers = new Headers(init?.headers)
|
|
103
|
+
for (const [key, value] of Object.entries(attributionHeaders())) headers.set(key, value)
|
|
104
|
+
headers.set('x-opencode-session', this.sessionId)
|
|
105
|
+
return fetch(input, { ...init, headers, redirect: 'error' })
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async *chatStream(params: ChatCompletionParams): AsyncIterable<StreamChunk> {
|
|
110
|
+
const model =
|
|
111
|
+
params.model ||
|
|
112
|
+
this.config.model ||
|
|
113
|
+
(this.anonymous ? 'muse-spark-1.3-contributor-free' : 'glm-5.3-flash')
|
|
114
|
+
const info = findZenModel(this.service, model)
|
|
115
|
+
if (this.anonymous && info?.supportsAnonymousAccess !== true)
|
|
116
|
+
throw new ProviderRequestError({
|
|
117
|
+
providerId: this.id,
|
|
118
|
+
kind: 'auth',
|
|
119
|
+
detail:
|
|
120
|
+
'Anonymous Zen access requires a supported free model. Configure an API key for other models.',
|
|
121
|
+
})
|
|
122
|
+
const protocol = this.config.protocol ?? info?.protocol
|
|
123
|
+
if (!protocol)
|
|
124
|
+
throw new ProviderRequestError({
|
|
125
|
+
providerId: this.id,
|
|
126
|
+
kind: 'bad_request',
|
|
127
|
+
detail:
|
|
128
|
+
'This model has no known wire format. Update the provider or configure protocol explicitly.',
|
|
129
|
+
})
|
|
130
|
+
const route = params.providerRoute ?? { providerId: this.id, model, chainIndex: 0 }
|
|
131
|
+
const controller = new AbortController()
|
|
132
|
+
const signal = AbortSignal.any([
|
|
133
|
+
controller.signal,
|
|
134
|
+
AbortSignal.timeout(this.timeout),
|
|
135
|
+
...(params.signal ? [params.signal] : []),
|
|
136
|
+
])
|
|
137
|
+
let reader: ReadableStreamDefaultReader<LanguageModelV3StreamPart> | undefined
|
|
138
|
+
try {
|
|
139
|
+
signal.throwIfAborted()
|
|
140
|
+
let options: ReturnType<typeof createCallOptions>
|
|
141
|
+
try {
|
|
142
|
+
options = createCallOptions({ ...params, model }, route, this.service, protocol)
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if (isProviderRequestError(error)) throw error
|
|
145
|
+
throw new ProviderRequestError({
|
|
146
|
+
providerId: this.id,
|
|
147
|
+
kind: 'bad_request',
|
|
148
|
+
detail: 'The message history contains content this model protocol cannot encode.',
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
options.abortSignal = signal
|
|
152
|
+
const settings = {
|
|
153
|
+
apiKey: this.apiKey,
|
|
154
|
+
baseURL: this.baseURL,
|
|
155
|
+
fetch: ((input, init) => {
|
|
156
|
+
// The pinned Google adapter converts JSON Schema into an OpenAPI subset,
|
|
157
|
+
// dropping constraints. Use the endpoint's JSON Schema field instead.
|
|
158
|
+
if (
|
|
159
|
+
protocol === 'google' &&
|
|
160
|
+
params.responseFormat?.type === 'json_schema' &&
|
|
161
|
+
typeof init?.body === 'string'
|
|
162
|
+
) {
|
|
163
|
+
const body = JSON.parse(init.body)
|
|
164
|
+
body.generationConfig ??= {}
|
|
165
|
+
delete body.generationConfig.responseSchema
|
|
166
|
+
body.generationConfig.responseJsonSchema = params.responseFormat.json_schema.schema
|
|
167
|
+
return this.requestFetch(input, { ...init, body: JSON.stringify(body) })
|
|
168
|
+
}
|
|
169
|
+
return this.requestFetch(input, init)
|
|
170
|
+
}) as typeof fetch,
|
|
171
|
+
}
|
|
172
|
+
let native: LanguageModelV3
|
|
173
|
+
switch (protocol) {
|
|
174
|
+
case 'chat':
|
|
175
|
+
native = createOpenAICompatible({
|
|
176
|
+
...settings,
|
|
177
|
+
name: 'opencode',
|
|
178
|
+
includeUsage: true,
|
|
179
|
+
supportsStructuredOutputs: true,
|
|
180
|
+
})(model)
|
|
181
|
+
break
|
|
182
|
+
case 'responses':
|
|
183
|
+
native = createOpenAI(settings).responses(model)
|
|
184
|
+
break
|
|
185
|
+
case 'messages':
|
|
186
|
+
native = createAnthropic(settings)(model)
|
|
187
|
+
break
|
|
188
|
+
case 'google':
|
|
189
|
+
native = createGoogleGenerativeAI(settings)(model)
|
|
190
|
+
break
|
|
191
|
+
}
|
|
192
|
+
const result = await native.doStream(options)
|
|
193
|
+
reader = result.stream.getReader()
|
|
194
|
+
let id = randomUUID() as string
|
|
195
|
+
const content: LanguageModelV3Content[] = []
|
|
196
|
+
const texts = new Map<string, Extract<LanguageModelV3Content, { type: 'text' }>>()
|
|
197
|
+
const thoughts = new Map<
|
|
198
|
+
string,
|
|
199
|
+
{ index: number; value: Extract<LanguageModelV3Content, { type: 'reasoning' }> }
|
|
200
|
+
>()
|
|
201
|
+
const tools = new Map<
|
|
202
|
+
string,
|
|
203
|
+
{
|
|
204
|
+
index: number
|
|
205
|
+
input: string
|
|
206
|
+
ended: boolean
|
|
207
|
+
value: Extract<LanguageModelV3Content, { type: 'tool-call' }>
|
|
208
|
+
}
|
|
209
|
+
>()
|
|
210
|
+
let finish: Extract<LanguageModelV3StreamPart, { type: 'finish' }> | undefined
|
|
211
|
+
while (true) {
|
|
212
|
+
const next = await reader.read()
|
|
213
|
+
if (next.done) break
|
|
214
|
+
const part = next.value
|
|
215
|
+
signal.throwIfAborted()
|
|
216
|
+
switch (part.type) {
|
|
217
|
+
case 'stream-start': {
|
|
218
|
+
const unsupported = part.warnings.filter((warning) => warning.type === 'unsupported')
|
|
219
|
+
if (unsupported.length)
|
|
220
|
+
throw new ProviderRequestError({
|
|
221
|
+
providerId: this.id,
|
|
222
|
+
kind: 'bad_request',
|
|
223
|
+
detail:
|
|
224
|
+
'The selected model protocol cannot honor one or more requested settings or content parts.',
|
|
225
|
+
})
|
|
226
|
+
break
|
|
227
|
+
}
|
|
228
|
+
case 'response-metadata':
|
|
229
|
+
if (part.id) id = part.id
|
|
230
|
+
break
|
|
231
|
+
case 'text-start': {
|
|
232
|
+
const value = {
|
|
233
|
+
type: 'text' as const,
|
|
234
|
+
text: '',
|
|
235
|
+
providerMetadata: part.providerMetadata,
|
|
236
|
+
}
|
|
237
|
+
texts.set(part.id, value)
|
|
238
|
+
content.push(value)
|
|
239
|
+
break
|
|
240
|
+
}
|
|
241
|
+
case 'text-delta': {
|
|
242
|
+
const value = texts.get(part.id)
|
|
243
|
+
if (!value) throw new Error('Text delta has no open block.')
|
|
244
|
+
value.text += part.delta
|
|
245
|
+
value.providerMetadata = mergeMetadata(value.providerMetadata, part.providerMetadata)
|
|
246
|
+
yield { id, delta: { content: part.delta } }
|
|
247
|
+
break
|
|
248
|
+
}
|
|
249
|
+
case 'text-end': {
|
|
250
|
+
const value = texts.get(part.id)
|
|
251
|
+
if (value)
|
|
252
|
+
value.providerMetadata = mergeMetadata(value.providerMetadata, part.providerMetadata)
|
|
253
|
+
break
|
|
254
|
+
}
|
|
255
|
+
case 'reasoning-start': {
|
|
256
|
+
const value = {
|
|
257
|
+
type: 'reasoning' as const,
|
|
258
|
+
text: '',
|
|
259
|
+
providerMetadata: part.providerMetadata,
|
|
260
|
+
}
|
|
261
|
+
const index = thoughts.size
|
|
262
|
+
thoughts.set(part.id, { index, value })
|
|
263
|
+
content.push(value)
|
|
264
|
+
yield { id, delta: { reasoning: { index, type: 'thinking', text: '' } } }
|
|
265
|
+
break
|
|
266
|
+
}
|
|
267
|
+
case 'reasoning-delta':
|
|
268
|
+
case 'reasoning-end': {
|
|
269
|
+
const thought = thoughts.get(part.id)
|
|
270
|
+
if (!thought) throw new Error('Reasoning delta has no open block.')
|
|
271
|
+
thought.value.providerMetadata = mergeMetadata(
|
|
272
|
+
thought.value.providerMetadata,
|
|
273
|
+
part.providerMetadata,
|
|
274
|
+
)
|
|
275
|
+
if (part.type === 'reasoning-delta') thought.value.text += part.delta
|
|
276
|
+
yield {
|
|
277
|
+
id,
|
|
278
|
+
delta: {
|
|
279
|
+
reasoning: {
|
|
280
|
+
index: thought.index,
|
|
281
|
+
...(part.type === 'reasoning-delta'
|
|
282
|
+
? { text: part.delta }
|
|
283
|
+
: {
|
|
284
|
+
...toReasoningBlocks([thought.value])[0],
|
|
285
|
+
text: undefined,
|
|
286
|
+
done: true,
|
|
287
|
+
}),
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
}
|
|
291
|
+
break
|
|
292
|
+
}
|
|
293
|
+
case 'tool-input-start': {
|
|
294
|
+
if (part.providerExecuted)
|
|
295
|
+
throw new Error('Provider-executed tools are not configured.')
|
|
296
|
+
const index = tools.size
|
|
297
|
+
const value = {
|
|
298
|
+
type: 'tool-call' as const,
|
|
299
|
+
toolCallId: part.id,
|
|
300
|
+
toolName: part.toolName,
|
|
301
|
+
input: '',
|
|
302
|
+
providerMetadata: part.providerMetadata,
|
|
303
|
+
}
|
|
304
|
+
tools.set(part.id, { index, input: '', ended: false, value })
|
|
305
|
+
content.push(value)
|
|
306
|
+
yield {
|
|
307
|
+
id,
|
|
308
|
+
delta: {
|
|
309
|
+
toolCalls: [
|
|
310
|
+
{
|
|
311
|
+
index,
|
|
312
|
+
id: part.id,
|
|
313
|
+
type: 'function',
|
|
314
|
+
function: { name: part.toolName, arguments: '' },
|
|
315
|
+
},
|
|
316
|
+
],
|
|
317
|
+
},
|
|
318
|
+
}
|
|
319
|
+
break
|
|
320
|
+
}
|
|
321
|
+
case 'tool-input-delta': {
|
|
322
|
+
const tool = tools.get(part.id)
|
|
323
|
+
if (!tool) throw new Error('Tool delta has no open call.')
|
|
324
|
+
tool.input += part.delta
|
|
325
|
+
tool.value.input = tool.input
|
|
326
|
+
tool.value.providerMetadata = mergeMetadata(
|
|
327
|
+
tool.value.providerMetadata,
|
|
328
|
+
part.providerMetadata,
|
|
329
|
+
)
|
|
330
|
+
yield {
|
|
331
|
+
id,
|
|
332
|
+
delta: { toolCalls: [{ index: tool.index, function: { arguments: part.delta } }] },
|
|
333
|
+
}
|
|
334
|
+
break
|
|
335
|
+
}
|
|
336
|
+
case 'tool-input-end': {
|
|
337
|
+
const tool = tools.get(part.id)
|
|
338
|
+
if (tool)
|
|
339
|
+
tool.value.providerMetadata = mergeMetadata(
|
|
340
|
+
tool.value.providerMetadata,
|
|
341
|
+
part.providerMetadata,
|
|
342
|
+
)
|
|
343
|
+
// The final tool-call includes signatures and the authoritative arguments.
|
|
344
|
+
break
|
|
345
|
+
}
|
|
346
|
+
case 'tool-call': {
|
|
347
|
+
if (part.providerExecuted)
|
|
348
|
+
throw new Error('Provider-executed tools are not configured.')
|
|
349
|
+
let tool = tools.get(part.toolCallId)
|
|
350
|
+
if (!tool) {
|
|
351
|
+
tool = { index: tools.size, input: '', ended: false, value: { ...part } }
|
|
352
|
+
tools.set(part.toolCallId, tool)
|
|
353
|
+
content.push(tool.value)
|
|
354
|
+
yield {
|
|
355
|
+
id,
|
|
356
|
+
delta: {
|
|
357
|
+
toolCalls: [
|
|
358
|
+
{
|
|
359
|
+
index: tool.index,
|
|
360
|
+
id: part.toolCallId,
|
|
361
|
+
type: 'function',
|
|
362
|
+
function: { name: part.toolName, arguments: '' },
|
|
363
|
+
},
|
|
364
|
+
],
|
|
365
|
+
},
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (tool.ended) throw new Error('Duplicate completed tool call.')
|
|
369
|
+
if (!part.input.startsWith(tool.input))
|
|
370
|
+
throw new Error('Final tool arguments do not match the stream.')
|
|
371
|
+
const suffix = part.input.slice(tool.input.length)
|
|
372
|
+
if (suffix)
|
|
373
|
+
yield {
|
|
374
|
+
id,
|
|
375
|
+
delta: { toolCalls: [{ index: tool.index, function: { arguments: suffix } }] },
|
|
376
|
+
}
|
|
377
|
+
tool.value.input = part.input
|
|
378
|
+
tool.value.providerMetadata = mergeMetadata(
|
|
379
|
+
tool.value.providerMetadata,
|
|
380
|
+
part.providerMetadata,
|
|
381
|
+
)
|
|
382
|
+
tool.ended = true
|
|
383
|
+
yield { id, delta: { toolCallEnd: { index: tool.index, id: part.toolCallId } } }
|
|
384
|
+
break
|
|
385
|
+
}
|
|
386
|
+
case 'finish':
|
|
387
|
+
finish = part
|
|
388
|
+
break
|
|
389
|
+
case 'error':
|
|
390
|
+
throw part.error
|
|
391
|
+
case 'raw':
|
|
392
|
+
case 'source':
|
|
393
|
+
break
|
|
394
|
+
default:
|
|
395
|
+
throw new Error('Unsupported model output part.')
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (!finish) throw new Error('The model stream ended before its final outcome.')
|
|
399
|
+
if (finish.finishReason.unified === 'error' || finish.finishReason.unified === 'other')
|
|
400
|
+
throw new Error('The model did not report a successful finish reason.')
|
|
401
|
+
const finishReason =
|
|
402
|
+
finish.finishReason.unified === 'length'
|
|
403
|
+
? 'length'
|
|
404
|
+
: finish.finishReason.unified === 'content-filter'
|
|
405
|
+
? 'content_filter'
|
|
406
|
+
: tools.size > 0
|
|
407
|
+
? 'tool_calls'
|
|
408
|
+
: 'stop'
|
|
409
|
+
yield {
|
|
410
|
+
id,
|
|
411
|
+
delta: {},
|
|
412
|
+
finishReason,
|
|
413
|
+
usage: mapUsage(finish.usage),
|
|
414
|
+
replayState: createReplayState(
|
|
415
|
+
{ ...params, model },
|
|
416
|
+
route,
|
|
417
|
+
this.service,
|
|
418
|
+
protocol,
|
|
419
|
+
content,
|
|
420
|
+
),
|
|
421
|
+
}
|
|
422
|
+
} catch (error) {
|
|
423
|
+
if (params.signal?.aborted) throw params.signal.reason
|
|
424
|
+
if (isProviderRequestError(error)) throw error
|
|
425
|
+
throw this.failure(error)
|
|
426
|
+
} finally {
|
|
427
|
+
controller.abort()
|
|
428
|
+
if (reader) {
|
|
429
|
+
await reader.cancel().catch(() => {})
|
|
430
|
+
reader.releaseLock()
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async listModels(signal?: AbortSignal): Promise<ModelInfo[]> {
|
|
436
|
+
try {
|
|
437
|
+
const response = await this.requestFetch(`${this.baseURL}/models`, {
|
|
438
|
+
headers: { Authorization: `Bearer ${this.apiKey}` },
|
|
439
|
+
signal: AbortSignal.any([AbortSignal.timeout(this.timeout), ...(signal ? [signal] : [])]),
|
|
440
|
+
})
|
|
441
|
+
if (!response.ok) {
|
|
442
|
+
await response.body?.cancel()
|
|
443
|
+
throw providerHttpError({
|
|
444
|
+
providerId: this.id,
|
|
445
|
+
status: response.status,
|
|
446
|
+
body: undefined,
|
|
447
|
+
retryAfter: response.headers.get('retry-after'),
|
|
448
|
+
})
|
|
449
|
+
}
|
|
450
|
+
const body: unknown = await readCatalogue(response)
|
|
451
|
+
if (!body || typeof body !== 'object' || !('data' in body) || !Array.isArray(body.data))
|
|
452
|
+
throw new Error('The model catalogue returned an invalid response.')
|
|
453
|
+
const result: ModelInfo[] = []
|
|
454
|
+
const seen = new Set<string>()
|
|
455
|
+
for (const item of body.data) {
|
|
456
|
+
if (!item || typeof item !== 'object' || typeof item.id !== 'string' || seen.has(item.id))
|
|
457
|
+
continue
|
|
458
|
+
seen.add(item.id)
|
|
459
|
+
const model = findZenModel(this.service, item.id)
|
|
460
|
+
if (model && (!this.anonymous || model.supportsAnonymousAccess === true))
|
|
461
|
+
result.push({ ...model })
|
|
462
|
+
}
|
|
463
|
+
return result
|
|
464
|
+
} catch (error) {
|
|
465
|
+
if (signal?.aborted) throw signal.reason
|
|
466
|
+
if (isProviderRequestError(error)) throw error
|
|
467
|
+
throw this.failure(error)
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
reasoningEffortLevelsFor(
|
|
472
|
+
model: string,
|
|
473
|
+
_thinking?: ThinkingConfig,
|
|
474
|
+
): readonly ReasoningEffort[] | undefined {
|
|
475
|
+
return findZenModel(this.service, model)?.effortLevels
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async resolveContextWindow(model: string, signal?: AbortSignal): Promise<number | undefined> {
|
|
479
|
+
signal?.throwIfAborted()
|
|
480
|
+
return findZenModel(this.service, model)?.contextWindow
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
private failure(error: unknown): ProviderRequestError {
|
|
484
|
+
const scrub = (text: string) =>
|
|
485
|
+
this.anonymous ? text : text.split(this.apiKey).join('[REDACTED:api-key]')
|
|
486
|
+
if (APICallError.isInstance(error))
|
|
487
|
+
return providerHttpError({
|
|
488
|
+
providerId: this.id,
|
|
489
|
+
status: error.statusCode ?? 502,
|
|
490
|
+
body: scrub(error.responseBody ?? error.message),
|
|
491
|
+
retryAfter: error.responseHeaders?.['retry-after'],
|
|
492
|
+
})
|
|
493
|
+
// Native streaming adapters can emit plain error envelopes after HTTP 200.
|
|
494
|
+
// Keep only classification fields; never attach the original error/cause.
|
|
495
|
+
const envelope = safeErrorEnvelope(error)
|
|
496
|
+
const body = scrub(JSON.stringify(envelope))
|
|
497
|
+
if (envelope.status !== undefined)
|
|
498
|
+
return providerHttpError({ providerId: this.id, status: envelope.status, body })
|
|
499
|
+
return providerVendorError({
|
|
500
|
+
providerId: this.id,
|
|
501
|
+
error: Object.assign(new Error(body), {
|
|
502
|
+
...(envelope.code !== undefined ? { code: scrub(envelope.code) } : {}),
|
|
503
|
+
}),
|
|
504
|
+
})
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export class ZenGoProvider extends ZenProvider {
|
|
509
|
+
constructor(config: ZenGoConfig) {
|
|
510
|
+
super(config, 'go')
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function safeErrorEnvelope(error: unknown): {
|
|
515
|
+
type?: string
|
|
516
|
+
code?: string
|
|
517
|
+
message: string
|
|
518
|
+
status?: number
|
|
519
|
+
} {
|
|
520
|
+
const own = (value: unknown, key: string): unknown =>
|
|
521
|
+
typeof value === 'object' && value !== null
|
|
522
|
+
? Object.getOwnPropertyDescriptor(value, key)?.value
|
|
523
|
+
: undefined
|
|
524
|
+
const nested = own(error, 'error')
|
|
525
|
+
const source = typeof nested === 'object' && nested !== null ? nested : error
|
|
526
|
+
const type = own(source, 'type')
|
|
527
|
+
const code = own(source, 'code')
|
|
528
|
+
const message = typeof source === 'string' ? source : own(source, 'message')
|
|
529
|
+
const status = own(source, 'status') ?? own(source, 'statusCode') ?? own(error, 'status')
|
|
530
|
+
return {
|
|
531
|
+
...(typeof type === 'string' && type.length <= 80 ? { type } : {}),
|
|
532
|
+
...(typeof code === 'string' && code.length <= 80 ? { code } : {}),
|
|
533
|
+
message: typeof message === 'string' ? message.slice(0, 8192) : 'The model stream failed.',
|
|
534
|
+
...(typeof status === 'number' && Number.isInteger(status) && status >= 400 && status <= 599
|
|
535
|
+
? { status }
|
|
536
|
+
: {}),
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function mergeMetadata(
|
|
541
|
+
left?: SharedV3ProviderMetadata,
|
|
542
|
+
right?: SharedV3ProviderMetadata,
|
|
543
|
+
): SharedV3ProviderMetadata | undefined {
|
|
544
|
+
if (!right) return left
|
|
545
|
+
const result = { ...left }
|
|
546
|
+
for (const [key, value] of Object.entries(right)) result[key] = { ...result[key], ...value }
|
|
547
|
+
return result
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function mapUsage(usage: LanguageModelV3Usage): TokenUsage {
|
|
551
|
+
const promptTokens = usage.inputTokens.total ?? 0
|
|
552
|
+
const completionTokens = usage.outputTokens.total ?? 0
|
|
553
|
+
return {
|
|
554
|
+
promptTokens,
|
|
555
|
+
completionTokens,
|
|
556
|
+
totalTokens: promptTokens + completionTokens,
|
|
557
|
+
cachedTokens: usage.inputTokens.cacheRead ?? 0,
|
|
558
|
+
cacheWriteTokens: usage.inputTokens.cacheWrite ?? 0,
|
|
559
|
+
...(usage.outputTokens.reasoning !== undefined
|
|
560
|
+
? { reasoningTokens: usage.outputTokens.reasoning }
|
|
561
|
+
: {}),
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
async function readCatalogue(response: Response): Promise<unknown> {
|
|
566
|
+
const reader = response.body?.getReader()
|
|
567
|
+
if (!reader) throw new Error('Empty model catalogue.')
|
|
568
|
+
const chunks: Uint8Array[] = []
|
|
569
|
+
let bytes = 0
|
|
570
|
+
try {
|
|
571
|
+
while (true) {
|
|
572
|
+
const next = await reader.read()
|
|
573
|
+
if (next.done) break
|
|
574
|
+
bytes += next.value.byteLength
|
|
575
|
+
if (bytes > 1_048_576) throw new Error('The model catalogue exceeds 1 MiB.')
|
|
576
|
+
chunks.push(next.value)
|
|
577
|
+
}
|
|
578
|
+
return JSON.parse(
|
|
579
|
+
new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks, bytes)),
|
|
580
|
+
)
|
|
581
|
+
} finally {
|
|
582
|
+
await reader.cancel().catch(() => {})
|
|
583
|
+
reader.releaseLock()
|
|
584
|
+
}
|
|
585
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ProviderRegistry, type RegisterOptions } from '@namzu/sdk'
|
|
2
|
+
import { ZEN_CAPABILITIES, ZenGoProvider, ZenProvider } from './client.js'
|
|
3
|
+
import type { ZenGoProviderConfig, ZenProviderConfig } from './types.js'
|
|
4
|
+
|
|
5
|
+
declare module '@namzu/sdk' {
|
|
6
|
+
interface ProviderConfigRegistry {
|
|
7
|
+
zen: ZenProviderConfig
|
|
8
|
+
'zen-go': ZenGoProviderConfig
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function registerZen(options?: RegisterOptions): void {
|
|
13
|
+
ProviderRegistry.register('zen', ZenProvider, ZEN_CAPABILITIES, options)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function registerZenGo(options?: RegisterOptions): void {
|
|
17
|
+
ProviderRegistry.register('zen-go', ZenGoProvider, ZEN_CAPABILITIES, options)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export {
|
|
21
|
+
ZEN_BASE_URL,
|
|
22
|
+
ZEN_GO_BASE_URL,
|
|
23
|
+
ZEN_CAPABILITIES,
|
|
24
|
+
ZenProvider,
|
|
25
|
+
ZenGoProvider,
|
|
26
|
+
} from './client.js'
|
|
27
|
+
export type { ZenConfig, ZenGoConfig, ZenProviderConfig, ZenGoProviderConfig } from './types.js'
|
|
28
|
+
export { getZenModels, findZenModel } from './models.js'
|
|
29
|
+
export type { ZenModel, ZenProtocol, ZenService } from './models.js'
|
|
30
|
+
export type {
|
|
31
|
+
ChatCompletionParams,
|
|
32
|
+
ModelInfo,
|
|
33
|
+
ReasoningEffort,
|
|
34
|
+
StreamChunk,
|
|
35
|
+
ThinkingConfig,
|
|
36
|
+
} from '@namzu/sdk'
|