@mofeng2223/dsh-claude-provider 0.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/LICENSE +21 -0
- package/README.md +72 -0
- package/cordis.patch.yml +18 -0
- package/lib/client.js +2911 -0
- package/package.json +54 -0
- package/src/index.js +453 -0
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mofeng2223/dsh-claude-provider",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Custom Claude provider support for DeepSeek Harness",
|
|
5
|
+
"author": "mofeng2223",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"deepseek-harness",
|
|
9
|
+
"dsh-plugin",
|
|
10
|
+
"claude",
|
|
11
|
+
"anthropic"
|
|
12
|
+
],
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public",
|
|
15
|
+
"registry": "https://registry.npmjs.org/"
|
|
16
|
+
},
|
|
17
|
+
"type": "module",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": "./src/index.js",
|
|
20
|
+
"./client": "./lib/client.js",
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"src",
|
|
25
|
+
"lib/client.js",
|
|
26
|
+
"cordis.patch.yml",
|
|
27
|
+
"README.md",
|
|
28
|
+
"LICENSE"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "node scripts/build-client.mjs",
|
|
32
|
+
"test": "node --test"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=22.19"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@deepseek-ai/dsh-client-ui-settings-models": "0.1.0-rc.6"
|
|
39
|
+
},
|
|
40
|
+
"dsh": {
|
|
41
|
+
"bundle": {
|
|
42
|
+
"patch": "./cordis.patch.yml"
|
|
43
|
+
},
|
|
44
|
+
"client": {
|
|
45
|
+
"platform": "web",
|
|
46
|
+
"inject": [
|
|
47
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
48
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
49
|
+
"@deepseek-ai/dsh-client-locale",
|
|
50
|
+
"@deepseek-ai/dsh-api-remotes"
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
2
|
+
|
|
3
|
+
export const name = 'claude-provider'
|
|
4
|
+
export const inject = ['llm']
|
|
5
|
+
|
|
6
|
+
const DEFAULT_EFFORT_MAP = Object.freeze({
|
|
7
|
+
minimal: 'low',
|
|
8
|
+
low: 'low',
|
|
9
|
+
medium: 'medium',
|
|
10
|
+
high: 'high',
|
|
11
|
+
xhigh: 'xhigh',
|
|
12
|
+
max: 'max',
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const INTERLEAVED_THINKING_BETA = 'interleaved-thinking-2025-05-14'
|
|
16
|
+
export const CLAUDE_DISCOVERY_API = 'mofeng-anthropic-models'
|
|
17
|
+
|
|
18
|
+
const PI_AI_SETTINGS_NS = 'llm-pi-ai'
|
|
19
|
+
const MAX_DISCOVERY_RESPONSE_BYTES = 4 * 1024 * 1024
|
|
20
|
+
const MAX_DISCOVERY_PAGES = 100
|
|
21
|
+
|
|
22
|
+
function requireNonEmptyString(value, field) {
|
|
23
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
24
|
+
throw new Error(`claude-provider: ${field} must be a non-empty string`)
|
|
25
|
+
}
|
|
26
|
+
return value
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function positiveInteger(...values) {
|
|
30
|
+
for (const value of values) {
|
|
31
|
+
if (typeof value === 'number' && Number.isSafeInteger(value) && value > 0) return value
|
|
32
|
+
}
|
|
33
|
+
return undefined
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function nonEmptyString(...values) {
|
|
37
|
+
for (const value of values) {
|
|
38
|
+
if (typeof value === 'string' && value.length > 0) return value
|
|
39
|
+
}
|
|
40
|
+
return undefined
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Build the Anthropic Models API endpoint from the same base used by Messages. */
|
|
44
|
+
export function anthropicModelsUrl(baseURL) {
|
|
45
|
+
const raw = requireNonEmptyString(baseURL, 'baseURL').trim()
|
|
46
|
+
let url
|
|
47
|
+
try {
|
|
48
|
+
url = new URL(raw)
|
|
49
|
+
} catch (error) {
|
|
50
|
+
throw new Error('Claude 模型目录:API 地址不是有效 URL', { cause: error })
|
|
51
|
+
}
|
|
52
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
53
|
+
throw new Error('Claude 模型目录:API 地址只能使用 http 或 https')
|
|
54
|
+
}
|
|
55
|
+
const path = url.pathname.replace(/\/+$/, '')
|
|
56
|
+
url.pathname = /\/v1$/i.test(path) ? `${path}/models` : `${path}/v1/models`
|
|
57
|
+
url.search = ''
|
|
58
|
+
url.hash = ''
|
|
59
|
+
return url
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function readBoundedResponse(response, url) {
|
|
63
|
+
const declared = Number(response.headers.get('content-length') ?? Number.NaN)
|
|
64
|
+
if (Number.isFinite(declared) && declared > MAX_DISCOVERY_RESPONSE_BYTES) {
|
|
65
|
+
await response.body?.cancel()
|
|
66
|
+
throw new Error(`Claude 模型目录:${url} 返回内容超过 4 MiB`)
|
|
67
|
+
}
|
|
68
|
+
if (response.body === null) return ''
|
|
69
|
+
const reader = response.body.getReader()
|
|
70
|
+
const chunks = []
|
|
71
|
+
let total = 0
|
|
72
|
+
try {
|
|
73
|
+
for (;;) {
|
|
74
|
+
const { done, value } = await reader.read()
|
|
75
|
+
if (done) break
|
|
76
|
+
total += value.byteLength
|
|
77
|
+
if (total > MAX_DISCOVERY_RESPONSE_BYTES) {
|
|
78
|
+
throw new Error(`Claude 模型目录:${url} 返回内容超过 4 MiB`)
|
|
79
|
+
}
|
|
80
|
+
chunks.push(value)
|
|
81
|
+
}
|
|
82
|
+
} finally {
|
|
83
|
+
await reader.cancel().catch(() => {})
|
|
84
|
+
}
|
|
85
|
+
const body = new Uint8Array(total)
|
|
86
|
+
let offset = 0
|
|
87
|
+
for (const chunk of chunks) {
|
|
88
|
+
body.set(chunk, offset)
|
|
89
|
+
offset += chunk.byteLength
|
|
90
|
+
}
|
|
91
|
+
return new TextDecoder().decode(body)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Parse one Anthropic `/v1/models` page into DSH discovery rows. */
|
|
95
|
+
export function readAnthropicModelPage(body) {
|
|
96
|
+
const data = body?.data
|
|
97
|
+
if (!Array.isArray(data)) {
|
|
98
|
+
throw new Error('Claude 模型目录:接口返回中没有 data 数组')
|
|
99
|
+
}
|
|
100
|
+
const models = []
|
|
101
|
+
for (const raw of data) {
|
|
102
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) continue
|
|
103
|
+
const id = nonEmptyString(raw.id)
|
|
104
|
+
if (id === undefined) continue
|
|
105
|
+
const name = nonEmptyString(raw.display_name, raw.name)
|
|
106
|
+
const contextWindow = positiveInteger(raw.max_input_tokens, raw.context_window, raw.context_length)
|
|
107
|
+
const maxTokens = positiveInteger(raw.max_tokens, raw.max_output_tokens)
|
|
108
|
+
models.push({
|
|
109
|
+
id,
|
|
110
|
+
...name === undefined ? {} : { name },
|
|
111
|
+
...contextWindow === undefined ? {} : { contextWindow },
|
|
112
|
+
...maxTokens === undefined ? {} : { maxTokens },
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
models,
|
|
117
|
+
hasMore: body?.has_more === true,
|
|
118
|
+
lastId: nonEmptyString(body?.last_id),
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function discoveryHeaders(apiKey) {
|
|
123
|
+
const value = requireNonEmptyString(apiKey, 'apiKey').trim()
|
|
124
|
+
if (value.length === 0) throw new Error('Claude 模型目录:请先填写 API 密钥')
|
|
125
|
+
try {
|
|
126
|
+
return new Headers({
|
|
127
|
+
accept: 'application/json',
|
|
128
|
+
'anthropic-version': '2023-06-01',
|
|
129
|
+
'x-api-key': value,
|
|
130
|
+
})
|
|
131
|
+
} catch (error) {
|
|
132
|
+
throw new Error('Claude 模型目录:API 密钥包含无法用于 HTTP 请求头的字符', { cause: error })
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function responseErrorMessage(body) {
|
|
137
|
+
const message = body?.error?.message
|
|
138
|
+
return typeof message === 'string' && message.length > 0 ? message.slice(0, 500) : undefined
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Query only the native Anthropic Models API, including cursor pagination. */
|
|
142
|
+
export async function discoverAnthropicModels({ baseURL, apiKey, signal, fetchImpl = globalThis.fetch }) {
|
|
143
|
+
if (typeof fetchImpl !== 'function') throw new Error('Claude 模型目录:当前运行环境没有 fetch')
|
|
144
|
+
const endpoint = anthropicModelsUrl(baseURL)
|
|
145
|
+
const headers = discoveryHeaders(apiKey)
|
|
146
|
+
const models = []
|
|
147
|
+
const seen = new Set()
|
|
148
|
+
let afterId
|
|
149
|
+
for (let page = 0; page < MAX_DISCOVERY_PAGES; page += 1) {
|
|
150
|
+
if (signal?.aborted) throw new Error('Claude 模型目录:获取已取消')
|
|
151
|
+
const url = new URL(endpoint)
|
|
152
|
+
if (afterId !== undefined) url.searchParams.set('after_id', afterId)
|
|
153
|
+
let response
|
|
154
|
+
try {
|
|
155
|
+
response = await fetchImpl(url, { method: 'GET', headers, signal })
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (signal?.aborted) throw new Error('Claude 模型目录:获取已取消', { cause: error })
|
|
158
|
+
throw new Error(`Claude 模型目录:无法连接 ${url.origin}`, { cause: error })
|
|
159
|
+
}
|
|
160
|
+
const text = await readBoundedResponse(response, url.href)
|
|
161
|
+
let body
|
|
162
|
+
try {
|
|
163
|
+
body = JSON.parse(text)
|
|
164
|
+
} catch (error) {
|
|
165
|
+
throw new Error(`Claude 模型目录:${url.href} 未返回 JSON`, { cause: error })
|
|
166
|
+
}
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
const detail = responseErrorMessage(body)
|
|
169
|
+
throw new Error(
|
|
170
|
+
`Claude 模型目录:${url.href} 返回 ${response.status}`
|
|
171
|
+
+ `${response.status === 401 || response.status === 403 ? ',请检查 API 密钥' : ''}`
|
|
172
|
+
+ `${detail === undefined ? '' : `:${detail}`}`,
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
const parsed = readAnthropicModelPage(body)
|
|
176
|
+
for (const model of parsed.models) {
|
|
177
|
+
if (seen.has(model.id)) continue
|
|
178
|
+
seen.add(model.id)
|
|
179
|
+
models.push(model)
|
|
180
|
+
}
|
|
181
|
+
if (!parsed.hasMore) return models
|
|
182
|
+
if (parsed.lastId === undefined || parsed.lastId === afterId) {
|
|
183
|
+
throw new Error('Claude 模型目录:分页响应缺少有效的 last_id')
|
|
184
|
+
}
|
|
185
|
+
afterId = parsed.lastId
|
|
186
|
+
}
|
|
187
|
+
throw new Error(`Claude 模型目录:分页超过 ${MAX_DISCOVERY_PAGES} 页`)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function configuredCredentialRef(ctx, provider) {
|
|
191
|
+
if (typeof provider !== 'string' || provider.length === 0) return undefined
|
|
192
|
+
const section = ctx.get('settings')?.get(PI_AI_SETTINGS_NS)
|
|
193
|
+
const profile = section?.providers?.[provider]
|
|
194
|
+
const ref = profile?.apiKeyEnv
|
|
195
|
+
return typeof ref === 'string' && ref.length > 0 ? ref : undefined
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function discoveryApiKey(ctx, request) {
|
|
199
|
+
if (typeof request.apiKey === 'string' && request.apiKey.trim().length > 0) return request.apiKey
|
|
200
|
+
const ref = configuredCredentialRef(ctx, request.provider)
|
|
201
|
+
if (ref === undefined) throw new Error('Claude 模型目录:请先填写 API 密钥')
|
|
202
|
+
const value = (await ctx.get('credentials')?.resolve(ref))?.value ?? process.env[ref]
|
|
203
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
204
|
+
throw new Error(`Claude 模型目录:未找到已保存的凭据 ${ref}`)
|
|
205
|
+
}
|
|
206
|
+
return value
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function installClaudeModelDiscovery(ctx) {
|
|
210
|
+
const llm = ctx.llm
|
|
211
|
+
const upstream = llm.discoverModels
|
|
212
|
+
const wrapped = async function (settingsNs, request) {
|
|
213
|
+
if (settingsNs !== PI_AI_SETTINGS_NS || request.api !== CLAUDE_DISCOVERY_API) {
|
|
214
|
+
return upstream.call(this, settingsNs, request)
|
|
215
|
+
}
|
|
216
|
+
// The built-in Anthropic route already has pi-ai's catalog and should keep
|
|
217
|
+
// using it. The plugin extension is only for hand-declared Claude routes.
|
|
218
|
+
if (request.provider === 'anthropic') {
|
|
219
|
+
return upstream.call(this, settingsNs, { ...request, api: 'anthropic-messages' })
|
|
220
|
+
}
|
|
221
|
+
if (typeof request.baseURL !== 'string' || request.baseURL.length === 0) {
|
|
222
|
+
throw new Error('Claude 模型目录:请先填写 API 地址')
|
|
223
|
+
}
|
|
224
|
+
const apiKey = await discoveryApiKey(ctx, request)
|
|
225
|
+
return discoverAnthropicModels({
|
|
226
|
+
baseURL: request.baseURL,
|
|
227
|
+
apiKey,
|
|
228
|
+
signal: request.signal,
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
llm.discoverModels = wrapped
|
|
232
|
+
ctx.effect(() => () => {
|
|
233
|
+
if (llm.discoverModels === wrapped) llm.discoverModels = upstream
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function resolveConfig(config = {}) {
|
|
238
|
+
if (!Array.isArray(config.routes) || config.routes.length === 0) {
|
|
239
|
+
throw new Error('claude-provider: routes must contain at least one provider')
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const targets = new Map()
|
|
243
|
+
for (const [routeIndex, route] of config.routes.entries()) {
|
|
244
|
+
const provider = requireNonEmptyString(route?.provider, `routes[${routeIndex}].provider`)
|
|
245
|
+
if (!Array.isArray(route.models) || route.models.length === 0) {
|
|
246
|
+
throw new Error(`claude-provider: route "${provider}" must contain at least one model`)
|
|
247
|
+
}
|
|
248
|
+
if (targets.has(provider)) {
|
|
249
|
+
throw new Error(`claude-provider: duplicate provider route "${provider}"`)
|
|
250
|
+
}
|
|
251
|
+
targets.set(provider, new Set(route.models.map((model, modelIndex) =>
|
|
252
|
+
requireNonEmptyString(model, `routes[${routeIndex}].models[${modelIndex}]`))))
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const effortMap = { ...DEFAULT_EFFORT_MAP, ...config.effortMap }
|
|
256
|
+
for (const [level, effort] of Object.entries(effortMap)) {
|
|
257
|
+
requireNonEmptyString(level, 'effortMap key')
|
|
258
|
+
requireNonEmptyString(effort, `effortMap.${level}`)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return { targets, effortMap, debug: config.debug === true }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function isTarget(targets, provider, model) {
|
|
265
|
+
const matches = patterns => patterns !== undefined && [...patterns].some(pattern =>
|
|
266
|
+
pattern === '*' || pattern === model || pattern.endsWith('*') && model.startsWith(pattern.slice(0, -1)))
|
|
267
|
+
return matches(targets.get(provider)) || matches(targets.get('*'))
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Four- and five-level model profiles are adaptive; the two-level toggle is legacy budget thinking. */
|
|
271
|
+
export function shouldUseAdaptiveThinking(modelInfo) {
|
|
272
|
+
const efforts = modelInfo?.reasoning?.efforts
|
|
273
|
+
if (!Array.isArray(efforts)) return false
|
|
274
|
+
const ids = new Set(efforts.map(effort => String(effort.id)))
|
|
275
|
+
return ids.has('low') && ids.has('medium') && ids.has('high') && ids.has('max')
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function effortFromBudget(budget) {
|
|
279
|
+
if (typeof budget !== 'number' || !Number.isFinite(budget)) return 'high'
|
|
280
|
+
if (budget <= 2048) return 'low'
|
|
281
|
+
if (budget <= 8192) return 'medium'
|
|
282
|
+
return 'high'
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function selectedEffort(state, thinking) {
|
|
286
|
+
if (state.level !== undefined && state.level !== 'off') {
|
|
287
|
+
return state.effortMap[state.level] ?? state.level
|
|
288
|
+
}
|
|
289
|
+
return effortFromBudget(thinking.budget_tokens)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Convert one matching Anthropic Messages body from legacy extended thinking
|
|
294
|
+
* to adaptive thinking. The input object is never mutated.
|
|
295
|
+
*/
|
|
296
|
+
export function rewriteAnthropicPayload(payload, state) {
|
|
297
|
+
if (payload === null || typeof payload !== 'object' || Array.isArray(payload)) return payload
|
|
298
|
+
if (payload.model !== state.model) return payload
|
|
299
|
+
const thinking = payload.thinking
|
|
300
|
+
if (thinking === null || typeof thinking !== 'object' || Array.isArray(thinking)) return payload
|
|
301
|
+
|
|
302
|
+
if (thinking.type === 'disabled') {
|
|
303
|
+
const rewritten = { ...payload }
|
|
304
|
+
delete rewritten.thinking
|
|
305
|
+
delete rewritten.output_config
|
|
306
|
+
return rewritten
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (thinking.type !== 'enabled') return payload
|
|
310
|
+
const { budget_tokens: _budgetTokens, ...preservedThinking } = thinking
|
|
311
|
+
return {
|
|
312
|
+
...payload,
|
|
313
|
+
thinking: { ...preservedThinking, type: 'adaptive' },
|
|
314
|
+
output_config: {
|
|
315
|
+
...(payload.output_config ?? {}),
|
|
316
|
+
effort: selectedEffort(state, thinking),
|
|
317
|
+
},
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function withoutLegacyThinkingBeta(headers) {
|
|
322
|
+
if (headers === undefined) return undefined
|
|
323
|
+
const rewritten = new Headers(headers)
|
|
324
|
+
const value = rewritten.get('anthropic-beta')
|
|
325
|
+
if (value === null) return rewritten
|
|
326
|
+
const features = value.split(',').map(feature => feature.trim()).filter(Boolean)
|
|
327
|
+
const kept = features.filter(feature => feature !== INTERLEAVED_THINKING_BETA)
|
|
328
|
+
if (kept.length === features.length) return rewritten
|
|
329
|
+
if (kept.length === 0) rewritten.delete('anthropic-beta')
|
|
330
|
+
else rewritten.set('anthropic-beta', kept.join(','))
|
|
331
|
+
return rewritten
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function decodeBody(body) {
|
|
335
|
+
if (typeof body === 'string') return body
|
|
336
|
+
if (body instanceof Uint8Array) return new TextDecoder().decode(body)
|
|
337
|
+
if (body instanceof ArrayBuffer) return new TextDecoder().decode(new Uint8Array(body))
|
|
338
|
+
if (ArrayBuffer.isView(body)) {
|
|
339
|
+
return new TextDecoder().decode(new Uint8Array(body.buffer, body.byteOffset, body.byteLength))
|
|
340
|
+
}
|
|
341
|
+
return undefined
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function rewriteJsonBody(body, state) {
|
|
345
|
+
const text = decodeBody(body)
|
|
346
|
+
if (text === undefined) return undefined
|
|
347
|
+
let parsed
|
|
348
|
+
try {
|
|
349
|
+
parsed = JSON.parse(text)
|
|
350
|
+
} catch {
|
|
351
|
+
return undefined
|
|
352
|
+
}
|
|
353
|
+
const rewritten = rewriteAnthropicPayload(parsed, state)
|
|
354
|
+
return rewritten === parsed ? undefined : JSON.stringify(rewritten)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function rewriteFetchArguments(input, init, state) {
|
|
358
|
+
if (init?.body !== undefined && init.body !== null) {
|
|
359
|
+
const body = rewriteJsonBody(init.body, state)
|
|
360
|
+
if (body === undefined) return undefined
|
|
361
|
+
return [input, { ...init, body, headers: withoutLegacyThinkingBeta(init.headers) }]
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (typeof Request !== 'undefined' && input instanceof Request && input.body !== null) {
|
|
365
|
+
const text = await input.clone().text()
|
|
366
|
+
const body = rewriteJsonBody(text, state)
|
|
367
|
+
if (body === undefined) return undefined
|
|
368
|
+
const requestInit = {
|
|
369
|
+
body,
|
|
370
|
+
headers: withoutLegacyThinkingBeta(input.headers),
|
|
371
|
+
}
|
|
372
|
+
if (input.method !== 'GET' && input.method !== 'HEAD') requestInit.duplex = 'half'
|
|
373
|
+
return [new Request(input, requestInit), init]
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return undefined
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function contextualStream(storage, state, next) {
|
|
380
|
+
return {
|
|
381
|
+
[Symbol.asyncIterator]() {
|
|
382
|
+
let iterator
|
|
383
|
+
const invoke = (method, value) => storage.run(state, () => {
|
|
384
|
+
iterator ??= next()[Symbol.asyncIterator]()
|
|
385
|
+
const operation = iterator[method]
|
|
386
|
+
if (operation === undefined) return Promise.resolve({ done: true, value })
|
|
387
|
+
return operation.call(iterator, value)
|
|
388
|
+
})
|
|
389
|
+
return {
|
|
390
|
+
next: value => invoke('next', value),
|
|
391
|
+
return: value => invoke('return', value),
|
|
392
|
+
throw: error => invoke('throw', error),
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function* modelAwareStream(ctx, storage, resolved, options, next) {
|
|
399
|
+
let modelInfo
|
|
400
|
+
try {
|
|
401
|
+
modelInfo = await ctx.llm.resolveModelInfo(options.provider, options.model, options.signal)
|
|
402
|
+
} catch {
|
|
403
|
+
yield* next()
|
|
404
|
+
return
|
|
405
|
+
}
|
|
406
|
+
if (!shouldUseAdaptiveThinking(modelInfo)) {
|
|
407
|
+
yield* next()
|
|
408
|
+
return
|
|
409
|
+
}
|
|
410
|
+
const state = {
|
|
411
|
+
provider: options.provider,
|
|
412
|
+
model: options.model,
|
|
413
|
+
level: options.reasoningEffort === undefined ? undefined : String(options.reasoningEffort),
|
|
414
|
+
effortMap: resolved.effortMap,
|
|
415
|
+
}
|
|
416
|
+
yield* contextualStream(storage, state, next)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export function apply(ctx, config) {
|
|
420
|
+
const resolved = resolveConfig(config)
|
|
421
|
+
installClaudeModelDiscovery(ctx)
|
|
422
|
+
const storage = new AsyncLocalStorage()
|
|
423
|
+
const upstreamFetch = globalThis.fetch
|
|
424
|
+
if (typeof upstreamFetch !== 'function') {
|
|
425
|
+
throw new Error('claude-provider: global fetch is unavailable')
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const adaptiveFetch = async (input, init) => {
|
|
429
|
+
const state = storage.getStore()
|
|
430
|
+
if (state === undefined) return upstreamFetch(input, init)
|
|
431
|
+
const rewritten = await rewriteFetchArguments(input, init, state)
|
|
432
|
+
if (rewritten !== undefined && resolved.debug) {
|
|
433
|
+
process.stderr.write(
|
|
434
|
+
`[claude-provider] rewrote ${state.provider}/${state.model}`
|
|
435
|
+
+ ` effort=${state.level ?? 'inferred'}\n`,
|
|
436
|
+
)
|
|
437
|
+
}
|
|
438
|
+
return rewritten === undefined
|
|
439
|
+
? upstreamFetch(input, init)
|
|
440
|
+
: upstreamFetch(rewritten[0], rewritten[1])
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
globalThis.fetch = adaptiveFetch
|
|
444
|
+
ctx.effect(() => () => {
|
|
445
|
+
storage.disable()
|
|
446
|
+
if (globalThis.fetch === adaptiveFetch) globalThis.fetch = upstreamFetch
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
ctx.on('llm/stream', (options, next) => {
|
|
450
|
+
if (!isTarget(resolved.targets, options.provider, options.model)) return next()
|
|
451
|
+
return modelAwareStream(ctx, storage, resolved, options, next)
|
|
452
|
+
})
|
|
453
|
+
}
|