@eduwork/dsh-oidc 0.1.0-alpha.11
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 +120 -0
- package/CHANGELOG.md +120 -0
- package/LICENSE +21 -0
- package/README.en.md +184 -0
- package/README.md +143 -0
- package/THIRD_PARTY_NOTICES.en.md +51 -0
- package/THIRD_PARTY_NOTICES.md +53 -0
- package/cordis.patch.yml +8 -0
- package/docs/EDUWORK-MIGRATION.en.md +23 -0
- package/docs/EDUWORK-MIGRATION.md +23 -0
- package/docs/architecture.en.md +125 -0
- package/docs/architecture.md +125 -0
- package/docs/compatibility.en.md +61 -0
- package/docs/compatibility.md +61 -0
- package/docs/decisions/0001-one-plugin-composition-boundary.en.md +34 -0
- package/docs/decisions/0001-one-plugin-composition-boundary.md +34 -0
- package/docs/dsh-integration.en.md +171 -0
- package/docs/dsh-integration.md +171 -0
- package/docs/ecnu-reference.en.md +66 -0
- package/docs/ecnu-reference.md +66 -0
- package/docs/enterprise-profile.en.md +159 -0
- package/docs/enterprise-profile.md +159 -0
- package/docs/getting-started.en.md +137 -0
- package/docs/getting-started.md +239 -0
- package/docs/key-binding-protocol.en.md +227 -0
- package/docs/key-binding-protocol.md +227 -0
- package/docs/oidc-interoperability.en.md +100 -0
- package/docs/oidc-interoperability.md +100 -0
- package/docs/release-checklist.en.md +60 -0
- package/docs/release-checklist.md +60 -0
- package/docs/security-model.en.md +138 -0
- package/docs/security-model.md +138 -0
- package/docs/server-integration-contract.en.md +706 -0
- package/docs/server-integration-contract.md +706 -0
- package/examples/ecnu.enterprise-profile.example.json +81 -0
- package/examples/enterprise-profile.example.json +77 -0
- package/lib/client.js +5855 -0
- package/lib/index.js +59 -0
- package/lib/oidc.js +649 -0
- package/lib/profile.js +371 -0
- package/lib/provider/core.js +138 -0
- package/lib/provider/index.js +156 -0
- package/lib/provider/transform-adapter.js +65 -0
- package/lib/provider/transforms.js +59 -0
- package/lib/typert-schemas.js +84 -0
- package/lib/typert.host.js +38 -0
- package/lib/typert.remote-client.js +37 -0
- package/package.json +120 -0
- package/protocol/openapi.yaml +257 -0
- package/schema/enterprise-profile.v1alpha1.schema.json +174 -0
package/lib/profile.js
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
|
|
3
|
+
export const PROFILE_SCHEMA_VERSION = 'dsh-oidc/v1alpha1'
|
|
4
|
+
const MAX_PROFILE_BYTES = 512 * 1024
|
|
5
|
+
const idPattern = /^[a-z][a-z0-9-]{0,63}$/
|
|
6
|
+
const credentialPattern = /^[A-Za-z_][A-Za-z0-9_]*$/
|
|
7
|
+
const colorPattern = /^#[0-9a-fA-F]{6}$/
|
|
8
|
+
const allowedModalities = new Set(['text', 'image'])
|
|
9
|
+
const allowedRootKeys = new Set([
|
|
10
|
+
'schemaVersion', 'id', 'displayName', 'organization', 'nativeInstitutionID',
|
|
11
|
+
'allowInsecureDevelopment', 'insecureDevelopmentOrigin', 'brand', 'oidc', 'keyBinding', 'provider',
|
|
12
|
+
])
|
|
13
|
+
const allowedOidcKeys = new Set(['issuer', 'clientId', 'scopes'])
|
|
14
|
+
const allowedKeyBindingKeys = new Set(['baseURL', 'credentialRef'])
|
|
15
|
+
const allowedBrandKeys = new Set([
|
|
16
|
+
'productName', 'organizationName', 'mark', 'logoURL', 'primaryColor',
|
|
17
|
+
'loginTitle', 'loginDescription', 'supportURL',
|
|
18
|
+
])
|
|
19
|
+
const allowedProviderKeys = new Set([
|
|
20
|
+
'id', 'displayName', 'adapter', 'baseURL', 'reasoning', 'defaultContextWindow',
|
|
21
|
+
'defaultMaxTokens', 'maxRequestImageBytes', 'requestImagePixelBudget',
|
|
22
|
+
'requestImageMaxBytes', 'streamIdleTimeoutMs', 'retryPolicy', 'compat', 'models',
|
|
23
|
+
])
|
|
24
|
+
const allowedModelKeys = new Set([
|
|
25
|
+
'id', 'name', 'input', 'contextWindow', 'maxTokens', 'reasoning',
|
|
26
|
+
'reasoningEfforts', 'compat',
|
|
27
|
+
])
|
|
28
|
+
const allowedCompatKeys = new Set([
|
|
29
|
+
'supportsDeveloperRole', 'supportsStore', 'supportsReasoningEffort',
|
|
30
|
+
'supportsUsageInStreaming', 'maxTokensField', 'supportsStrictMode',
|
|
31
|
+
'supportsLongCacheRetention', 'thinkingFormat',
|
|
32
|
+
'requiresReasoningContentOnAssistantMessages',
|
|
33
|
+
])
|
|
34
|
+
|
|
35
|
+
function object(value, label) {
|
|
36
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) throw new Error(`${label} must be an object`)
|
|
37
|
+
return value
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function exactKeys(value, allowed, label) {
|
|
41
|
+
for (const key of Object.keys(value)) if (!allowed.has(key)) throw new Error(`${label}.${key} is not allowed`)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function text(value, label, max = 2048) {
|
|
45
|
+
if (typeof value !== 'string' || value.trim() === '' || value !== value.trim() || value.length > max) {
|
|
46
|
+
throw new Error(`${label} must be a non-empty trimmed string no longer than ${max} characters`)
|
|
47
|
+
}
|
|
48
|
+
return value
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isLoopback(hostname) {
|
|
52
|
+
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function insecureDevelopmentOrigin(value, label) {
|
|
56
|
+
const raw = text(value, label)
|
|
57
|
+
const parsed = new URL(raw)
|
|
58
|
+
if (parsed.protocol !== 'http:' || parsed.username || parsed.password || parsed.search || parsed.hash || (parsed.pathname !== '/' && parsed.pathname !== '')) {
|
|
59
|
+
throw new Error(`${label} must be an exact HTTP origin without credentials, path, query, or fragment`)
|
|
60
|
+
}
|
|
61
|
+
return raw.replace(/\/+$/, '')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function endpointProtocolAllowed(parsed, allowInsecure, allowedInsecureOrigin) {
|
|
65
|
+
return parsed.protocol === 'https:' || (allowInsecure && parsed.protocol === 'http:' && (
|
|
66
|
+
isLoopback(parsed.hostname) || parsed.origin === allowedInsecureOrigin
|
|
67
|
+
))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function issuerURL(value, label, allowInsecure = false, allowedInsecureOrigin) {
|
|
71
|
+
const raw = text(value, label)
|
|
72
|
+
const parsed = new URL(raw)
|
|
73
|
+
const permitted = endpointProtocolAllowed(parsed, allowInsecure, allowedInsecureOrigin)
|
|
74
|
+
if (!permitted || parsed.username || parsed.password || parsed.search || parsed.hash) {
|
|
75
|
+
throw new Error(`${label} must be an HTTPS issuer URL without credentials, query, or fragment${allowInsecure ? ' (loopback or the exact allowlisted HTTP origin is allowed in development)' : ''}`)
|
|
76
|
+
}
|
|
77
|
+
return raw
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function exactURL(value, label, allowInsecure = false, allowedInsecureOrigin) {
|
|
81
|
+
const raw = text(value, label)
|
|
82
|
+
const parsed = new URL(raw)
|
|
83
|
+
const permitted = endpointProtocolAllowed(parsed, allowInsecure, allowedInsecureOrigin)
|
|
84
|
+
if (!permitted || parsed.username || parsed.password || parsed.hash || parsed.search) {
|
|
85
|
+
throw new Error(`${label} must be an absolute HTTPS URL without credentials, query, or fragment${allowInsecure ? ' (loopback or the exact allowlisted HTTP origin is allowed in development)' : ''}`)
|
|
86
|
+
}
|
|
87
|
+
return raw.replace(/\/+$/, '')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function safeHTTPSResource(value, label, max) {
|
|
91
|
+
const raw = text(value, label, max)
|
|
92
|
+
const parsed = new URL(raw)
|
|
93
|
+
if (parsed.protocol !== 'https:' || parsed.username || parsed.password || parsed.hash) {
|
|
94
|
+
throw new Error(`${label} must be an HTTPS URL without credentials or fragment`)
|
|
95
|
+
}
|
|
96
|
+
return raw
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function positiveInteger(value, label) {
|
|
100
|
+
if (!Number.isSafeInteger(value) || value <= 0) throw new Error(`${label} must be a positive safe integer`)
|
|
101
|
+
return value
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function optionalPositiveInteger(value, label) {
|
|
105
|
+
return value === undefined ? undefined : positiveInteger(value, label)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function boolean(value, label) {
|
|
109
|
+
if (typeof value !== 'boolean') throw new Error(`${label} must be a boolean`)
|
|
110
|
+
return value
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function normalizeBrand(value = {}) {
|
|
114
|
+
const source = object(value, 'brand')
|
|
115
|
+
exactKeys(source, allowedBrandKeys, 'brand')
|
|
116
|
+
const result = {}
|
|
117
|
+
if (source.productName !== undefined) result.productName = text(source.productName, 'brand.productName', 80)
|
|
118
|
+
if (source.organizationName !== undefined) result.organizationName = text(source.organizationName, 'brand.organizationName', 120)
|
|
119
|
+
if (source.mark !== undefined) result.mark = text(source.mark, 'brand.mark', 4)
|
|
120
|
+
if (source.primaryColor !== undefined) {
|
|
121
|
+
if (typeof source.primaryColor !== 'string' || !colorPattern.test(source.primaryColor)) throw new Error('brand.primaryColor must be a six-digit hex color')
|
|
122
|
+
result.primaryColor = source.primaryColor.toLowerCase()
|
|
123
|
+
}
|
|
124
|
+
if (source.logoURL !== undefined) {
|
|
125
|
+
const logo = text(source.logoURL, 'brand.logoURL', 128 * 1024)
|
|
126
|
+
if (!/^https:\/\//.test(logo) && !/^data:image\/(?:png|webp);base64,[A-Za-z0-9+/]+=*$/.test(logo)) {
|
|
127
|
+
throw new Error('brand.logoURL must be HTTPS or a base64 PNG/WebP data URL')
|
|
128
|
+
}
|
|
129
|
+
result.logoURL = logo.startsWith('https://') ? safeHTTPSResource(logo, 'brand.logoURL', 128 * 1024) : logo
|
|
130
|
+
}
|
|
131
|
+
for (const key of ['loginTitle', 'loginDescription']) {
|
|
132
|
+
if (source[key] !== undefined) result[key] = text(source[key], `brand.${key}`, key === 'loginTitle' ? 120 : 500)
|
|
133
|
+
}
|
|
134
|
+
if (source.supportURL !== undefined) result.supportURL = exactURL(source.supportURL, 'brand.supportURL')
|
|
135
|
+
return Object.freeze(result)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function normalizeCompat(value, label) {
|
|
139
|
+
const source = object(value, label)
|
|
140
|
+
exactKeys(source, allowedCompatKeys, label)
|
|
141
|
+
const result = {}
|
|
142
|
+
for (const key of allowedCompatKeys) {
|
|
143
|
+
if (source[key] === undefined) continue
|
|
144
|
+
result[key] = key === 'maxTokensField' || key === 'thinkingFormat'
|
|
145
|
+
? text(source[key], `${label}.${key}`, 64)
|
|
146
|
+
: boolean(source[key], `${label}.${key}`)
|
|
147
|
+
}
|
|
148
|
+
return Object.freeze(result)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function normalizeReasoningEfforts(value, providerID, modelID) {
|
|
152
|
+
if (value === false) return false
|
|
153
|
+
const source = object(value, `${providerID}.${modelID}.reasoningEfforts`)
|
|
154
|
+
if (Object.keys(source).length === 0 || Object.keys(source).length > 16) throw new Error(`${providerID}.${modelID}.reasoningEfforts must contain 1-16 entries`)
|
|
155
|
+
return Object.freeze(Object.fromEntries(Object.entries(source).map(([level, wire]) => {
|
|
156
|
+
text(level, `${providerID}.${modelID}.reasoningEfforts level`, 32)
|
|
157
|
+
if (wire !== null) text(wire, `${providerID}.${modelID}.reasoningEfforts.${level}`, 32)
|
|
158
|
+
return [level, wire]
|
|
159
|
+
})))
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function normalizeModels(models, providerID) {
|
|
163
|
+
if (!Array.isArray(models) || models.length === 0 || models.length > 128) throw new Error(`${providerID}.models must contain 1-128 entries`)
|
|
164
|
+
const seen = new Set()
|
|
165
|
+
return Object.freeze(models.map((raw, index) => {
|
|
166
|
+
const label = `${providerID}.models[${index}]`
|
|
167
|
+
const model = object(raw, label)
|
|
168
|
+
exactKeys(model, allowedModelKeys, label)
|
|
169
|
+
const id = text(model.id, `${label}.id`, 256)
|
|
170
|
+
if (seen.has(id)) throw new Error(`${providerID} repeats model ${id}`)
|
|
171
|
+
seen.add(id)
|
|
172
|
+
const input = model.input ?? ['text']
|
|
173
|
+
if (!Array.isArray(input) || input.length === 0 || input.some(value => !allowedModalities.has(value))) {
|
|
174
|
+
throw new Error(`${providerID}.${id}.input must contain text and/or image`)
|
|
175
|
+
}
|
|
176
|
+
if (new Set(input).size !== input.length) throw new Error(`${providerID}.${id}.input contains duplicates`)
|
|
177
|
+
return Object.freeze({
|
|
178
|
+
id,
|
|
179
|
+
name: model.name === undefined ? id : text(model.name, `${providerID}.${id}.name`, 256),
|
|
180
|
+
input: Object.freeze([...new Set(input)]),
|
|
181
|
+
...(model.contextWindow === undefined ? {} : { contextWindow: positiveInteger(model.contextWindow, `${providerID}.${id}.contextWindow`) }),
|
|
182
|
+
...(model.maxTokens === undefined ? {} : { maxTokens: positiveInteger(model.maxTokens, `${providerID}.${id}.maxTokens`) }),
|
|
183
|
+
...(model.reasoning === undefined ? {} : { reasoning: boolean(model.reasoning, `${providerID}.${id}.reasoning`) }),
|
|
184
|
+
...(model.reasoningEfforts === undefined ? {} : { reasoningEfforts: normalizeReasoningEfforts(model.reasoningEfforts, providerID, id) }),
|
|
185
|
+
...(model.compat === undefined ? {} : { compat: normalizeCompat(model.compat, `${providerID}.${id}.compat`) }),
|
|
186
|
+
})
|
|
187
|
+
}))
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function providerCredentialRef(providerID) {
|
|
191
|
+
return `${providerID.toUpperCase().replace(/[^A-Z0-9]/g, '_')}_API_KEY`
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function normalizeRetryPolicy(value) {
|
|
195
|
+
const source = object(value, 'profile.provider.retryPolicy')
|
|
196
|
+
exactKeys(source, new Set(['mode', 'maxRetries', 'retryableCodes', 'backoff']), 'profile.provider.retryPolicy')
|
|
197
|
+
if (!['normal', 'always'].includes(source.mode)) throw new Error('profile.provider.retryPolicy.mode must be normal or always')
|
|
198
|
+
if (source.maxRetries !== undefined && (!Number.isSafeInteger(source.maxRetries) || source.maxRetries < 0)) {
|
|
199
|
+
throw new Error('profile.provider.retryPolicy.maxRetries must be a non-negative safe integer')
|
|
200
|
+
}
|
|
201
|
+
if (source.retryableCodes !== undefined) {
|
|
202
|
+
if (!Array.isArray(source.retryableCodes) || source.retryableCodes.length === 0 || source.retryableCodes.length > 64
|
|
203
|
+
|| source.retryableCodes.some(code => typeof code !== 'string' || code === '' || code.length > 128)
|
|
204
|
+
|| new Set(source.retryableCodes).size !== source.retryableCodes.length) {
|
|
205
|
+
throw new Error('profile.provider.retryPolicy.retryableCodes must contain 1-64 unique non-empty strings')
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
let backoff
|
|
209
|
+
if (source.backoff !== undefined) {
|
|
210
|
+
const raw = object(source.backoff, 'profile.provider.retryPolicy.backoff')
|
|
211
|
+
exactKeys(raw, new Set(['initialDelayMs', 'maxDelayMs', 'jitterRatio']), 'profile.provider.retryPolicy.backoff')
|
|
212
|
+
for (const key of ['initialDelayMs', 'maxDelayMs']) {
|
|
213
|
+
if (raw[key] !== undefined && (!Number.isFinite(raw[key]) || raw[key] <= 0 || raw[key] > 2147483647)) {
|
|
214
|
+
throw new Error(`profile.provider.retryPolicy.backoff.${key} is invalid`)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (raw.jitterRatio !== undefined && (!Number.isFinite(raw.jitterRatio) || raw.jitterRatio < 0 || raw.jitterRatio > 1)) {
|
|
218
|
+
throw new Error('profile.provider.retryPolicy.backoff.jitterRatio must be between 0 and 1')
|
|
219
|
+
}
|
|
220
|
+
if (raw.initialDelayMs !== undefined && raw.maxDelayMs !== undefined && raw.initialDelayMs > raw.maxDelayMs) {
|
|
221
|
+
throw new Error('profile.provider.retryPolicy.backoff.initialDelayMs must not exceed maxDelayMs')
|
|
222
|
+
}
|
|
223
|
+
backoff = Object.freeze({ ...raw })
|
|
224
|
+
}
|
|
225
|
+
return Object.freeze({
|
|
226
|
+
mode: source.mode,
|
|
227
|
+
...(source.maxRetries === undefined ? {} : { maxRetries: source.maxRetries }),
|
|
228
|
+
...(source.retryableCodes === undefined ? {} : { retryableCodes: Object.freeze([...source.retryableCodes]) }),
|
|
229
|
+
...(backoff === undefined ? {} : { backoff }),
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function boundedProfile(raw) {
|
|
234
|
+
let encoded
|
|
235
|
+
try { encoded = JSON.stringify(raw) }
|
|
236
|
+
catch (cause) { throw new Error('enterprise profile must be JSON-serializable', { cause }) }
|
|
237
|
+
if (Buffer.byteLength(encoded, 'utf8') > MAX_PROFILE_BYTES) throw new Error(`enterprise profile exceeds ${MAX_PROFILE_BYTES} bytes`)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function normalizeEnterpriseProfile(raw) {
|
|
241
|
+
boundedProfile(raw)
|
|
242
|
+
const source = object(raw, 'enterprise profile')
|
|
243
|
+
exactKeys(source, allowedRootKeys, 'profile')
|
|
244
|
+
if (source.schemaVersion !== PROFILE_SCHEMA_VERSION) throw new Error(`unsupported enterprise profile schemaVersion ${String(source.schemaVersion)}`)
|
|
245
|
+
const id = text(source.id, 'profile.id', 64)
|
|
246
|
+
if (!idPattern.test(id)) throw new Error('profile.id is invalid')
|
|
247
|
+
const allowInsecureDevelopment = source.allowInsecureDevelopment === true
|
|
248
|
+
const allowedInsecureOrigin = source.insecureDevelopmentOrigin === undefined
|
|
249
|
+
? undefined
|
|
250
|
+
: insecureDevelopmentOrigin(source.insecureDevelopmentOrigin, 'profile.insecureDevelopmentOrigin')
|
|
251
|
+
if (allowedInsecureOrigin !== undefined && !allowInsecureDevelopment) {
|
|
252
|
+
throw new Error('profile.insecureDevelopmentOrigin requires allowInsecureDevelopment=true')
|
|
253
|
+
}
|
|
254
|
+
const oidc = object(source.oidc, 'profile.oidc')
|
|
255
|
+
const keyBinding = object(source.keyBinding, 'profile.keyBinding')
|
|
256
|
+
const provider = object(source.provider, 'profile.provider')
|
|
257
|
+
exactKeys(oidc, allowedOidcKeys, 'profile.oidc')
|
|
258
|
+
exactKeys(keyBinding, allowedKeyBindingKeys, 'profile.keyBinding')
|
|
259
|
+
exactKeys(provider, allowedProviderKeys, 'profile.provider')
|
|
260
|
+
const providerID = text(provider.id, 'profile.provider.id', 64)
|
|
261
|
+
if (!idPattern.test(providerID)) throw new Error('profile.provider.id is invalid')
|
|
262
|
+
const credentialRef = keyBinding.credentialRef === undefined
|
|
263
|
+
? providerCredentialRef(providerID)
|
|
264
|
+
: text(keyBinding.credentialRef, 'profile.keyBinding.credentialRef', 128)
|
|
265
|
+
if (!credentialPattern.test(credentialRef)) {
|
|
266
|
+
throw new Error('profile.keyBinding.credentialRef must be a valid DSH credential reference')
|
|
267
|
+
}
|
|
268
|
+
if (provider.adapter !== 'openai-compatible') throw new Error(`unsupported provider adapter ${String(provider.adapter)}`)
|
|
269
|
+
const scopes = oidc.scopes
|
|
270
|
+
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
|
+
throw new Error('profile.oidc.scopes must contain openid and profile; every scope must be a non-empty token')
|
|
272
|
+
}
|
|
273
|
+
if (new Set(scopes).size !== scopes.length) throw new Error('profile.oidc.scopes contains duplicates')
|
|
274
|
+
return Object.freeze({
|
|
275
|
+
schemaVersion: PROFILE_SCHEMA_VERSION,
|
|
276
|
+
id,
|
|
277
|
+
displayName: text(source.displayName, 'profile.displayName', 120),
|
|
278
|
+
organization: text(source.organization ?? source.displayName, 'profile.organization', 120),
|
|
279
|
+
allowInsecureDevelopment,
|
|
280
|
+
...(allowedInsecureOrigin === undefined ? {} : { insecureDevelopmentOrigin: allowedInsecureOrigin }),
|
|
281
|
+
nativeInstitutionID: source.nativeInstitutionID === undefined ? id : text(source.nativeInstitutionID, 'profile.nativeInstitutionID', 64),
|
|
282
|
+
brand: normalizeBrand(source.brand),
|
|
283
|
+
oidc: Object.freeze({
|
|
284
|
+
issuer: issuerURL(oidc.issuer, 'profile.oidc.issuer', allowInsecureDevelopment, allowedInsecureOrigin),
|
|
285
|
+
clientId: text(oidc.clientId, 'profile.oidc.clientId', 256),
|
|
286
|
+
scopes: Object.freeze([...scopes]),
|
|
287
|
+
}),
|
|
288
|
+
keyBinding: Object.freeze({
|
|
289
|
+
type: 'worker-user-center-v1',
|
|
290
|
+
baseURL: exactURL(keyBinding.baseURL, 'profile.keyBinding.baseURL', allowInsecureDevelopment, allowedInsecureOrigin),
|
|
291
|
+
providerId: providerID,
|
|
292
|
+
credentialRef,
|
|
293
|
+
}),
|
|
294
|
+
provider: Object.freeze({
|
|
295
|
+
id: providerID,
|
|
296
|
+
displayName: text(provider.displayName ?? source.displayName, 'profile.provider.displayName', 120),
|
|
297
|
+
adapter: provider.adapter,
|
|
298
|
+
baseURL: exactURL(provider.baseURL, 'profile.provider.baseURL', allowInsecureDevelopment, allowedInsecureOrigin),
|
|
299
|
+
reasoning: provider.reasoning === undefined ? 'high' : text(provider.reasoning, 'profile.provider.reasoning', 32),
|
|
300
|
+
defaultContextWindow: optionalPositiveInteger(provider.defaultContextWindow, 'profile.provider.defaultContextWindow'),
|
|
301
|
+
defaultMaxTokens: optionalPositiveInteger(provider.defaultMaxTokens, 'profile.provider.defaultMaxTokens'),
|
|
302
|
+
maxRequestImageBytes: optionalPositiveInteger(provider.maxRequestImageBytes, 'profile.provider.maxRequestImageBytes'),
|
|
303
|
+
requestImagePixelBudget: optionalPositiveInteger(provider.requestImagePixelBudget, 'profile.provider.requestImagePixelBudget'),
|
|
304
|
+
requestImageMaxBytes: optionalPositiveInteger(provider.requestImageMaxBytes, 'profile.provider.requestImageMaxBytes'),
|
|
305
|
+
streamIdleTimeoutMs: optionalPositiveInteger(provider.streamIdleTimeoutMs, 'profile.provider.streamIdleTimeoutMs'),
|
|
306
|
+
retryPolicy: provider.retryPolicy === undefined ? undefined : normalizeRetryPolicy(provider.retryPolicy),
|
|
307
|
+
compat: provider.compat === undefined ? undefined : normalizeCompat(provider.compat, 'profile.provider.compat'),
|
|
308
|
+
models: normalizeModels(provider.models, providerID),
|
|
309
|
+
}),
|
|
310
|
+
})
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function loadEnterpriseProfiles(raw = {}, environment = process.env) {
|
|
314
|
+
const profiles = []
|
|
315
|
+
if (raw.profile !== undefined) profiles.push(normalizeEnterpriseProfile(raw.profile))
|
|
316
|
+
if (raw.profiles !== undefined) {
|
|
317
|
+
if (!Array.isArray(raw.profiles)) throw new Error('dsh-oidc profiles must be an array')
|
|
318
|
+
profiles.push(...raw.profiles.map(normalizeEnterpriseProfile))
|
|
319
|
+
}
|
|
320
|
+
if (typeof raw.profilePathEnv === 'string' && environment[raw.profilePathEnv]) {
|
|
321
|
+
const parsed = JSON.parse(readFileSync(environment[raw.profilePathEnv], 'utf8'))
|
|
322
|
+
const rows = Array.isArray(parsed) ? parsed : parsed.profiles ?? [parsed]
|
|
323
|
+
profiles.push(...rows.map(normalizeEnterpriseProfile))
|
|
324
|
+
}
|
|
325
|
+
const unique = new Map()
|
|
326
|
+
const providers = new Set()
|
|
327
|
+
for (const profile of profiles) {
|
|
328
|
+
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}`)
|
|
330
|
+
unique.set(profile.id, profile)
|
|
331
|
+
providers.add(profile.provider.id)
|
|
332
|
+
}
|
|
333
|
+
return unique
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function enterpriseProviderConfig(profiles) {
|
|
337
|
+
return {
|
|
338
|
+
providers: Object.fromEntries([...profiles.values()].map(profile => [profile.provider.id, {
|
|
339
|
+
displayName: profile.provider.displayName,
|
|
340
|
+
apiKeyEnv: profile.keyBinding.credentialRef,
|
|
341
|
+
baseURL: profile.provider.baseURL,
|
|
342
|
+
allowInsecureDevelopment: profile.allowInsecureDevelopment,
|
|
343
|
+
...(profile.insecureDevelopmentOrigin === undefined ? {} : { insecureDevelopmentOrigin: profile.insecureDevelopmentOrigin }),
|
|
344
|
+
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,
|
|
354
|
+
}])),
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export function publicProfile(profile) {
|
|
359
|
+
return {
|
|
360
|
+
id: profile.id,
|
|
361
|
+
displayName: profile.displayName,
|
|
362
|
+
organization: profile.organization,
|
|
363
|
+
brand: profile.brand,
|
|
364
|
+
credentialRef: profile.keyBinding.credentialRef,
|
|
365
|
+
provider: {
|
|
366
|
+
id: profile.provider.id,
|
|
367
|
+
displayName: profile.provider.displayName,
|
|
368
|
+
models: profile.provider.models.map(model => ({ id: model.id, name: model.name, input: model.input })),
|
|
369
|
+
},
|
|
370
|
+
}
|
|
371
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
const modalities = new Set(['text', 'image'])
|
|
2
|
+
export const ENTERPRISE_SETTINGS_NAMESPACE = 'provider-enterprise'
|
|
3
|
+
export const ENTERPRISE_DEFAULT_MAX_REQUEST_IMAGE_BYTES = 20 * 1024 * 1024
|
|
4
|
+
export const ENTERPRISE_DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET = 2048 * 2048
|
|
5
|
+
export const ENTERPRISE_DEFAULT_REQUEST_IMAGE_MAX_BYTES = 1024 * 1024
|
|
6
|
+
export const ENTERPRISE_DEFAULT_RETRY_POLICY = Object.freeze({ mode: 'normal', maxRetries: 2 })
|
|
7
|
+
|
|
8
|
+
function nonEmpty(value, label) {
|
|
9
|
+
if (typeof value !== 'string' || value.trim().length === 0) throw new Error(`${label} must be a non-empty string`)
|
|
10
|
+
return value.trim()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function exactDevelopmentOrigin(value, label) {
|
|
14
|
+
if (value === undefined) return undefined
|
|
15
|
+
const raw = nonEmpty(value, label).replace(/\/+$/, '')
|
|
16
|
+
const parsed = new URL(raw)
|
|
17
|
+
if (parsed.protocol !== 'http:' || parsed.username || parsed.password || parsed.hash || parsed.search || (parsed.pathname !== '/' && parsed.pathname !== '')) {
|
|
18
|
+
throw new Error(`${label} must be an exact HTTP origin`)
|
|
19
|
+
}
|
|
20
|
+
return raw
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function endpointURL(value, label, allowInsecureDevelopment = false, insecureDevelopmentOrigin) {
|
|
24
|
+
const raw = nonEmpty(value, label).replace(/\/+$/, '')
|
|
25
|
+
const parsed = new URL(raw)
|
|
26
|
+
const loopback = ['localhost', '127.0.0.1', '[::1]'].includes(parsed.hostname)
|
|
27
|
+
const allowedOrigin = exactDevelopmentOrigin(insecureDevelopmentOrigin, `${label}.insecureDevelopmentOrigin`)
|
|
28
|
+
const permittedHTTP = parsed.protocol === 'http:' && (loopback || (allowInsecureDevelopment === true && parsed.origin === allowedOrigin))
|
|
29
|
+
if ((parsed.protocol !== 'https:' && !permittedHTTP) || parsed.username || parsed.password || parsed.hash || parsed.search) {
|
|
30
|
+
throw new Error(`${label} must be HTTPS or explicitly allowed development HTTP without credentials, query, or fragment`)
|
|
31
|
+
}
|
|
32
|
+
return raw
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function positiveInteger(value, fallback, label) {
|
|
36
|
+
const resolved = value ?? fallback
|
|
37
|
+
if (!Number.isSafeInteger(resolved) || resolved <= 0) throw new Error(`${label} must be a positive integer`)
|
|
38
|
+
return resolved
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizeInput(value, label) {
|
|
42
|
+
const input = value ?? ['text']
|
|
43
|
+
if (!Array.isArray(input) || input.length === 0 || input.some(item => !modalities.has(item))) {
|
|
44
|
+
throw new Error(`${label} must contain text and/or image`)
|
|
45
|
+
}
|
|
46
|
+
return [...new Set(input)]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeThinkingMap(value, reasoning) {
|
|
50
|
+
if (value === false || reasoning === false) return { off: null }
|
|
51
|
+
const source = value ?? { off: null, high: 'high', max: 'max' }
|
|
52
|
+
if (typeof source !== 'object' || source === null || Array.isArray(source)) throw new Error('reasoningEfforts must be an object or false')
|
|
53
|
+
return Object.fromEntries(Object.entries(source).map(([key, level]) => [key, level ?? null]))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function resolveEnterpriseProfiles(raw = {}) {
|
|
57
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) throw new Error('enterprise provider config must be an object')
|
|
58
|
+
const providers = raw.providers ?? {}
|
|
59
|
+
if (typeof providers !== 'object' || providers === null || Array.isArray(providers)) throw new Error('enterprise provider config.providers must be an object')
|
|
60
|
+
|
|
61
|
+
return Object.entries(providers).map(([provider, source]) => {
|
|
62
|
+
const route = nonEmpty(provider, 'provider route')
|
|
63
|
+
if (typeof source !== 'object' || source === null || Array.isArray(source)) throw new Error(`${route} config must be an object`)
|
|
64
|
+
const displayName = nonEmpty(source.displayName ?? route, `${route}.displayName`)
|
|
65
|
+
const baseURL = endpointURL(source.baseURL, `${route}.baseURL`, source.allowInsecureDevelopment, source.insecureDevelopmentOrigin)
|
|
66
|
+
const credentialRef = nonEmpty(source.apiKeyEnv, `${route}.apiKeyEnv`)
|
|
67
|
+
const contextWindow = positiveInteger(source.defaultContextWindow, 262144, `${route}.defaultContextWindow`)
|
|
68
|
+
const maxTokens = positiveInteger(source.defaultMaxTokens, 32768, `${route}.defaultMaxTokens`)
|
|
69
|
+
const maxRequestImageBytes = positiveInteger(source.maxRequestImageBytes, ENTERPRISE_DEFAULT_MAX_REQUEST_IMAGE_BYTES, `${route}.maxRequestImageBytes`)
|
|
70
|
+
const requestImagePixelBudget = positiveInteger(source.requestImagePixelBudget, ENTERPRISE_DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET, `${route}.requestImagePixelBudget`)
|
|
71
|
+
const requestImageMaxBytes = positiveInteger(source.requestImageMaxBytes, ENTERPRISE_DEFAULT_REQUEST_IMAGE_MAX_BYTES, `${route}.requestImageMaxBytes`)
|
|
72
|
+
if (!Array.isArray(source.models) || source.models.length === 0) throw new Error(`${route}.models must not be empty`)
|
|
73
|
+
|
|
74
|
+
const ids = new Set()
|
|
75
|
+
const models = source.models.map((model, index) => {
|
|
76
|
+
if (typeof model !== 'object' || model === null || Array.isArray(model)) throw new Error(`${route}.models[${index}] must be an object`)
|
|
77
|
+
const id = nonEmpty(model.id, `${route}.models[${index}].id`)
|
|
78
|
+
if (ids.has(id)) throw new Error(`${route} declares duplicate model ${id}`)
|
|
79
|
+
ids.add(id)
|
|
80
|
+
const reasoning = model.reasoning !== false && model.reasoningEfforts !== false
|
|
81
|
+
return {
|
|
82
|
+
id,
|
|
83
|
+
name: nonEmpty(model.name ?? id, `${route}.${id}.name`),
|
|
84
|
+
api: 'openai-completions',
|
|
85
|
+
provider: route,
|
|
86
|
+
baseUrl: baseURL,
|
|
87
|
+
reasoning,
|
|
88
|
+
thinkingLevelMap: normalizeThinkingMap(model.reasoningEfforts, reasoning),
|
|
89
|
+
input: normalizeInput(model.input, `${route}.${id}.input`),
|
|
90
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
91
|
+
contextWindow: positiveInteger(model.contextWindow, contextWindow, `${route}.${id}.contextWindow`),
|
|
92
|
+
maxTokens: positiveInteger(model.maxTokens, maxTokens, `${route}.${id}.maxTokens`),
|
|
93
|
+
compat: {
|
|
94
|
+
supportsDeveloperRole: false,
|
|
95
|
+
supportsStore: false,
|
|
96
|
+
supportsReasoningEffort: reasoning,
|
|
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 ?? {}),
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
provider: route,
|
|
111
|
+
displayName,
|
|
112
|
+
baseURL,
|
|
113
|
+
credentialRef,
|
|
114
|
+
reasoning: source.reasoning ?? 'high',
|
|
115
|
+
maxRequestImageBytes,
|
|
116
|
+
requestImagePixelBudget,
|
|
117
|
+
requestImageMaxBytes,
|
|
118
|
+
streamIdleTimeoutMs: positiveInteger(source.streamIdleTimeoutMs, 300000, `${route}.streamIdleTimeoutMs`),
|
|
119
|
+
retryPolicy: source.retryPolicy ?? ENTERPRISE_DEFAULT_RETRY_POLICY,
|
|
120
|
+
configuredMaxTokens: new Map(models.map(model => [model.id, model.maxTokens])),
|
|
121
|
+
models,
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function configurableEntries(profiles) {
|
|
127
|
+
return [...profiles.values()].map(profile => ({
|
|
128
|
+
provider: profile.provider,
|
|
129
|
+
displayName: profile.displayName,
|
|
130
|
+
settingsNs: ENTERPRISE_SETTINGS_NAMESPACE,
|
|
131
|
+
settingsPath: ['providers', profile.provider],
|
|
132
|
+
declared: true,
|
|
133
|
+
}))
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function settingsBase(rawConfig) {
|
|
137
|
+
return { providers: rawConfig.providers ?? {} }
|
|
138
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment'
|
|
2
|
+
import { LlmError, assertUsableApiKey, resolveRetryPolicy } from '@deepseek-ai/dsh-llm'
|
|
3
|
+
import { Config as PiAiConfig, PiAiAdapter } from '@deepseek-ai/dsh-llm-pi-ai'
|
|
4
|
+
import { createProvider } from '@earendil-works/pi-ai'
|
|
5
|
+
import { openAICompletionsApi } from '@earendil-works/pi-ai/api/openai-completions.lazy'
|
|
6
|
+
import {
|
|
7
|
+
configurableEntries, ENTERPRISE_DEFAULT_MAX_REQUEST_IMAGE_BYTES, ENTERPRISE_DEFAULT_REQUEST_IMAGE_MAX_BYTES,
|
|
8
|
+
ENTERPRISE_DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET, ENTERPRISE_DEFAULT_RETRY_POLICY, ENTERPRISE_SETTINGS_NAMESPACE,
|
|
9
|
+
resolveEnterpriseProfiles, settingsBase,
|
|
10
|
+
} from './core.js'
|
|
11
|
+
import { TransformingEnterpriseAdapter } from './transform-adapter.js'
|
|
12
|
+
import { EnterpriseModelTransforms } from './transforms.js'
|
|
13
|
+
|
|
14
|
+
export const name = 'dsh-oidc-provider'
|
|
15
|
+
export const inject = ['llm']
|
|
16
|
+
export const SETTINGS_NAMESPACE = ENTERPRISE_SETTINGS_NAMESPACE
|
|
17
|
+
|
|
18
|
+
function installEnterpriseSettings(ctx, rawConfig, hooks) {
|
|
19
|
+
ctx.inject(['settings'], (settingsCtx) => {
|
|
20
|
+
settingsCtx.settings.installSection(
|
|
21
|
+
ctx,
|
|
22
|
+
SETTINGS_NAMESPACE,
|
|
23
|
+
PiAiConfig,
|
|
24
|
+
settingsBase(rawConfig),
|
|
25
|
+
hooks,
|
|
26
|
+
)
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function apiKeyAuth(displayName) {
|
|
31
|
+
return {
|
|
32
|
+
name: displayName,
|
|
33
|
+
resolve: ({ credential }) => Promise.resolve({
|
|
34
|
+
auth: credential?.key === undefined ? {} : { apiKey: credential.key },
|
|
35
|
+
source: displayName,
|
|
36
|
+
}),
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function profilesFrom(rawConfig) {
|
|
41
|
+
const profiles = new Map()
|
|
42
|
+
for (const route of resolveEnterpriseProfiles(rawConfig)) {
|
|
43
|
+
profiles.set(route.provider, {
|
|
44
|
+
provider: route.provider,
|
|
45
|
+
displayName: route.displayName,
|
|
46
|
+
apiKeyEnv: route.credentialRef,
|
|
47
|
+
reasoning: route.reasoning,
|
|
48
|
+
maxRequestImageBytes: route.maxRequestImageBytes,
|
|
49
|
+
requestImagePixelBudget: route.requestImagePixelBudget,
|
|
50
|
+
requestImageMaxBytes: route.requestImageMaxBytes,
|
|
51
|
+
streamIdleTimeoutMs: route.streamIdleTimeoutMs,
|
|
52
|
+
retryPolicy: resolveRetryPolicy(route.retryPolicy, `${name}: ${route.provider}.retryPolicy`),
|
|
53
|
+
configuredMaxTokens: route.configuredMaxTokens,
|
|
54
|
+
modelPolicies: new Map(route.models.map(model => [model.id, {
|
|
55
|
+
reasoning: model.reasoning,
|
|
56
|
+
supportsReasoningEffort: model.compat.supportsReasoningEffort,
|
|
57
|
+
}])),
|
|
58
|
+
piProvider: createProvider({
|
|
59
|
+
id: route.provider,
|
|
60
|
+
name: route.displayName,
|
|
61
|
+
baseUrl: route.baseURL,
|
|
62
|
+
auth: { apiKey: apiKeyAuth(route.displayName) },
|
|
63
|
+
models: route.models,
|
|
64
|
+
api: openAICompletionsApi(),
|
|
65
|
+
}),
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
return profiles
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isolatedPiAiAuth() {
|
|
72
|
+
const stored = new Map()
|
|
73
|
+
return {
|
|
74
|
+
credentials: {
|
|
75
|
+
read: id => Promise.resolve(stored.get(id)),
|
|
76
|
+
list: () => Promise.resolve([]),
|
|
77
|
+
async modify(id, mutate) {
|
|
78
|
+
const next = await mutate(stored.get(id))
|
|
79
|
+
if (next === undefined) stored.delete(id)
|
|
80
|
+
else stored.set(id, next)
|
|
81
|
+
return next
|
|
82
|
+
},
|
|
83
|
+
delete: id => { stored.delete(id); return Promise.resolve() },
|
|
84
|
+
},
|
|
85
|
+
authContext: {
|
|
86
|
+
env: () => Promise.resolve(undefined),
|
|
87
|
+
fileExists: () => Promise.resolve(false),
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function apply(ctx, rawConfig = {}) {
|
|
93
|
+
const transforms = new EnterpriseModelTransforms(ctx)
|
|
94
|
+
let current = () => settingsBase(rawConfig)
|
|
95
|
+
let profiles = profilesFrom(current())
|
|
96
|
+
if (profiles.size === 0) return
|
|
97
|
+
|
|
98
|
+
const baseAdapter = new PiAiAdapter({
|
|
99
|
+
profiles: () => profiles,
|
|
100
|
+
auth: isolatedPiAiAuth(),
|
|
101
|
+
resolveApiKey: async (provider, profile) => {
|
|
102
|
+
const ref = profile.apiKeyEnv
|
|
103
|
+
const credentials = ctx.get('credentials')
|
|
104
|
+
const hit = credentials === undefined
|
|
105
|
+
? launchEnvironmentOf(ctx).get(ref)?.value
|
|
106
|
+
: (await credentials.resolve(ref))?.value
|
|
107
|
+
if (typeof hit === 'string' && hit.length > 0) return assertUsableApiKey(hit, name, ref)
|
|
108
|
+
throw new LlmError(`${name}: no credential for provider route "${provider}" (${ref})`, 'MISSING_CREDENTIAL')
|
|
109
|
+
},
|
|
110
|
+
resolveAttachments: () => ctx.get('attachments'),
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
const adapter = new TransformingEnterpriseAdapter(baseAdapter, transforms, (provider, model) => (
|
|
114
|
+
profiles.get(provider)?.modelPolicies.get(model) ?? { reasoning: true, supportsReasoningEffort: true }
|
|
115
|
+
))
|
|
116
|
+
const registration = ctx.llm.registerAdapter([...profiles.keys()], adapter)
|
|
117
|
+
let directory = ctx.llm.registerConfigurableProviders(configurableEntries(profiles))
|
|
118
|
+
|
|
119
|
+
if (rawConfig.settingsEnabled !== true) {
|
|
120
|
+
installEnterpriseSettings(ctx, rawConfig, { setSource() {}, onChange() {} })
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const refresh = () => {
|
|
125
|
+
const next = profilesFrom(current())
|
|
126
|
+
const previous = profiles
|
|
127
|
+
profiles = next
|
|
128
|
+
try {
|
|
129
|
+
registration.replace([...next.keys()])
|
|
130
|
+
directory.replace(configurableEntries(next))
|
|
131
|
+
} catch (error) {
|
|
132
|
+
profiles = previous
|
|
133
|
+
throw error
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
installEnterpriseSettings(ctx, rawConfig, {
|
|
138
|
+
setSource(source) { current = source },
|
|
139
|
+
onChange() {
|
|
140
|
+
try { refresh() }
|
|
141
|
+
catch (error) {
|
|
142
|
+
ctx.logger.error('dsh-oidc-provider: keeping previous routes after a refused settings update')
|
|
143
|
+
ctx.logger.error(error)
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export { resolveEnterpriseProfiles } from './core.js'
|
|
150
|
+
export { TransformingEnterpriseAdapter } from './transform-adapter.js'
|
|
151
|
+
export { EnterpriseModelTransforms } from './transforms.js'
|
|
152
|
+
export {
|
|
153
|
+
configurableEntries, ENTERPRISE_DEFAULT_MAX_REQUEST_IMAGE_BYTES, ENTERPRISE_DEFAULT_REQUEST_IMAGE_MAX_BYTES,
|
|
154
|
+
ENTERPRISE_DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET, ENTERPRISE_DEFAULT_RETRY_POLICY,
|
|
155
|
+
ENTERPRISE_SETTINGS_NAMESPACE, settingsBase,
|
|
156
|
+
}
|