@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/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'