@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/options.ts ADDED
@@ -0,0 +1,259 @@
1
+ import { getModelCapabilities as getMessagesCapabilities } from '@ai-sdk/anthropic/internal'
2
+ import type { LanguageModelV3CallOptions, SharedV3ProviderOptions } from '@ai-sdk/provider'
3
+ import { type ChatCompletionParams, ProviderRequestError, type ProviderRoute } from '@namzu/sdk'
4
+ import { type ZenProtocol, type ZenService, findZenModel } from './models.js'
5
+ import { toModelPrompt } from './prompt.js'
6
+
7
+ export function createCallOptions(
8
+ params: ChatCompletionParams,
9
+ route: ProviderRoute,
10
+ service: ZenService,
11
+ protocol: ZenProtocol,
12
+ ): LanguageModelV3CallOptions {
13
+ const refuse = (detail: string): never => {
14
+ throw new ProviderRequestError({ providerId: route.providerId, kind: 'bad_request', detail })
15
+ }
16
+ const known = findZenModel(service, params.model)
17
+ let maxOutputTokens = params.maxTokens ?? 4096
18
+ if (!Number.isSafeInteger(maxOutputTokens) || maxOutputTokens <= 0)
19
+ refuse('maxTokens must be a positive integer.')
20
+ if (
21
+ params.effort !== undefined &&
22
+ known?.effortLevels &&
23
+ !known.effortLevels.includes(params.effort)
24
+ )
25
+ refuse('The selected model does not advertise this reasoning effort level.')
26
+ if (params.repetitionPenalty !== undefined)
27
+ refuse('repetitionPenalty is not supported by this driver.')
28
+ if (params.topK !== undefined && protocol !== 'google' && protocol !== 'messages')
29
+ refuse('topK is not supported by this model protocol.')
30
+ if (
31
+ protocol === 'messages' &&
32
+ (params.frequencyPenalty !== undefined || params.presencePenalty !== undefined)
33
+ )
34
+ refuse('Frequency and presence penalties are not supported by the messages protocol.')
35
+ if (params.cacheControl?.type === 'ephemeral' && protocol !== 'messages')
36
+ refuse('Explicit ephemeral cache control requires the messages protocol.')
37
+ const options: SharedV3ProviderOptions = {}
38
+ switch (protocol) {
39
+ case 'chat': {
40
+ if (params.thinking?.budgetTokens !== undefined || params.thinking?.display !== undefined)
41
+ refuse('This chat protocol cannot honor a thinking budget or display setting.')
42
+ options.opencode = {
43
+ ...(params.effort !== undefined ? { reasoningEffort: params.effort } : {}),
44
+ ...(params.parallelToolCalls !== undefined
45
+ ? { parallel_tool_calls: params.parallelToolCalls }
46
+ : {}),
47
+ ...(params.thinking ? { thinking: { type: params.thinking.type } } : {}),
48
+ ...(params.responseFormat?.type === 'json_schema'
49
+ ? { strictJsonSchema: params.responseFormat.json_schema.strict ?? true }
50
+ : {}),
51
+ }
52
+ break
53
+ }
54
+ case 'responses': {
55
+ if (params.stop !== undefined)
56
+ refuse('Stop sequences are not supported by the responses protocol.')
57
+ if (params.frequencyPenalty !== undefined || params.presencePenalty !== undefined)
58
+ refuse('Frequency and presence penalties are not supported by the responses protocol.')
59
+ if (params.thinking?.type === 'enabled' || params.thinking?.budgetTokens !== undefined)
60
+ refuse('The responses protocol uses adaptive effort, not a fixed thinking budget.')
61
+ if (
62
+ params.thinking?.type === 'disabled' &&
63
+ params.effort !== undefined &&
64
+ params.effort !== 'none'
65
+ )
66
+ refuse('Disabled thinking conflicts with a non-none reasoning effort.')
67
+ if (
68
+ params.thinking?.type === 'disabled' &&
69
+ known?.effortLevels &&
70
+ !known.effortLevels.includes('none')
71
+ )
72
+ refuse('This model does not advertise disabled reasoning.')
73
+ // The pinned native adapter removes sampling parameters from reasoning
74
+ // requests. Its warning arrives after the POST, so reject that loss here.
75
+ const gpt = /^gpt-(\d+)(?:\.(\d+))?(?:-(.+))?$/.exec(params.model)
76
+ const isReasoning =
77
+ Boolean(known?.effortLevels?.length) ||
78
+ /^o\d+(?:-|$)/.test(params.model) ||
79
+ (gpt !== null &&
80
+ Number(gpt[1]) >= 5 &&
81
+ !(gpt[2] === undefined && gpt[3]?.startsWith('chat')))
82
+ const effort = params.thinking?.type === 'disabled' ? 'none' : params.effort
83
+ const allowsSampling =
84
+ effort === 'none' && gpt !== null && Number(gpt[1]) === 5 && Number(gpt[2]) >= 1
85
+ if (
86
+ isReasoning &&
87
+ !allowsSampling &&
88
+ (params.temperature !== undefined || params.topP !== undefined)
89
+ )
90
+ refuse('Sampling parameters are not supported with this model’s reasoning configuration.')
91
+ if (
92
+ !isReasoning &&
93
+ (params.effort !== undefined || params.thinking?.display === 'summarized')
94
+ )
95
+ refuse('This responses model does not support reasoning controls.')
96
+ options.openai = {
97
+ store: false,
98
+ include: ['reasoning.encrypted_content'],
99
+ ...(known?.effortLevels?.length ? { forceReasoning: true } : {}),
100
+ ...(params.effort !== undefined ? { reasoningEffort: params.effort } : {}),
101
+ ...(params.thinking?.type === 'disabled' ? { reasoningEffort: 'none' } : {}),
102
+ ...(params.thinking?.display === 'summarized' ? { reasoningSummary: 'auto' } : {}),
103
+ ...(params.parallelToolCalls !== undefined
104
+ ? { parallelToolCalls: params.parallelToolCalls }
105
+ : {}),
106
+ ...(params.responseFormat?.type === 'json_schema'
107
+ ? { strictJsonSchema: params.responseFormat.json_schema.strict ?? true }
108
+ : {}),
109
+ }
110
+ break
111
+ }
112
+ case 'messages': {
113
+ const thinking = params.thinking
114
+ const capabilities = getMessagesCapabilities(params.model)
115
+ const isThinking = thinking?.type === 'enabled' || thinking?.type === 'adaptive'
116
+ if (
117
+ (isThinking || capabilities.rejectsSamplingParameters) &&
118
+ (params.temperature !== undefined || params.topP !== undefined || params.topK !== undefined)
119
+ )
120
+ refuse('Sampling parameters are not supported with this model’s thinking configuration.')
121
+ if (
122
+ params.temperature !== undefined &&
123
+ (!Number.isFinite(params.temperature) || params.temperature < 0 || params.temperature > 1)
124
+ )
125
+ refuse('The messages protocol requires temperature between 0 and 1.')
126
+ if (
127
+ params.model.includes('claude-') &&
128
+ params.temperature !== undefined &&
129
+ params.topP !== undefined
130
+ )
131
+ refuse('This model cannot honor temperature and topP together.')
132
+ if (
133
+ params.responseFormat?.type === 'json_schema' &&
134
+ params.responseFormat.json_schema.strict === false
135
+ )
136
+ refuse('The messages protocol cannot disable schema enforcement.')
137
+ if (params.responseFormat?.type === 'json_object')
138
+ refuse('The messages protocol requires a schema for JSON output.')
139
+ if (
140
+ capabilities.rejectsThinkingDisabledAboveHighEffort &&
141
+ thinking?.type === 'disabled' &&
142
+ (params.effort === 'xhigh' || params.effort === 'max')
143
+ )
144
+ refuse('This model cannot disable thinking at the requested effort level.')
145
+ if (thinking?.budgetTokens !== undefined && thinking.type !== 'enabled')
146
+ refuse('A thinking budget requires manual enabled thinking.')
147
+ if (thinking?.display !== undefined && thinking.type !== 'adaptive')
148
+ refuse('Thinking display selection requires adaptive thinking.')
149
+ if (capabilities.isKnownModel && maxOutputTokens > capabilities.maxOutputTokens)
150
+ refuse('maxTokens exceeds the selected model’s output limit.')
151
+ if (thinking?.type === 'enabled') {
152
+ // Namzu's cap includes thinking; the native adapter adds its budget
153
+ // to maxOutputTokens, including a default 1024 when omitted.
154
+ const budget = thinking.budgetTokens ?? 1024
155
+ if (!Number.isSafeInteger(budget) || budget < 1024 || budget >= maxOutputTokens)
156
+ refuse('Manual thinking requires a budget of at least 1024 below maxTokens.')
157
+ maxOutputTokens -= budget
158
+ }
159
+ if (
160
+ !capabilities.supportsStructuredOutput &&
161
+ params.responseFormat?.type === 'json_schema' &&
162
+ params.parallelToolCalls === true
163
+ )
164
+ refuse('This model cannot combine schema output with parallel tool calls.')
165
+ if (
166
+ !capabilities.supportsStructuredOutput &&
167
+ params.tools?.some((tool) => params.enforceToolInputSchema?.includes(tool.function.name))
168
+ )
169
+ refuse('This model protocol cannot enforce strict tool input schemas.')
170
+ options.anthropic = {
171
+ ...(params.responseFormat?.type === 'json_schema'
172
+ ? { structuredOutputMode: 'outputFormat' }
173
+ : {}),
174
+ sendReasoning: true,
175
+ ...(thinking ? { thinking: { ...thinking } } : {}),
176
+ ...(params.effort !== undefined ? { effort: params.effort } : {}),
177
+ ...(params.parallelToolCalls !== undefined
178
+ ? { disableParallelToolUse: !params.parallelToolCalls }
179
+ : {}),
180
+ ...(params.cacheControl ? { cacheControl: { type: 'ephemeral' } } : {}),
181
+ }
182
+ break
183
+ }
184
+ case 'google': {
185
+ if (params.parallelToolCalls !== undefined)
186
+ refuse('The google protocol has no parallelToolCalls switch.')
187
+ if (params.thinking?.budgetTokens !== undefined && params.thinking.type !== 'enabled')
188
+ refuse('A thinking budget requires manual enabled thinking.')
189
+ if (
190
+ params.thinking?.type === 'disabled' &&
191
+ params.effort !== undefined &&
192
+ params.effort !== 'none'
193
+ )
194
+ refuse('Disabled thinking conflicts with a reasoning effort level.')
195
+ options.google = {
196
+ thinkingConfig: {
197
+ includeThoughts: params.thinking?.display !== 'omitted',
198
+ ...(params.effort !== undefined ? { thinkingLevel: params.effort } : {}),
199
+ ...(params.thinking?.type === 'disabled' ? { thinkingBudget: 0 } : {}),
200
+ ...(params.thinking?.budgetTokens !== undefined
201
+ ? { thinkingBudget: params.thinking.budgetTokens }
202
+ : {}),
203
+ },
204
+ }
205
+ break
206
+ }
207
+ }
208
+ const enforced = new Set(params.enforceToolInputSchema)
209
+ return {
210
+ prompt: toModelPrompt(params, route, service, protocol),
211
+ maxOutputTokens,
212
+ ...(params.temperature !== undefined ? { temperature: params.temperature } : {}),
213
+ ...(params.topP !== undefined ? { topP: params.topP } : {}),
214
+ ...(params.topK !== undefined ? { topK: params.topK } : {}),
215
+ ...(params.frequencyPenalty !== undefined ? { frequencyPenalty: params.frequencyPenalty } : {}),
216
+ ...(params.presencePenalty !== undefined ? { presencePenalty: params.presencePenalty } : {}),
217
+ ...(params.stop ? { stopSequences: params.stop } : {}),
218
+ ...(params.tools
219
+ ? {
220
+ tools: params.tools.map((tool) => ({
221
+ type: 'function' as const,
222
+ name: tool.function.name,
223
+ description: tool.function.description,
224
+ inputSchema: tool.function.parameters,
225
+ // Responses strict schemas cannot express Namzu's general
226
+ // tool contract (including conditional edit shapes). Like
227
+ // the Codex driver, retain the schema and validate inputs
228
+ // at execution; enforcement is a capability-dependent hint.
229
+ ...(protocol === 'responses'
230
+ ? { strict: false }
231
+ : enforced.has(tool.function.name)
232
+ ? { strict: true }
233
+ : {}),
234
+ })),
235
+ }
236
+ : {}),
237
+ ...(params.toolChoice
238
+ ? {
239
+ toolChoice:
240
+ typeof params.toolChoice === 'string'
241
+ ? { type: params.toolChoice }
242
+ : { type: 'tool', toolName: params.toolChoice.function.name },
243
+ }
244
+ : {}),
245
+ ...(params.responseFormat
246
+ ? {
247
+ responseFormat:
248
+ params.responseFormat.type === 'json_object'
249
+ ? { type: 'json' as const }
250
+ : {
251
+ type: 'json' as const,
252
+ schema: params.responseFormat.json_schema.schema,
253
+ name: params.responseFormat.json_schema.name,
254
+ },
255
+ }
256
+ : {}),
257
+ providerOptions: options,
258
+ }
259
+ }