@eduwork/dsh-oidc 0.1.0-alpha.11 → 0.2.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.en.md +58 -1
- package/CHANGELOG.md +58 -1
- package/CODE_OF_CONDUCT.en.md +17 -0
- package/CODE_OF_CONDUCT.md +17 -0
- package/CONTRIBUTING.en.md +56 -0
- package/CONTRIBUTING.md +56 -0
- package/GOVERNANCE.en.md +41 -0
- package/GOVERNANCE.md +41 -0
- package/MAINTAINERS.en.md +13 -0
- package/MAINTAINERS.md +13 -0
- package/README.en.md +24 -22
- package/README.md +18 -16
- package/SECURITY.en.md +45 -0
- package/SECURITY.md +45 -0
- package/SUPPORT.en.md +15 -0
- package/SUPPORT.md +15 -0
- package/THIRD_PARTY_NOTICES.en.md +7 -5
- package/THIRD_PARTY_NOTICES.md +7 -5
- package/cordis.patch.yml +2 -1
- package/docs/EDUWORK-MIGRATION.en.md +17 -5
- package/docs/EDUWORK-MIGRATION.md +17 -5
- package/docs/account-extensions.en.md +40 -0
- package/docs/account-extensions.md +42 -0
- package/docs/architecture.en.md +9 -9
- package/docs/architecture.md +11 -10
- package/docs/compatibility.en.md +13 -3
- package/docs/compatibility.md +13 -3
- package/docs/decisions/0001-one-plugin-composition-boundary.en.md +1 -1
- package/docs/decisions/0001-one-plugin-composition-boundary.md +1 -1
- package/docs/desktop-host.en.md +69 -0
- package/docs/desktop-host.md +79 -0
- package/docs/development.en.md +42 -0
- package/docs/development.md +42 -0
- package/docs/dsh-integration.en.md +24 -8
- package/docs/dsh-integration.md +24 -8
- package/docs/ecnu-reference.en.md +3 -3
- package/docs/ecnu-reference.md +3 -3
- package/docs/enterprise-profile.en.md +21 -13
- package/docs/enterprise-profile.md +20 -12
- package/docs/getting-started.en.md +13 -17
- package/docs/getting-started.md +17 -19
- package/docs/key-binding-protocol.en.md +2 -2
- package/docs/key-binding-protocol.md +2 -2
- package/docs/oidc-interoperability.en.md +3 -3
- package/docs/oidc-interoperability.md +2 -2
- package/docs/public-resource-protocol.en.md +51 -0
- package/docs/public-resource-protocol.md +50 -0
- package/docs/release-checklist.en.md +13 -8
- package/docs/release-checklist.md +13 -8
- package/docs/server-integration-contract.en.md +124 -8
- package/docs/server-integration-contract.md +136 -8
- package/examples/ecnu.enterprise-profile.example.json +2 -2
- package/examples/enterprise-profile.example.json +78 -77
- package/examples/identity-only.example.json +10 -0
- package/examples/resources-discovery.example.json +20 -0
- package/lib/callback-page.js +42 -0
- package/lib/client.js +938 -294
- package/lib/desktop-oidc.js +205 -0
- package/lib/index.js +148 -18
- package/lib/oidc.js +314 -58
- package/lib/profile.js +96 -39
- package/lib/provider/core.js +76 -15
- package/lib/provider/index.js +54 -22
- package/lib/provider/transform-adapter.js +32 -11
- package/lib/resources.js +23 -0
- package/lib/typert-schemas.js +34 -3
- package/lib/typert.host.js +42 -36
- package/lib/typert.remote-client.js +41 -35
- package/package.json +117 -23
- package/protocol/openapi.yaml +2 -2
- package/protocol/resources.openapi.yaml +51 -0
- package/schema/enterprise-profile.v1alpha1.schema.json +193 -174
package/lib/profile.js
CHANGED
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs'
|
|
2
2
|
|
|
3
3
|
export const PROFILE_SCHEMA_VERSION = 'dsh-oidc/v1alpha1'
|
|
4
|
+
export const DEFAULT_CREDENTIAL_REF = 'EDUWORK_API_KEY'
|
|
4
5
|
const MAX_PROFILE_BYTES = 512 * 1024
|
|
5
6
|
const idPattern = /^[a-z][a-z0-9-]{0,63}$/
|
|
6
7
|
const credentialPattern = /^[A-Za-z_][A-Za-z0-9_]*$/
|
|
7
8
|
const colorPattern = /^#[0-9a-fA-F]{6}$/
|
|
8
9
|
const allowedModalities = new Set(['text', 'image'])
|
|
10
|
+
const allowedThinkingLevels = new Set(['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'])
|
|
11
|
+
const allowedMaxTokensFields = new Set(['max_completion_tokens', 'max_tokens'])
|
|
12
|
+
const allowedThinkingFormats = new Set([
|
|
13
|
+
'openai', 'deepseek', 'openrouter', 'together', 'baseten', 'zai', 'qwen',
|
|
14
|
+
'chat-template', 'qwen-chat-template', 'string-thinking', 'ant-ling',
|
|
15
|
+
])
|
|
9
16
|
const allowedRootKeys = new Set([
|
|
10
17
|
'schemaVersion', 'id', 'displayName', 'organization', 'nativeInstitutionID',
|
|
11
18
|
'allowInsecureDevelopment', 'insecureDevelopmentOrigin', 'brand', 'oidc', 'keyBinding', 'provider',
|
|
12
19
|
])
|
|
13
20
|
const allowedOidcKeys = new Set(['issuer', 'clientId', 'scopes'])
|
|
14
|
-
const allowedKeyBindingKeys = new Set(['baseURL', 'credentialRef'])
|
|
21
|
+
const allowedKeyBindingKeys = new Set(['type', 'baseURL', 'credentialRef'])
|
|
15
22
|
const allowedBrandKeys = new Set([
|
|
16
23
|
'productName', 'organizationName', 'mark', 'logoURL', 'primaryColor',
|
|
17
24
|
'loginTitle', 'loginDescription', 'supportURL',
|
|
@@ -19,11 +26,11 @@ const allowedBrandKeys = new Set([
|
|
|
19
26
|
const allowedProviderKeys = new Set([
|
|
20
27
|
'id', 'displayName', 'adapter', 'baseURL', 'reasoning', 'defaultContextWindow',
|
|
21
28
|
'defaultMaxTokens', 'maxRequestImageBytes', 'requestImagePixelBudget',
|
|
22
|
-
'requestImageMaxBytes', 'streamIdleTimeoutMs', 'retryPolicy', 'compat', 'models',
|
|
29
|
+
'requestImageMaxBytes', 'streamIdleTimeoutMs', 'retryPolicy', 'compat', 'models', 'modelSource',
|
|
23
30
|
])
|
|
24
31
|
const allowedModelKeys = new Set([
|
|
25
32
|
'id', 'name', 'input', 'contextWindow', 'maxTokens', 'reasoning',
|
|
26
|
-
'reasoningEfforts', 'compat',
|
|
33
|
+
'reasoningEfforts', 'defaultReasoningEffort', 'compat',
|
|
27
34
|
])
|
|
28
35
|
const allowedCompatKeys = new Set([
|
|
29
36
|
'supportsDeveloperRole', 'supportsStore', 'supportsReasoningEffort',
|
|
@@ -141,9 +148,15 @@ function normalizeCompat(value, label) {
|
|
|
141
148
|
const result = {}
|
|
142
149
|
for (const key of allowedCompatKeys) {
|
|
143
150
|
if (source[key] === undefined) continue
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
151
|
+
if (key === 'maxTokensField') {
|
|
152
|
+
result[key] = text(source[key], `${label}.${key}`, 64)
|
|
153
|
+
if (!allowedMaxTokensFields.has(result[key])) throw new Error(`${label}.${key} is not supported by DSH pi-ai`)
|
|
154
|
+
} else if (key === 'thinkingFormat') {
|
|
155
|
+
result[key] = text(source[key], `${label}.${key}`, 64)
|
|
156
|
+
if (!allowedThinkingFormats.has(result[key])) throw new Error(`${label}.${key} is not supported by DSH pi-ai`)
|
|
157
|
+
} else {
|
|
158
|
+
result[key] = boolean(source[key], `${label}.${key}`)
|
|
159
|
+
}
|
|
147
160
|
}
|
|
148
161
|
return Object.freeze(result)
|
|
149
162
|
}
|
|
@@ -151,15 +164,18 @@ function normalizeCompat(value, label) {
|
|
|
151
164
|
function normalizeReasoningEfforts(value, providerID, modelID) {
|
|
152
165
|
if (value === false) return false
|
|
153
166
|
const source = object(value, `${providerID}.${modelID}.reasoningEfforts`)
|
|
154
|
-
if (Object.keys(source).length === 0 || Object.keys(source).length >
|
|
167
|
+
if (Object.keys(source).length === 0 || Object.keys(source).length > allowedThinkingLevels.size) {
|
|
168
|
+
throw new Error(`${providerID}.${modelID}.reasoningEfforts must contain 1-${allowedThinkingLevels.size} entries`)
|
|
169
|
+
}
|
|
155
170
|
return Object.freeze(Object.fromEntries(Object.entries(source).map(([level, wire]) => {
|
|
156
|
-
|
|
171
|
+
if (!allowedThinkingLevels.has(level)) throw new Error(`${providerID}.${modelID}.reasoningEfforts level ${level} is not supported by DSH pi-ai`)
|
|
172
|
+
if (wire === null && level !== 'off') throw new Error(`${providerID}.${modelID}.reasoningEfforts.${level} may be null only for off`)
|
|
157
173
|
if (wire !== null) text(wire, `${providerID}.${modelID}.reasoningEfforts.${level}`, 32)
|
|
158
174
|
return [level, wire]
|
|
159
175
|
})))
|
|
160
176
|
}
|
|
161
177
|
|
|
162
|
-
function normalizeModels(models, providerID) {
|
|
178
|
+
export function normalizeModels(models, providerID) {
|
|
163
179
|
if (!Array.isArray(models) || models.length === 0 || models.length > 128) throw new Error(`${providerID}.models must contain 1-128 entries`)
|
|
164
180
|
const seen = new Set()
|
|
165
181
|
return Object.freeze(models.map((raw, index) => {
|
|
@@ -174,6 +190,19 @@ function normalizeModels(models, providerID) {
|
|
|
174
190
|
throw new Error(`${providerID}.${id}.input must contain text and/or image`)
|
|
175
191
|
}
|
|
176
192
|
if (new Set(input).size !== input.length) throw new Error(`${providerID}.${id}.input contains duplicates`)
|
|
193
|
+
const normalizedReasoningEfforts = model.reasoningEfforts === undefined
|
|
194
|
+
? undefined
|
|
195
|
+
: normalizeReasoningEfforts(model.reasoningEfforts, providerID, id)
|
|
196
|
+
let defaultReasoningEffort
|
|
197
|
+
if (model.defaultReasoningEffort !== undefined) {
|
|
198
|
+
defaultReasoningEffort = text(model.defaultReasoningEffort, `${providerID}.${id}.defaultReasoningEffort`, 32)
|
|
199
|
+
if (!allowedThinkingLevels.has(defaultReasoningEffort)) {
|
|
200
|
+
throw new Error(`${providerID}.${id}.defaultReasoningEffort is not supported by DSH pi-ai`)
|
|
201
|
+
}
|
|
202
|
+
if (!normalizedReasoningEfforts || normalizedReasoningEfforts === false || !(defaultReasoningEffort in normalizedReasoningEfforts)) {
|
|
203
|
+
throw new Error(`${providerID}.${id}.defaultReasoningEffort must be one of the model's declared reasoningEfforts`)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
177
206
|
return Object.freeze({
|
|
178
207
|
id,
|
|
179
208
|
name: model.name === undefined ? id : text(model.name, `${providerID}.${id}.name`, 256),
|
|
@@ -181,14 +210,15 @@ function normalizeModels(models, providerID) {
|
|
|
181
210
|
...(model.contextWindow === undefined ? {} : { contextWindow: positiveInteger(model.contextWindow, `${providerID}.${id}.contextWindow`) }),
|
|
182
211
|
...(model.maxTokens === undefined ? {} : { maxTokens: positiveInteger(model.maxTokens, `${providerID}.${id}.maxTokens`) }),
|
|
183
212
|
...(model.reasoning === undefined ? {} : { reasoning: boolean(model.reasoning, `${providerID}.${id}.reasoning`) }),
|
|
184
|
-
...(
|
|
213
|
+
...(normalizedReasoningEfforts === undefined ? {} : { reasoningEfforts: normalizedReasoningEfforts }),
|
|
214
|
+
...(defaultReasoningEffort === undefined ? {} : { defaultReasoningEffort }),
|
|
185
215
|
...(model.compat === undefined ? {} : { compat: normalizeCompat(model.compat, `${providerID}.${id}.compat`) }),
|
|
186
216
|
})
|
|
187
217
|
}))
|
|
188
218
|
}
|
|
189
219
|
|
|
190
|
-
function
|
|
191
|
-
return `${
|
|
220
|
+
export function legacyCredentialRefs(profile) {
|
|
221
|
+
return [...new Set([profile.id, profile.provider?.id].filter(Boolean).map(id => `${id.toUpperCase().replace(/[^A-Z0-9]/g, '_')}_API_KEY`))]
|
|
192
222
|
}
|
|
193
223
|
|
|
194
224
|
function normalizeRetryPolicy(value) {
|
|
@@ -252,20 +282,29 @@ export function normalizeEnterpriseProfile(raw) {
|
|
|
252
282
|
throw new Error('profile.insecureDevelopmentOrigin requires allowInsecureDevelopment=true')
|
|
253
283
|
}
|
|
254
284
|
const oidc = object(source.oidc, 'profile.oidc')
|
|
255
|
-
const
|
|
256
|
-
|
|
285
|
+
const identityOnly = source.keyBinding === undefined && source.provider === undefined
|
|
286
|
+
if (!identityOnly && (source.keyBinding === undefined || source.provider === undefined)) {
|
|
287
|
+
throw new Error('profile.keyBinding and profile.provider must either both be present or both be omitted')
|
|
288
|
+
}
|
|
289
|
+
const keyBinding = identityOnly ? {} : object(source.keyBinding, 'profile.keyBinding')
|
|
290
|
+
const provider = identityOnly ? {} : object(source.provider, 'profile.provider')
|
|
257
291
|
exactKeys(oidc, allowedOidcKeys, 'profile.oidc')
|
|
258
292
|
exactKeys(keyBinding, allowedKeyBindingKeys, 'profile.keyBinding')
|
|
259
293
|
exactKeys(provider, allowedProviderKeys, 'profile.provider')
|
|
260
|
-
const providerID = text(provider.id, 'profile.provider.id', 64)
|
|
294
|
+
const providerID = identityOnly ? id : text(provider.id, 'profile.provider.id', 64)
|
|
261
295
|
if (!idPattern.test(providerID)) throw new Error('profile.provider.id is invalid')
|
|
262
|
-
|
|
263
|
-
?
|
|
296
|
+
let credentialRef = keyBinding.credentialRef === undefined
|
|
297
|
+
? DEFAULT_CREDENTIAL_REF
|
|
264
298
|
: text(keyBinding.credentialRef, 'profile.keyBinding.credentialRef', 128)
|
|
265
299
|
if (!credentialPattern.test(credentialRef)) {
|
|
266
300
|
throw new Error('profile.keyBinding.credentialRef must be a valid DSH credential reference')
|
|
267
301
|
}
|
|
268
|
-
if (provider
|
|
302
|
+
if (legacyCredentialRefs({ id, provider: { id: providerID } }).includes(credentialRef)) credentialRef = DEFAULT_CREDENTIAL_REF
|
|
303
|
+
if (!identityOnly && provider.adapter !== 'openai-compatible') throw new Error(`unsupported provider adapter ${String(provider.adapter)}`)
|
|
304
|
+
const protocol = keyBinding.type ?? 'worker-user-center-v1'
|
|
305
|
+
if (!['worker-user-center-v1', 'eduwork-resources-v1'].includes(protocol)) throw new Error('profile.keyBinding.type is not supported')
|
|
306
|
+
const modelSource = provider.modelSource ?? 'profile'
|
|
307
|
+
if (!['profile', 'discovery'].includes(modelSource)) throw new Error('profile.provider.modelSource must be profile or discovery')
|
|
269
308
|
const scopes = oidc.scopes
|
|
270
309
|
if (!Array.isArray(scopes) || scopes.length < 2 || scopes.length > 64 || !scopes.includes('openid') || !scopes.includes('profile') || scopes.some(scope => typeof scope !== 'string' || scope.length > 128 || scope.trim() !== scope || scope === '' || /\s/.test(scope))) {
|
|
271
310
|
throw new Error('profile.oidc.scopes must contain openid and profile; every scope must be a non-empty token')
|
|
@@ -285,8 +324,8 @@ export function normalizeEnterpriseProfile(raw) {
|
|
|
285
324
|
clientId: text(oidc.clientId, 'profile.oidc.clientId', 256),
|
|
286
325
|
scopes: Object.freeze([...scopes]),
|
|
287
326
|
}),
|
|
288
|
-
keyBinding: Object.freeze({
|
|
289
|
-
type:
|
|
327
|
+
...(identityOnly ? {} : { keyBinding: Object.freeze({
|
|
328
|
+
type: protocol,
|
|
290
329
|
baseURL: exactURL(keyBinding.baseURL, 'profile.keyBinding.baseURL', allowInsecureDevelopment, allowedInsecureOrigin),
|
|
291
330
|
providerId: providerID,
|
|
292
331
|
credentialRef,
|
|
@@ -296,7 +335,13 @@ export function normalizeEnterpriseProfile(raw) {
|
|
|
296
335
|
displayName: text(provider.displayName ?? source.displayName, 'profile.provider.displayName', 120),
|
|
297
336
|
adapter: provider.adapter,
|
|
298
337
|
baseURL: exactURL(provider.baseURL, 'profile.provider.baseURL', allowInsecureDevelopment, allowedInsecureOrigin),
|
|
299
|
-
reasoning: provider.reasoning === undefined
|
|
338
|
+
reasoning: provider.reasoning === undefined
|
|
339
|
+
? 'high'
|
|
340
|
+
: (() => {
|
|
341
|
+
const reasoning = text(provider.reasoning, 'profile.provider.reasoning', 32)
|
|
342
|
+
if (!allowedThinkingLevels.has(reasoning)) throw new Error('profile.provider.reasoning is not supported by DSH pi-ai')
|
|
343
|
+
return reasoning
|
|
344
|
+
})(),
|
|
300
345
|
defaultContextWindow: optionalPositiveInteger(provider.defaultContextWindow, 'profile.provider.defaultContextWindow'),
|
|
301
346
|
defaultMaxTokens: optionalPositiveInteger(provider.defaultMaxTokens, 'profile.provider.defaultMaxTokens'),
|
|
302
347
|
maxRequestImageBytes: optionalPositiveInteger(provider.maxRequestImageBytes, 'profile.provider.maxRequestImageBytes'),
|
|
@@ -305,7 +350,10 @@ export function normalizeEnterpriseProfile(raw) {
|
|
|
305
350
|
streamIdleTimeoutMs: optionalPositiveInteger(provider.streamIdleTimeoutMs, 'profile.provider.streamIdleTimeoutMs'),
|
|
306
351
|
retryPolicy: provider.retryPolicy === undefined ? undefined : normalizeRetryPolicy(provider.retryPolicy),
|
|
307
352
|
compat: provider.compat === undefined ? undefined : normalizeCompat(provider.compat, 'profile.provider.compat'),
|
|
308
|
-
|
|
353
|
+
modelSource,
|
|
354
|
+
models: modelSource === 'discovery' && (provider.models === undefined || provider.models.length === 0)
|
|
355
|
+
? Object.freeze([]) : normalizeModels(provider.models, providerID),
|
|
356
|
+
}),
|
|
309
357
|
}),
|
|
310
358
|
})
|
|
311
359
|
}
|
|
@@ -317,8 +365,10 @@ export function loadEnterpriseProfiles(raw = {}, environment = process.env) {
|
|
|
317
365
|
if (!Array.isArray(raw.profiles)) throw new Error('dsh-oidc profiles must be an array')
|
|
318
366
|
profiles.push(...raw.profiles.map(normalizeEnterpriseProfile))
|
|
319
367
|
}
|
|
320
|
-
|
|
321
|
-
|
|
368
|
+
const pathEnv = raw.profilePathEnv ?? 'EDUWORK_OIDC_PROFILE'
|
|
369
|
+
const profilePath = environment[pathEnv] || (pathEnv === 'EDUWORK_OIDC_PROFILE' ? environment.DSH_OIDC_ENTERPRISE_PROFILE : undefined)
|
|
370
|
+
if (typeof profilePath === 'string' && profilePath) {
|
|
371
|
+
const parsed = JSON.parse(readFileSync(profilePath, 'utf8'))
|
|
322
372
|
const rows = Array.isArray(parsed) ? parsed : parsed.profiles ?? [parsed]
|
|
323
373
|
profiles.push(...rows.map(normalizeEnterpriseProfile))
|
|
324
374
|
}
|
|
@@ -326,31 +376,38 @@ export function loadEnterpriseProfiles(raw = {}, environment = process.env) {
|
|
|
326
376
|
const providers = new Set()
|
|
327
377
|
for (const profile of profiles) {
|
|
328
378
|
if (unique.has(profile.id)) throw new Error(`dsh-oidc repeats profile ${profile.id}`)
|
|
329
|
-
if (providers.has(profile.provider.id)) throw new Error(`dsh-oidc repeats provider route ${profile.provider.id}`)
|
|
379
|
+
if (profile.provider && providers.has(profile.provider.id)) throw new Error(`dsh-oidc repeats provider route ${profile.provider.id}`)
|
|
330
380
|
unique.set(profile.id, profile)
|
|
331
|
-
providers.add(profile.provider.id)
|
|
381
|
+
if (profile.provider) providers.add(profile.provider.id)
|
|
332
382
|
}
|
|
333
383
|
return unique
|
|
334
384
|
}
|
|
335
385
|
|
|
336
386
|
export function enterpriseProviderConfig(profiles) {
|
|
337
387
|
return {
|
|
338
|
-
providers: Object.fromEntries([...profiles.values()].map(profile => [profile.provider.id, {
|
|
388
|
+
providers: Object.fromEntries([...profiles.values()].filter(profile => profile.provider?.models.length).map(profile => [profile.provider.id, {
|
|
339
389
|
displayName: profile.provider.displayName,
|
|
340
390
|
apiKeyEnv: profile.keyBinding.credentialRef,
|
|
341
391
|
baseURL: profile.provider.baseURL,
|
|
342
392
|
allowInsecureDevelopment: profile.allowInsecureDevelopment,
|
|
343
393
|
...(profile.insecureDevelopmentOrigin === undefined ? {} : { insecureDevelopmentOrigin: profile.insecureDevelopmentOrigin }),
|
|
344
394
|
reasoning: profile.provider.reasoning,
|
|
345
|
-
defaultContextWindow: profile.provider.defaultContextWindow,
|
|
346
|
-
defaultMaxTokens: profile.provider.defaultMaxTokens,
|
|
347
|
-
maxRequestImageBytes: profile.provider.maxRequestImageBytes,
|
|
348
|
-
requestImagePixelBudget: profile.provider.requestImagePixelBudget,
|
|
349
|
-
requestImageMaxBytes: profile.provider.requestImageMaxBytes,
|
|
350
|
-
streamIdleTimeoutMs: profile.provider.streamIdleTimeoutMs,
|
|
351
|
-
retryPolicy: profile.provider.retryPolicy,
|
|
352
|
-
compat: profile.provider.compat,
|
|
353
|
-
models: profile.provider.models
|
|
395
|
+
...(profile.provider.defaultContextWindow === undefined ? {} : { defaultContextWindow: profile.provider.defaultContextWindow }),
|
|
396
|
+
...(profile.provider.defaultMaxTokens === undefined ? {} : { defaultMaxTokens: profile.provider.defaultMaxTokens }),
|
|
397
|
+
...(profile.provider.maxRequestImageBytes === undefined ? {} : { maxRequestImageBytes: profile.provider.maxRequestImageBytes }),
|
|
398
|
+
...(profile.provider.requestImagePixelBudget === undefined ? {} : { requestImagePixelBudget: profile.provider.requestImagePixelBudget }),
|
|
399
|
+
...(profile.provider.requestImageMaxBytes === undefined ? {} : { requestImageMaxBytes: profile.provider.requestImageMaxBytes }),
|
|
400
|
+
...(profile.provider.streamIdleTimeoutMs === undefined ? {} : { streamIdleTimeoutMs: profile.provider.streamIdleTimeoutMs }),
|
|
401
|
+
...(profile.provider.retryPolicy === undefined ? {} : { retryPolicy: structuredClone(profile.provider.retryPolicy) }),
|
|
402
|
+
...(profile.provider.compat === undefined ? {} : { compat: { ...profile.provider.compat } }),
|
|
403
|
+
models: profile.provider.models.map(model => ({
|
|
404
|
+
...model,
|
|
405
|
+
input: [...model.input],
|
|
406
|
+
...(model.reasoningEfforts && typeof model.reasoningEfforts === 'object'
|
|
407
|
+
? { reasoningEfforts: { ...model.reasoningEfforts } }
|
|
408
|
+
: {}),
|
|
409
|
+
...(model.compat === undefined ? {} : { compat: { ...model.compat } }),
|
|
410
|
+
})),
|
|
354
411
|
}])),
|
|
355
412
|
}
|
|
356
413
|
}
|
|
@@ -361,11 +418,11 @@ export function publicProfile(profile) {
|
|
|
361
418
|
displayName: profile.displayName,
|
|
362
419
|
organization: profile.organization,
|
|
363
420
|
brand: profile.brand,
|
|
364
|
-
credentialRef: profile.keyBinding
|
|
365
|
-
provider: {
|
|
421
|
+
credentialRef: profile.keyBinding?.credentialRef ?? '',
|
|
422
|
+
...(profile.provider ? { provider: {
|
|
366
423
|
id: profile.provider.id,
|
|
367
424
|
displayName: profile.provider.displayName,
|
|
368
425
|
models: profile.provider.models.map(model => ({ id: model.id, name: model.name, input: model.input })),
|
|
369
|
-
},
|
|
426
|
+
} } : {}),
|
|
370
427
|
}
|
|
371
428
|
}
|
package/lib/provider/core.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
const modalities = new Set(['text', 'image'])
|
|
2
|
+
export const REASONING_EFFORT_ORDER = Object.freeze(['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'])
|
|
3
|
+
const reasoningEfforts = new Set(REASONING_EFFORT_ORDER)
|
|
2
4
|
export const ENTERPRISE_SETTINGS_NAMESPACE = 'provider-enterprise'
|
|
3
5
|
export const ENTERPRISE_DEFAULT_MAX_REQUEST_IMAGE_BYTES = 20 * 1024 * 1024
|
|
4
6
|
export const ENTERPRISE_DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET = 2048 * 2048
|
|
@@ -50,7 +52,43 @@ function normalizeThinkingMap(value, reasoning) {
|
|
|
50
52
|
if (value === false || reasoning === false) return { off: null }
|
|
51
53
|
const source = value ?? { off: null, high: 'high', max: 'max' }
|
|
52
54
|
if (typeof source !== 'object' || source === null || Array.isArray(source)) throw new Error('reasoningEfforts must be an object or false')
|
|
53
|
-
|
|
55
|
+
const entries = Object.entries(source)
|
|
56
|
+
if (entries.length === 0 || !entries.some(([effort]) => effort !== 'off')) {
|
|
57
|
+
throw new Error('reasoningEfforts must declare at least one thinking level')
|
|
58
|
+
}
|
|
59
|
+
for (const [effort, wire] of entries) {
|
|
60
|
+
if (!reasoningEfforts.has(effort)) throw new Error(`reasoningEfforts level ${effort} is not supported`)
|
|
61
|
+
if (wire === null && effort !== 'off') throw new Error(`reasoningEfforts.${effort} may be null only for off`)
|
|
62
|
+
if (wire !== null && (typeof wire !== 'string' || wire.length === 0)) throw new Error(`reasoningEfforts.${effort} must be a non-empty string`)
|
|
63
|
+
}
|
|
64
|
+
const map = {}
|
|
65
|
+
for (const effort of REASONING_EFFORT_ORDER) {
|
|
66
|
+
const wire = source[effort]
|
|
67
|
+
if (wire === undefined) map[effort] = null
|
|
68
|
+
else if (wire !== null) map[effort] = wire
|
|
69
|
+
}
|
|
70
|
+
return map
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function declaredReasoningEfforts(value, reasoning) {
|
|
74
|
+
if (reasoning === false || value === false) return []
|
|
75
|
+
const source = value ?? { off: null, high: 'high', max: 'max' }
|
|
76
|
+
return REASONING_EFFORT_ORDER.filter(effort => Object.hasOwn(source, effort))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function reasoningPreference(value, label) {
|
|
80
|
+
const preference = value ?? 'high'
|
|
81
|
+
if (!reasoningEfforts.has(preference)) throw new Error(`${label} is not a supported reasoning effort`)
|
|
82
|
+
return preference
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function nearestReasoningEffort(preference, supported) {
|
|
86
|
+
const preferredRank = REASONING_EFFORT_ORDER.indexOf(preference)
|
|
87
|
+
return [...supported].sort((left, right) => {
|
|
88
|
+
const leftRank = REASONING_EFFORT_ORDER.indexOf(left)
|
|
89
|
+
const rightRank = REASONING_EFFORT_ORDER.indexOf(right)
|
|
90
|
+
return Math.abs(leftRank - preferredRank) - Math.abs(rightRank - preferredRank) || leftRank - rightRank
|
|
91
|
+
})[0]
|
|
54
92
|
}
|
|
55
93
|
|
|
56
94
|
export function resolveEnterpriseProfiles(raw = {}) {
|
|
@@ -69,6 +107,7 @@ export function resolveEnterpriseProfiles(raw = {}) {
|
|
|
69
107
|
const maxRequestImageBytes = positiveInteger(source.maxRequestImageBytes, ENTERPRISE_DEFAULT_MAX_REQUEST_IMAGE_BYTES, `${route}.maxRequestImageBytes`)
|
|
70
108
|
const requestImagePixelBudget = positiveInteger(source.requestImagePixelBudget, ENTERPRISE_DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET, `${route}.requestImagePixelBudget`)
|
|
71
109
|
const requestImageMaxBytes = positiveInteger(source.requestImageMaxBytes, ENTERPRISE_DEFAULT_REQUEST_IMAGE_MAX_BYTES, `${route}.requestImageMaxBytes`)
|
|
110
|
+
const providerReasoning = reasoningPreference(source.reasoning, `${route}.reasoning`)
|
|
72
111
|
if (!Array.isArray(source.models) || source.models.length === 0) throw new Error(`${route}.models must not be empty`)
|
|
73
112
|
|
|
74
113
|
const ids = new Set()
|
|
@@ -78,6 +117,35 @@ export function resolveEnterpriseProfiles(raw = {}) {
|
|
|
78
117
|
if (ids.has(id)) throw new Error(`${route} declares duplicate model ${id}`)
|
|
79
118
|
ids.add(id)
|
|
80
119
|
const reasoning = model.reasoning !== false && model.reasoningEfforts !== false
|
|
120
|
+
const compat = {
|
|
121
|
+
supportsDeveloperRole: false,
|
|
122
|
+
supportsStore: false,
|
|
123
|
+
supportsReasoningEffort: reasoning,
|
|
124
|
+
supportsUsageInStreaming: true,
|
|
125
|
+
maxTokensField: 'max_tokens',
|
|
126
|
+
supportsStrictMode: false,
|
|
127
|
+
supportsLongCacheRetention: false,
|
|
128
|
+
thinkingFormat: 'deepseek',
|
|
129
|
+
requiresReasoningContentOnAssistantMessages: true,
|
|
130
|
+
...(source.compat ?? {}),
|
|
131
|
+
...(model.compat ?? {}),
|
|
132
|
+
}
|
|
133
|
+
const supportsReasoningEffort = reasoning && compat.supportsReasoningEffort !== false
|
|
134
|
+
const efforts = supportsReasoningEffort ? declaredReasoningEfforts(model.reasoningEfforts, reasoning) : []
|
|
135
|
+
if (model.defaultReasoningEffort !== undefined && !reasoningEfforts.has(model.defaultReasoningEffort)) {
|
|
136
|
+
throw new Error(`${route}.${id}.defaultReasoningEffort is not a supported reasoning effort`)
|
|
137
|
+
}
|
|
138
|
+
if (model.defaultReasoningEffort !== undefined && !efforts.includes(model.defaultReasoningEffort)) {
|
|
139
|
+
throw new Error(`${route}.${id}.defaultReasoningEffort must be one of the model's declared reasoningEfforts`)
|
|
140
|
+
}
|
|
141
|
+
const defaultReasoningEffort = supportsReasoningEffort
|
|
142
|
+
? (model.defaultReasoningEffort ?? nearestReasoningEffort(providerReasoning, efforts))
|
|
143
|
+
: undefined
|
|
144
|
+
// PiAi uses an internal effort to enable thinking even when the gateway
|
|
145
|
+
// accepts no reasoning_effort field. Keep that default separate from UI choices.
|
|
146
|
+
const internalThinkingEffort = reasoning && !supportsReasoningEffort
|
|
147
|
+
? nearestReasoningEffort(providerReasoning, declaredReasoningEfforts(model.reasoningEfforts, reasoning))
|
|
148
|
+
: undefined
|
|
81
149
|
return {
|
|
82
150
|
id,
|
|
83
151
|
name: nonEmpty(model.name ?? id, `${route}.${id}.name`),
|
|
@@ -90,18 +158,10 @@ export function resolveEnterpriseProfiles(raw = {}) {
|
|
|
90
158
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
91
159
|
contextWindow: positiveInteger(model.contextWindow, contextWindow, `${route}.${id}.contextWindow`),
|
|
92
160
|
maxTokens: positiveInteger(model.maxTokens, maxTokens, `${route}.${id}.maxTokens`),
|
|
93
|
-
compat
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
supportsUsageInStreaming: true,
|
|
98
|
-
maxTokensField: 'max_tokens',
|
|
99
|
-
supportsStrictMode: false,
|
|
100
|
-
supportsLongCacheRetention: false,
|
|
101
|
-
thinkingFormat: 'deepseek',
|
|
102
|
-
requiresReasoningContentOnAssistantMessages: true,
|
|
103
|
-
...(source.compat ?? {}),
|
|
104
|
-
...(model.compat ?? {}),
|
|
161
|
+
compat,
|
|
162
|
+
modelPolicy: {
|
|
163
|
+
reasoning, supportsReasoningEffort, reasoningEfforts: efforts, defaultReasoningEffort,
|
|
164
|
+
...(internalThinkingEffort === undefined ? {} : { internalThinkingEffort }),
|
|
105
165
|
},
|
|
106
166
|
}
|
|
107
167
|
})
|
|
@@ -111,14 +171,15 @@ export function resolveEnterpriseProfiles(raw = {}) {
|
|
|
111
171
|
displayName,
|
|
112
172
|
baseURL,
|
|
113
173
|
credentialRef,
|
|
114
|
-
reasoning:
|
|
174
|
+
reasoning: providerReasoning,
|
|
115
175
|
maxRequestImageBytes,
|
|
116
176
|
requestImagePixelBudget,
|
|
117
177
|
requestImageMaxBytes,
|
|
118
178
|
streamIdleTimeoutMs: positiveInteger(source.streamIdleTimeoutMs, 300000, `${route}.streamIdleTimeoutMs`),
|
|
119
179
|
retryPolicy: source.retryPolicy ?? ENTERPRISE_DEFAULT_RETRY_POLICY,
|
|
120
180
|
configuredMaxTokens: new Map(models.map(model => [model.id, model.maxTokens])),
|
|
121
|
-
models,
|
|
181
|
+
modelPolicies: new Map(models.map(model => [model.id, model.modelPolicy])),
|
|
182
|
+
models: models.map(({ modelPolicy: _modelPolicy, ...model }) => model),
|
|
122
183
|
}
|
|
123
184
|
})
|
|
124
185
|
}
|
package/lib/provider/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment'
|
|
2
|
-
import { LlmError, assertUsableApiKey, resolveRetryPolicy } from '@deepseek-ai/dsh-llm'
|
|
2
|
+
import { LlmError, assertUsableApiKey, resolveImageAttachmentAccess, resolveRetryPolicy } from '@deepseek-ai/dsh-llm'
|
|
3
3
|
import { Config as PiAiConfig, PiAiAdapter } from '@deepseek-ai/dsh-llm-pi-ai'
|
|
4
4
|
import { createProvider } from '@earendil-works/pi-ai'
|
|
5
5
|
import { openAICompletionsApi } from '@earendil-works/pi-ai/api/openai-completions.lazy'
|
|
@@ -22,7 +22,13 @@ function installEnterpriseSettings(ctx, rawConfig, hooks) {
|
|
|
22
22
|
SETTINGS_NAMESPACE,
|
|
23
23
|
PiAiConfig,
|
|
24
24
|
settingsBase(rawConfig),
|
|
25
|
-
|
|
25
|
+
{
|
|
26
|
+
...hooks,
|
|
27
|
+
validate(value) {
|
|
28
|
+
assertServiceableEnterpriseProviders(value)
|
|
29
|
+
hooks.validate?.(value)
|
|
30
|
+
},
|
|
31
|
+
},
|
|
26
32
|
)
|
|
27
33
|
})
|
|
28
34
|
}
|
|
@@ -37,24 +43,24 @@ function apiKeyAuth(displayName) {
|
|
|
37
43
|
}
|
|
38
44
|
}
|
|
39
45
|
|
|
40
|
-
function profilesFrom(rawConfig) {
|
|
46
|
+
export function profilesFrom(rawConfig) {
|
|
41
47
|
const profiles = new Map()
|
|
42
48
|
for (const route of resolveEnterpriseProfiles(rawConfig)) {
|
|
43
49
|
profiles.set(route.provider, {
|
|
44
50
|
provider: route.provider,
|
|
45
51
|
displayName: route.displayName,
|
|
46
52
|
apiKeyEnv: route.credentialRef,
|
|
47
|
-
reasoning: route.reasoning,
|
|
48
53
|
maxRequestImageBytes: route.maxRequestImageBytes,
|
|
49
54
|
requestImagePixelBudget: route.requestImagePixelBudget,
|
|
50
55
|
requestImageMaxBytes: route.requestImageMaxBytes,
|
|
51
56
|
streamIdleTimeoutMs: route.streamIdleTimeoutMs,
|
|
52
57
|
retryPolicy: resolveRetryPolicy(route.retryPolicy, `${name}: ${route.provider}.retryPolicy`),
|
|
53
58
|
configuredMaxTokens: route.configuredMaxTokens,
|
|
54
|
-
modelPolicies:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
modelPolicies: route.modelPolicies,
|
|
60
|
+
// DSH 0.1.5-rc.1 reads per-model configuration errors before resolution.
|
|
61
|
+
// Our declarative profiles validate eagerly, so a successfully constructed
|
|
62
|
+
// provider has no deferred errors. Earlier adapters ignore this field.
|
|
63
|
+
modelErrors: new Map(),
|
|
58
64
|
piProvider: createProvider({
|
|
59
65
|
id: route.provider,
|
|
60
66
|
name: route.displayName,
|
|
@@ -68,6 +74,10 @@ function profilesFrom(rawConfig) {
|
|
|
68
74
|
return profiles
|
|
69
75
|
}
|
|
70
76
|
|
|
77
|
+
export function assertServiceableEnterpriseProviders(rawConfig) {
|
|
78
|
+
profilesFrom(rawConfig)
|
|
79
|
+
}
|
|
80
|
+
|
|
71
81
|
function isolatedPiAiAuth() {
|
|
72
82
|
const stored = new Map()
|
|
73
83
|
return {
|
|
@@ -89,50 +99,71 @@ function isolatedPiAiAuth() {
|
|
|
89
99
|
}
|
|
90
100
|
}
|
|
91
101
|
|
|
102
|
+
export function resolveEnterpriseImageAccess(ctx, attachments, ref) {
|
|
103
|
+
return resolveImageAttachmentAccess(
|
|
104
|
+
attachments,
|
|
105
|
+
hostPath => ctx.get('fs')?.processPathFromHostPath(hostPath),
|
|
106
|
+
ref,
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
92
110
|
export function apply(ctx, rawConfig = {}) {
|
|
93
111
|
const transforms = new EnterpriseModelTransforms(ctx)
|
|
94
112
|
let current = () => settingsBase(rawConfig)
|
|
95
113
|
let profiles = profilesFrom(current())
|
|
96
|
-
if (profiles.size === 0) return
|
|
97
114
|
|
|
98
115
|
const baseAdapter = new PiAiAdapter({
|
|
99
116
|
profiles: () => profiles,
|
|
100
117
|
auth: isolatedPiAiAuth(),
|
|
101
118
|
resolveApiKey: async (provider, profile) => {
|
|
119
|
+
if (await rawConfig.beforeResolveCredential?.(provider) === false) {
|
|
120
|
+
throw new LlmError(`${name}: organization sign-in is required for provider route "${provider}"`, 'MISSING_CREDENTIAL')
|
|
121
|
+
}
|
|
102
122
|
const ref = profile.apiKeyEnv
|
|
103
123
|
const credentials = ctx.get('credentials')
|
|
104
|
-
const hit =
|
|
124
|
+
const hit = typeof rawConfig.resolveCredential === 'function'
|
|
125
|
+
? (await rawConfig.resolveCredential(provider))?.value
|
|
126
|
+
: credentials === undefined
|
|
105
127
|
? launchEnvironmentOf(ctx).get(ref)?.value
|
|
106
128
|
: (await credentials.resolve(ref))?.value
|
|
107
129
|
if (typeof hit === 'string' && hit.length > 0) return assertUsableApiKey(hit, name, ref)
|
|
108
130
|
throw new LlmError(`${name}: no credential for provider route "${provider}" (${ref})`, 'MISSING_CREDENTIAL')
|
|
109
131
|
},
|
|
110
132
|
resolveAttachments: () => ctx.get('attachments'),
|
|
133
|
+
resolveImageAccess: (attachments, ref) => resolveEnterpriseImageAccess(ctx, attachments, ref),
|
|
111
134
|
})
|
|
112
135
|
|
|
113
136
|
const adapter = new TransformingEnterpriseAdapter(baseAdapter, transforms, (provider, model) => (
|
|
114
137
|
profiles.get(provider)?.modelPolicies.get(model) ?? { reasoning: true, supportsReasoningEffort: true }
|
|
115
138
|
))
|
|
116
|
-
|
|
117
|
-
let directory
|
|
118
|
-
|
|
119
|
-
if (rawConfig.settingsEnabled !== true) {
|
|
120
|
-
installEnterpriseSettings(ctx, rawConfig, { setSource() {}, onChange() {} })
|
|
121
|
-
return
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const refresh = () => {
|
|
125
|
-
const next = profilesFrom(current())
|
|
139
|
+
let registration
|
|
140
|
+
let directory
|
|
141
|
+
const replaceProfiles = next => {
|
|
126
142
|
const previous = profiles
|
|
127
143
|
profiles = next
|
|
128
144
|
try {
|
|
129
|
-
registration.replace([...next.keys()])
|
|
130
|
-
|
|
145
|
+
if (registration) registration.replace([...next.keys()])
|
|
146
|
+
else if (next.size) registration = ctx.llm.registerAdapter([...next.keys()], adapter)
|
|
147
|
+
if (directory) directory.replace(configurableEntries(next))
|
|
148
|
+
else if (next.size) directory = ctx.llm.registerConfigurableProviders(configurableEntries(next))
|
|
131
149
|
} catch (error) {
|
|
132
150
|
profiles = previous
|
|
151
|
+
registration?.replace([...previous.keys()])
|
|
133
152
|
throw error
|
|
134
153
|
}
|
|
135
154
|
}
|
|
155
|
+
if (profiles.size) replaceProfiles(profiles)
|
|
156
|
+
const controller = { replaceConfig(config) { replaceProfiles(profilesFrom(config)) } }
|
|
157
|
+
|
|
158
|
+
if (rawConfig.settingsEnabled !== true) {
|
|
159
|
+
installEnterpriseSettings(ctx, rawConfig, { setSource() {}, onChange() {} })
|
|
160
|
+
return controller
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const refresh = () => {
|
|
164
|
+
const next = profilesFrom(current())
|
|
165
|
+
replaceProfiles(next)
|
|
166
|
+
}
|
|
136
167
|
|
|
137
168
|
installEnterpriseSettings(ctx, rawConfig, {
|
|
138
169
|
setSource(source) { current = source },
|
|
@@ -144,6 +175,7 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
144
175
|
}
|
|
145
176
|
},
|
|
146
177
|
})
|
|
178
|
+
return controller
|
|
147
179
|
}
|
|
148
180
|
|
|
149
181
|
export { resolveEnterpriseProfiles } from './core.js'
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { LlmError } from '@deepseek-ai/dsh-llm'
|
|
2
|
+
|
|
1
3
|
/** Delegate one enterprise route while applying capabilities contributed by replaceable feature plugins. */
|
|
2
4
|
export class TransformingEnterpriseAdapter {
|
|
3
5
|
constructor(inner, transforms, modelPolicy = () => ({})) {
|
|
@@ -8,6 +10,7 @@ export class TransformingEnterpriseAdapter {
|
|
|
8
10
|
|
|
9
11
|
providerInfo(provider) { return this.inner.providerInfo(provider) }
|
|
10
12
|
providerRetryPolicy(provider) { return this.inner.providerRetryPolicy(provider) }
|
|
13
|
+
imageRequestPricing(provider, model) { return this.inner.imageRequestPricing(provider, model) }
|
|
11
14
|
|
|
12
15
|
async listModels(provider) {
|
|
13
16
|
const models = await this.inner.listModels(provider)
|
|
@@ -18,48 +21,66 @@ export class TransformingEnterpriseAdapter {
|
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
async resolveModel(provider, model, signal) {
|
|
21
|
-
|
|
24
|
+
const policy = this.modelPolicy(provider, model)
|
|
25
|
+
return this.decorateModel(provider, model, await this.inner.resolveModel(provider, model, signal), policy)
|
|
22
26
|
}
|
|
23
27
|
|
|
24
|
-
decorateModel(provider, model, resolved) {
|
|
28
|
+
decorateModel(provider, model, resolved, policy = this.modelPolicy(provider, model)) {
|
|
25
29
|
const decorated = {
|
|
26
30
|
...resolved,
|
|
27
31
|
inputModalities: this.transforms.capabilities(provider, model, resolved.inputModalities),
|
|
28
32
|
}
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
if (policy.reasoning !== false && policy.supportsReasoningEffort !== false && Array.isArray(policy.reasoningEfforts)) {
|
|
34
|
+
return {
|
|
35
|
+
...decorated,
|
|
36
|
+
reasoning: {
|
|
37
|
+
efforts: policy.reasoningEfforts.map(id => ({ id, name: `${id.charAt(0).toUpperCase()}${id.slice(1)}` })),
|
|
38
|
+
defaultEffort: policy.defaultReasoningEffort,
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
}
|
|
31
42
|
const { reasoning: _unsupportedReasoningEfforts, ...thinkingOnly } = decorated
|
|
32
43
|
return thinkingOnly
|
|
33
44
|
}
|
|
34
45
|
|
|
35
46
|
async prepareCall(provider, model, signal) {
|
|
47
|
+
const policy = this.modelPolicy(provider, model)
|
|
36
48
|
const prepared = typeof this.inner.prepareCall === 'function'
|
|
37
49
|
? await this.inner.prepareCall(provider, model, signal)
|
|
38
50
|
: { model: await this.inner.resolveModel(provider, model, signal), stream: options => this.inner.stream(options) }
|
|
39
51
|
return {
|
|
40
|
-
model: this.decorateModel(provider, model, prepared.model),
|
|
41
|
-
stream: options => this.streamPrepared(options, prepared),
|
|
52
|
+
model: this.decorateModel(provider, model, prepared.model, policy),
|
|
53
|
+
stream: options => this.streamPrepared(options, prepared, policy),
|
|
42
54
|
}
|
|
43
55
|
}
|
|
44
56
|
|
|
45
|
-
async * streamPrepared(options, prepared) {
|
|
57
|
+
async * streamPrepared(options, prepared, policy = this.modelPolicy(options.provider, options.model)) {
|
|
46
58
|
const nativeModelInfo = prepared.model
|
|
47
59
|
let request = options
|
|
48
|
-
const policy = this.modelPolicy(options.provider, options.model)
|
|
49
60
|
if (policy.reasoning === false) {
|
|
50
61
|
request = { ...options, reasoningEffort: 'off' }
|
|
51
62
|
} else if (policy.supportsReasoningEffort === false) {
|
|
52
63
|
const { reasoningEffort: _unsupportedReasoningEffort, ...withoutEffort } = options
|
|
53
|
-
|
|
64
|
+
// This enables the model's configured thinking mode inside PiAi;
|
|
65
|
+
// compat.supportsReasoningEffort=false suppresses the HTTP effort field.
|
|
66
|
+
request = policy.internalThinkingEffort === undefined
|
|
67
|
+
? withoutEffort
|
|
68
|
+
: { ...withoutEffort, reasoningEffort: policy.internalThinkingEffort }
|
|
69
|
+
} else if (Array.isArray(policy.reasoningEfforts)) {
|
|
70
|
+
const effort = options.reasoningEffort ?? policy.defaultReasoningEffort
|
|
71
|
+
if (!policy.reasoningEfforts.includes(effort)) {
|
|
72
|
+
throw new LlmError(`enterprise model "${options.provider}/${options.model}" does not support reasoning effort "${String(effort)}"`, 'UNSUPPORTED_REASONING_EFFORT')
|
|
73
|
+
}
|
|
74
|
+
request = { ...options, reasoningEffort: effort }
|
|
54
75
|
}
|
|
55
76
|
yield* prepared.stream(await this.transforms.apply(request, nativeModelInfo))
|
|
56
77
|
}
|
|
57
78
|
|
|
58
79
|
async * stream(options) {
|
|
80
|
+
const policy = this.modelPolicy(options.provider, options.model)
|
|
59
81
|
const prepared = typeof this.inner.prepareCall === 'function'
|
|
60
82
|
? await this.inner.prepareCall(options.provider, options.model, options.signal)
|
|
61
83
|
: { model: await this.inner.resolveModel(options.provider, options.model, options.signal), stream: request => this.inner.stream(request) }
|
|
62
|
-
yield* this.streamPrepared(options, prepared)
|
|
84
|
+
yield* this.streamPrepared(options, prepared, policy)
|
|
63
85
|
}
|
|
64
86
|
}
|
|
65
|
-
|