@ouraihub/aether-worker 0.1.0 → 0.1.2
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/package.json +5 -4
- package/src/app.ts +52 -16
- package/src/context.ts +9 -3
- package/src/conversations/action.ts +31 -8
- package/src/conversations/index.ts +144 -141
- package/src/conversations/model.ts +36 -11
- package/src/conversations/routes.ts +101 -0
- package/src/coordinator-helpers.ts +131 -0
- package/src/coordinator.ts +54 -126
- package/src/credentials.ts +22 -4
- package/src/files.ts +17 -5
- package/src/host.ts +10 -2
- package/src/identity.ts +54 -25
- package/src/images/index.ts +39 -12
- package/src/logger.ts +15 -4
- package/src/mcp/auth.ts +65 -17
- package/src/mcp/client.ts +89 -24
- package/src/mcp/common.ts +7 -3
- package/src/mcp/index.ts +74 -21
- package/src/mcp/oauth.ts +82 -23
- package/src/mcp/types.ts +9 -2
- package/src/outbound.ts +23 -8
- package/src/providers/catalog.ts +63 -0
- package/src/providers/http.ts +88 -0
- package/src/providers/index.ts +103 -163
- package/src/providers/model-options.ts +28 -0
- package/src/runtime-input.ts +44 -0
- package/src/runtime-tools.ts +298 -0
- package/src/runtime.ts +105 -264
- package/src/search/index.ts +89 -23
- package/src/skills/persistence.ts +18 -5
- package/src/skills/service.ts +60 -13
- package/src/storage.ts +108 -13
- package/src/testing/fakes.ts +97 -0
package/src/providers/index.ts
CHANGED
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
type ProviderSave,
|
|
14
14
|
} from '@ouraihub/aether-contracts'
|
|
15
15
|
import { KitFailure, badRequest } from '@ouraihub/aether-core/error'
|
|
16
|
+
import { inferReasoning } from '@ouraihub/aether-core/providers/infer'
|
|
17
|
+
import { probe as probeEndpoint } from '@ouraihub/aether-core/providers/probe'
|
|
16
18
|
import {
|
|
17
19
|
prepare,
|
|
18
20
|
validateHeaders,
|
|
@@ -22,75 +24,26 @@ import {
|
|
|
22
24
|
charCount,
|
|
23
25
|
type EndpointSnapshot,
|
|
24
26
|
} from '@ouraihub/aether-core/providers/connection'
|
|
25
|
-
import { inferReasoning } from '@ouraihub/aether-core/providers/infer'
|
|
26
|
-
import { probe as probeEndpoint } from '@ouraihub/aether-core/providers/probe'
|
|
27
27
|
import type { Connection } from '@ouraihub/aether-core/types'
|
|
28
28
|
import type { OutboundHttp } from '@ouraihub/aether-core/ports'
|
|
29
29
|
import { ownerKey, type Actor, type WorkerContext } from '../context.js'
|
|
30
|
-
import {
|
|
30
|
+
import type { CredentialPort, CredentialScope, SealedCredential } from '../credentials.js'
|
|
31
31
|
import type { DocumentStore, StoredDocument } from '../storage.js'
|
|
32
|
+
import { parseCatalog, type Catalog, type Secret } from './catalog.js'
|
|
33
|
+
import { modelOptions } from './model-options.js'
|
|
34
|
+
import { handleProviderRequest } from './http.js'
|
|
32
35
|
|
|
33
|
-
interface Secret {
|
|
34
|
-
endpointId: string
|
|
35
|
-
keyId: string
|
|
36
|
-
sealed: SealedCredential
|
|
37
|
-
}
|
|
38
|
-
interface Catalog {
|
|
39
|
-
providers: Provider[]
|
|
40
|
-
secrets: Secret[]
|
|
41
|
-
inferred: string[]
|
|
42
|
-
}
|
|
43
36
|
export interface ProviderServiceOptions {
|
|
44
37
|
documents: DocumentStore
|
|
45
38
|
credentials: CredentialPort
|
|
46
39
|
outbound: OutboundHttp
|
|
47
40
|
authorizeReveal: (request: Request, context: WorkerContext) => Promise<void>
|
|
48
41
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
!record(value) ||
|
|
55
|
-
!Array.isArray(value.providers) ||
|
|
56
|
-
!Array.isArray(value.secrets) ||
|
|
57
|
-
!Array.isArray(value.inferred)
|
|
58
|
-
)
|
|
59
|
-
throw new KitFailure('incompatible_db', 'Invalid provider catalog')
|
|
60
|
-
const providers: Provider[] = []
|
|
61
|
-
const secrets: Secret[] = []
|
|
62
|
-
const inferred: string[] = []
|
|
63
|
-
for (const item of value.providers) {
|
|
64
|
-
if (!isContract('Provider', item)) throw new KitFailure('incompatible_db', 'Invalid provider document')
|
|
65
|
-
providers.push(item)
|
|
66
|
-
}
|
|
67
|
-
for (const item of value.secrets) {
|
|
68
|
-
if (
|
|
69
|
-
!record(item) ||
|
|
70
|
-
typeof item.endpointId !== 'string' ||
|
|
71
|
-
typeof item.keyId !== 'string' ||
|
|
72
|
-
!isSealedCredential(item.sealed)
|
|
73
|
-
)
|
|
74
|
-
throw new KitFailure('incompatible_db', 'Invalid provider credential')
|
|
75
|
-
secrets.push({ endpointId: item.endpointId, keyId: item.keyId, sealed: item.sealed })
|
|
76
|
-
}
|
|
77
|
-
for (const item of value.inferred) {
|
|
78
|
-
if (typeof item !== 'string') throw new KitFailure('incompatible_db', 'Invalid reasoning declaration')
|
|
79
|
-
inferred.push(item)
|
|
80
|
-
}
|
|
81
|
-
const automatic = new Set(inferred)
|
|
82
|
-
for (const provider of providers)
|
|
83
|
-
for (const endpoint of provider.endpoints)
|
|
84
|
-
for (const model of endpoint.models)
|
|
85
|
-
if (automatic.has(JSON.stringify([endpoint.id, model.id])))
|
|
86
|
-
model.reasoning =
|
|
87
|
-
model.modelType === 'chat'
|
|
88
|
-
? inferReasoning(model.id, endpoint.protocol, endpoint.baseUrl, model.maxOutput)
|
|
89
|
-
: null
|
|
90
|
-
return { providers, secrets, inferred }
|
|
91
|
-
}
|
|
92
|
-
const missing = (entity: string): KitFailure => new KitFailure('not_found', `Unknown ${entity}`, { entity })
|
|
93
|
-
const reasoningKey = (endpointId: string, modelId: string): string => JSON.stringify([endpointId, modelId])
|
|
42
|
+
|
|
43
|
+
const missing = (entity: string): KitFailure =>
|
|
44
|
+
new KitFailure('not_found', `Unknown ${entity}`, { entity })
|
|
45
|
+
const reasoningKey = (endpointId: string, modelId: string): string =>
|
|
46
|
+
JSON.stringify([endpointId, modelId])
|
|
94
47
|
export class ProviderService {
|
|
95
48
|
private readonly cursors = new Map<string, number>()
|
|
96
49
|
private readonly options: ProviderServiceOptions
|
|
@@ -108,13 +61,31 @@ export class ProviderService {
|
|
|
108
61
|
}
|
|
109
62
|
private async load(actor: Actor): Promise<{ document: StoredDocument | null; catalog: Catalog }> {
|
|
110
63
|
const document = await this.options.documents.get(actor, 'providers', 'catalog')
|
|
111
|
-
return {
|
|
64
|
+
return {
|
|
65
|
+
document,
|
|
66
|
+
catalog: document
|
|
67
|
+
? parseCatalog(document.value)
|
|
68
|
+
: { providers: [], secrets: [], inferred: [] },
|
|
69
|
+
}
|
|
112
70
|
}
|
|
113
|
-
private async commit(
|
|
114
|
-
|
|
71
|
+
private async commit(
|
|
72
|
+
actor: Actor,
|
|
73
|
+
document: StoredDocument | null,
|
|
74
|
+
catalog: Catalog,
|
|
75
|
+
): Promise<Provider[]> {
|
|
76
|
+
await this.options.documents.compareAndSet(
|
|
77
|
+
actor,
|
|
78
|
+
'providers',
|
|
79
|
+
'catalog',
|
|
80
|
+
document?.revision ?? null,
|
|
81
|
+
catalog,
|
|
82
|
+
)
|
|
115
83
|
return this.list(actor)
|
|
116
84
|
}
|
|
117
|
-
private endpoint(
|
|
85
|
+
private endpoint(
|
|
86
|
+
catalog: Catalog,
|
|
87
|
+
endpointId: string,
|
|
88
|
+
): { provider: Provider; endpoint: Endpoint } {
|
|
118
89
|
for (const provider of catalog.providers) {
|
|
119
90
|
const endpoint = provider.endpoints.find((item) => item.id === endpointId)
|
|
120
91
|
if (endpoint) return { provider, endpoint }
|
|
@@ -122,7 +93,10 @@ export class ProviderService {
|
|
|
122
93
|
throw missing('endpoint')
|
|
123
94
|
}
|
|
124
95
|
private async open(actor: Actor, secret: Secret): Promise<string> {
|
|
125
|
-
const bytes = await this.options.credentials.open(
|
|
96
|
+
const bytes = await this.options.credentials.open(
|
|
97
|
+
this.scope(actor, secret.keyId),
|
|
98
|
+
secret.sealed,
|
|
99
|
+
)
|
|
126
100
|
try {
|
|
127
101
|
return new TextDecoder('utf-8', { fatal: true }).decode(bytes)
|
|
128
102
|
} finally {
|
|
@@ -134,7 +108,9 @@ export class ProviderService {
|
|
|
134
108
|
for (const provider of catalog.providers)
|
|
135
109
|
for (const endpoint of provider.endpoints)
|
|
136
110
|
for (const key of endpoint.keys) {
|
|
137
|
-
const secret = catalog.secrets.find(
|
|
111
|
+
const secret = catalog.secrets.find(
|
|
112
|
+
(item) => item.endpointId === endpoint.id && item.keyId === key.id,
|
|
113
|
+
)
|
|
138
114
|
let plain: string | null = null
|
|
139
115
|
if (secret)
|
|
140
116
|
try {
|
|
@@ -143,7 +119,12 @@ export class ProviderService {
|
|
|
143
119
|
/* Unreadable credentials remain visible but cannot execute. */
|
|
144
120
|
}
|
|
145
121
|
key.readable = plain !== null
|
|
146
|
-
key.masked =
|
|
122
|
+
key.masked =
|
|
123
|
+
plain === null
|
|
124
|
+
? null
|
|
125
|
+
: plain.length > 8
|
|
126
|
+
? `${plain.slice(0, 4)}…${plain.slice(-4)}`
|
|
127
|
+
: '********'
|
|
147
128
|
}
|
|
148
129
|
return catalog.providers
|
|
149
130
|
}
|
|
@@ -152,10 +133,12 @@ export class ProviderService {
|
|
|
152
133
|
const { document, catalog } = await this.load(actor)
|
|
153
134
|
const name = input.name.trim()
|
|
154
135
|
if (!name || charCount(name) > 80) throw badRequest('name', 'invalid')
|
|
155
|
-
if (input.notes !== undefined && charCount(input.notes) > 10000)
|
|
136
|
+
if (input.notes !== undefined && charCount(input.notes) > 10000)
|
|
137
|
+
throw badRequest('notes', 'too_long', 10000)
|
|
156
138
|
if (input.endpoints.length > 20) throw badRequest('endpoints', 'too_many', 20)
|
|
157
139
|
if (input.presetId && !input.id) {
|
|
158
|
-
if (catalog.providers.some((item) => item.presetId === input.presetId))
|
|
140
|
+
if (catalog.providers.some((item) => item.presetId === input.presetId))
|
|
141
|
+
return this.list(actor)
|
|
159
142
|
const adopted = catalog.providers.find((item) => item.name === name && item.presetId === null)
|
|
160
143
|
if (adopted) {
|
|
161
144
|
adopted.presetId = input.presetId
|
|
@@ -164,7 +147,8 @@ export class ProviderService {
|
|
|
164
147
|
}
|
|
165
148
|
const old = input.id ? catalog.providers.find((item) => item.id === input.id) : undefined
|
|
166
149
|
if (input.id && !old) throw missing('provider')
|
|
167
|
-
if (input.presetId && old && input.presetId !== old.presetId)
|
|
150
|
+
if (input.presetId && old && input.presetId !== old.presetId)
|
|
151
|
+
throw badRequest('presetId', 'immutable')
|
|
168
152
|
const provider: Provider = {
|
|
169
153
|
id: old?.id ?? crypto.randomUUID(),
|
|
170
154
|
name,
|
|
@@ -178,7 +162,8 @@ export class ProviderService {
|
|
|
178
162
|
const endpointIds = new Set<string>()
|
|
179
163
|
const inferred = new Set(catalog.inferred)
|
|
180
164
|
for (const saved of input.endpoints) {
|
|
181
|
-
if (!/^[a-z0-9-]{1,40}$/.test(saved.slug) || slugs.has(saved.slug))
|
|
165
|
+
if (!/^[a-z0-9-]{1,40}$/.test(saved.slug) || slugs.has(saved.slug))
|
|
166
|
+
throw badRequest('slug', 'invalid')
|
|
182
167
|
slugs.add(saved.slug)
|
|
183
168
|
const previous = saved.id ? old?.endpoints.find((item) => item.id === saved.id) : undefined
|
|
184
169
|
if (saved.id && !previous) throw missing('endpoint')
|
|
@@ -237,11 +222,15 @@ export class ProviderService {
|
|
|
237
222
|
catalog.providers = old
|
|
238
223
|
? catalog.providers.map((item) => (item.id === old.id ? provider : item))
|
|
239
224
|
: [...catalog.providers, provider]
|
|
240
|
-
const allEndpoints = new Set(
|
|
225
|
+
const allEndpoints = new Set(
|
|
226
|
+
catalog.providers.flatMap((item) => item.endpoints.map((endpoint) => endpoint.id)),
|
|
227
|
+
)
|
|
241
228
|
catalog.secrets = catalog.secrets.filter((item) => allEndpoints.has(item.endpointId))
|
|
242
229
|
catalog.inferred = [...inferred].filter((key) =>
|
|
243
230
|
catalog.providers.some((item) =>
|
|
244
|
-
item.endpoints.some((endpoint) =>
|
|
231
|
+
item.endpoints.some((endpoint) =>
|
|
232
|
+
endpoint.models.some((model) => reasoningKey(endpoint.id, model.id) === key),
|
|
233
|
+
),
|
|
245
234
|
),
|
|
246
235
|
)
|
|
247
236
|
return this.commit(actor, document, catalog)
|
|
@@ -285,7 +274,9 @@ export class ProviderService {
|
|
|
285
274
|
if (!plain) throw badRequest('apiKey', 'empty')
|
|
286
275
|
if (
|
|
287
276
|
new TextEncoder().encode(plain).length > 4096 ||
|
|
288
|
-
[...plain].some(
|
|
277
|
+
[...plain].some(
|
|
278
|
+
(character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127,
|
|
279
|
+
)
|
|
289
280
|
)
|
|
290
281
|
throw badRequest('apiKey', 'too_long', 4096)
|
|
291
282
|
for (const secret of catalog.secrets)
|
|
@@ -294,7 +285,9 @@ export class ProviderService {
|
|
|
294
285
|
try {
|
|
295
286
|
existing = await this.open(actor, secret)
|
|
296
287
|
} catch {
|
|
297
|
-
throw new KitFailure('needs_config', 'Credential cannot be read', {
|
|
288
|
+
throw new KitFailure('needs_config', 'Credential cannot be read', {
|
|
289
|
+
reason: 'key_unreadable',
|
|
290
|
+
})
|
|
298
291
|
}
|
|
299
292
|
if (existing === plain) throw badRequest('apiKey', 'duplicate')
|
|
300
293
|
}
|
|
@@ -317,7 +310,9 @@ export class ProviderService {
|
|
|
317
310
|
exhaustedUntil: input.restore ? null : (old?.exhaustedUntil ?? null),
|
|
318
311
|
exhaustedReason: input.restore ? null : (old?.exhaustedReason ?? null),
|
|
319
312
|
}
|
|
320
|
-
endpoint.keys = old
|
|
313
|
+
endpoint.keys = old
|
|
314
|
+
? endpoint.keys.map((item) => (item.id === id ? key : item))
|
|
315
|
+
: [...endpoint.keys, key]
|
|
321
316
|
return this.commit(actor, document, catalog)
|
|
322
317
|
}
|
|
323
318
|
async removeKey(actor: Actor, endpointId: string, keyId: string): Promise<Provider[]> {
|
|
@@ -325,7 +320,9 @@ export class ProviderService {
|
|
|
325
320
|
const { endpoint } = this.endpoint(catalog, endpointId)
|
|
326
321
|
if (!endpoint.keys.some((item) => item.id === keyId)) throw missing('key')
|
|
327
322
|
endpoint.keys = endpoint.keys.filter((item) => item.id !== keyId)
|
|
328
|
-
catalog.secrets = catalog.secrets.filter(
|
|
323
|
+
catalog.secrets = catalog.secrets.filter(
|
|
324
|
+
(item) => !(item.keyId === keyId && item.endpointId === endpointId),
|
|
325
|
+
)
|
|
329
326
|
return this.commit(actor, document, catalog)
|
|
330
327
|
}
|
|
331
328
|
async revealKey(actor: Actor, keyId: string): Promise<string> {
|
|
@@ -335,7 +332,9 @@ export class ProviderService {
|
|
|
335
332
|
try {
|
|
336
333
|
return await this.open(actor, secret)
|
|
337
334
|
} catch {
|
|
338
|
-
throw new KitFailure('needs_config', 'Credential cannot be read', {
|
|
335
|
+
throw new KitFailure('needs_config', 'Credential cannot be read', {
|
|
336
|
+
reason: 'key_unreadable',
|
|
337
|
+
})
|
|
339
338
|
}
|
|
340
339
|
}
|
|
341
340
|
private async snapshotFrom(
|
|
@@ -346,16 +345,25 @@ export class ProviderService {
|
|
|
346
345
|
): Promise<EndpointSnapshot> {
|
|
347
346
|
const { endpoint } = this.endpoint(catalog, endpointId)
|
|
348
347
|
const chosen =
|
|
349
|
-
keyId === undefined
|
|
348
|
+
keyId === undefined
|
|
349
|
+
? endpoint.keys.filter((key) => key.enabled)
|
|
350
|
+
: endpoint.keys.filter((key) => key.id === keyId)
|
|
350
351
|
if (keyId !== undefined && !chosen.length) throw missing('key')
|
|
351
352
|
const keys: string[] = []
|
|
352
353
|
for (const key of chosen) {
|
|
353
|
-
const secret = catalog.secrets.find(
|
|
354
|
-
|
|
354
|
+
const secret = catalog.secrets.find(
|
|
355
|
+
(item) => item.keyId === key.id && item.endpointId === endpoint.id,
|
|
356
|
+
)
|
|
357
|
+
if (!secret)
|
|
358
|
+
throw new KitFailure('needs_config', 'Credential cannot be read', {
|
|
359
|
+
reason: 'key_unreadable',
|
|
360
|
+
})
|
|
355
361
|
try {
|
|
356
362
|
keys.push(await this.open(actor, secret))
|
|
357
363
|
} catch {
|
|
358
|
-
throw new KitFailure('needs_config', 'Credential cannot be read', {
|
|
364
|
+
throw new KitFailure('needs_config', 'Credential cannot be read', {
|
|
365
|
+
reason: 'key_unreadable',
|
|
366
|
+
})
|
|
359
367
|
}
|
|
360
368
|
}
|
|
361
369
|
if (keyId === undefined && keys.length) {
|
|
@@ -381,12 +389,14 @@ export class ProviderService {
|
|
|
381
389
|
async resolve(actor: Actor, reference: ModelRef, requiredType?: ModelType): Promise<Connection> {
|
|
382
390
|
const { catalog } = await this.load(actor)
|
|
383
391
|
const provider = catalog.providers.find(
|
|
384
|
-
(item) =>
|
|
392
|
+
(item) =>
|
|
393
|
+
item.enabled && item.endpoints.some((endpoint) => endpoint.id === reference.endpointId),
|
|
385
394
|
)
|
|
386
395
|
const endpoint = provider?.endpoints.find((item) => item.id === reference.endpointId)
|
|
387
396
|
const model = endpoint?.models.find((item) => item.id === reference.modelId)
|
|
388
397
|
if (!endpoint || !model) throw missing('model')
|
|
389
|
-
if (requiredType !== undefined && model.modelType !== requiredType)
|
|
398
|
+
if (requiredType !== undefined && model.modelType !== requiredType)
|
|
399
|
+
throw badRequest('model', 'unsupported_type')
|
|
390
400
|
prepare({
|
|
391
401
|
endpoint: {
|
|
392
402
|
endpointId: endpoint.id,
|
|
@@ -403,35 +413,14 @@ export class ProviderService {
|
|
|
403
413
|
}
|
|
404
414
|
async modelOptions(actor: Actor, filter: ModelFilter = {}): Promise<ModelOption[]> {
|
|
405
415
|
const { catalog } = await this.load(actor)
|
|
406
|
-
return catalog
|
|
407
|
-
.filter((provider) => provider.enabled)
|
|
408
|
-
.flatMap((provider) =>
|
|
409
|
-
provider.endpoints.flatMap((endpoint) =>
|
|
410
|
-
endpoint.models
|
|
411
|
-
.filter(
|
|
412
|
-
(model) =>
|
|
413
|
-
(filter.modelType === undefined || model.modelType === filter.modelType) &&
|
|
414
|
-
(filter.supportsTools === undefined || model.supportsTools === filter.supportsTools),
|
|
415
|
-
)
|
|
416
|
-
.map((model) => ({
|
|
417
|
-
endpointId: endpoint.id,
|
|
418
|
-
modelId: model.id,
|
|
419
|
-
label: `${provider.name} · ${endpoint.slug} · ${model.id}`,
|
|
420
|
-
name: model.name,
|
|
421
|
-
modelType: model.modelType,
|
|
422
|
-
supportsTools: model.supportsTools,
|
|
423
|
-
reasoning: model.reasoning,
|
|
424
|
-
nativeWebSearch: model.nativeWebSearch,
|
|
425
|
-
canVision: model.canVision,
|
|
426
|
-
})),
|
|
427
|
-
),
|
|
428
|
-
)
|
|
416
|
+
return modelOptions(catalog, filter)
|
|
429
417
|
}
|
|
430
418
|
async probe(actor: Actor, target: ProbeTarget, signal: AbortSignal): Promise<ProbeResult> {
|
|
431
419
|
if (!isContract('ProbeTarget', target)) throw badRequest('target', 'invalid')
|
|
432
420
|
const { catalog } = await this.load(actor)
|
|
433
421
|
let endpoint: EndpointSnapshot
|
|
434
|
-
if (target.kind === 'saved')
|
|
422
|
+
if (target.kind === 'saved')
|
|
423
|
+
endpoint = await this.snapshotFrom(actor, catalog, target.endpointId)
|
|
435
424
|
else {
|
|
436
425
|
if (target.keyId !== undefined && target.endpointId === undefined) throw missing('key')
|
|
437
426
|
endpoint = target.endpointId
|
|
@@ -440,66 +429,17 @@ export class ProviderService {
|
|
|
440
429
|
baseUrl: target.baseUrl,
|
|
441
430
|
protocol: target.protocol,
|
|
442
431
|
}
|
|
443
|
-
: {
|
|
432
|
+
: {
|
|
433
|
+
endpointId: null,
|
|
434
|
+
baseUrl: target.baseUrl,
|
|
435
|
+
protocol: target.protocol,
|
|
436
|
+
keys: [],
|
|
437
|
+
headers: {},
|
|
438
|
+
}
|
|
444
439
|
}
|
|
445
440
|
return probeEndpoint(this.options.outbound, { endpoint, signal })
|
|
446
441
|
}
|
|
447
442
|
async handle(request: Request, context: WorkerContext): Promise<Response | null> {
|
|
448
|
-
|
|
449
|
-
const path = url.pathname
|
|
450
|
-
const root = '/api/v1/providers'
|
|
451
|
-
if (path !== root && !path.startsWith(`${root}/`)) return null
|
|
452
|
-
const actor = context.actor
|
|
453
|
-
const json = (value: unknown): Response => Response.json(value)
|
|
454
|
-
if (path === root && request.method === 'GET') return json(await this.list(actor))
|
|
455
|
-
if (path === root && request.method === 'POST') {
|
|
456
|
-
const value: unknown = await request.json()
|
|
457
|
-
if (!isContract('ProviderSave', value)) throw badRequest('provider', 'invalid')
|
|
458
|
-
return json(await this.save(actor, value))
|
|
459
|
-
}
|
|
460
|
-
if (path === `${root}/model-options` && request.method === 'GET') {
|
|
461
|
-
const value: unknown = {
|
|
462
|
-
...(url.searchParams.has('modelType') ? { modelType: url.searchParams.get('modelType') } : {}),
|
|
463
|
-
...(url.searchParams.has('supportsTools')
|
|
464
|
-
? { supportsTools: url.searchParams.get('supportsTools') === 'true' }
|
|
465
|
-
: {}),
|
|
466
|
-
}
|
|
467
|
-
if (
|
|
468
|
-
url.searchParams.has('supportsTools') &&
|
|
469
|
-
!['true', 'false'].includes(url.searchParams.get('supportsTools') ?? '')
|
|
470
|
-
)
|
|
471
|
-
throw badRequest('supportsTools', 'invalid')
|
|
472
|
-
if (!isContract('ModelFilter', value)) throw badRequest('filter', 'invalid')
|
|
473
|
-
return json(await this.modelOptions(actor, value))
|
|
474
|
-
}
|
|
475
|
-
if (path === `${root}/probe` && request.method === 'POST') {
|
|
476
|
-
const value: unknown = await request.json()
|
|
477
|
-
if (!isContract('ProbeTarget', value)) throw badRequest('target', 'invalid')
|
|
478
|
-
return json(await this.probe(actor, value, context.signal))
|
|
479
|
-
}
|
|
480
|
-
if (path === `${root}/keys` && request.method === 'POST') {
|
|
481
|
-
const value: unknown = await request.json()
|
|
482
|
-
if (!isContract('EndpointKeySave', value)) throw badRequest('key', 'invalid')
|
|
483
|
-
return json(await this.saveKey(actor, value))
|
|
484
|
-
}
|
|
485
|
-
const reveal = path.match(/^\/api\/v1\/providers\/keys\/([^/]+)\/reveal$/)
|
|
486
|
-
if (reveal && request.method === 'POST') {
|
|
487
|
-
await this.options.authorizeReveal(request, context)
|
|
488
|
-
return json(await this.revealKey(actor, decodeURIComponent(reveal[1] ?? '')))
|
|
489
|
-
}
|
|
490
|
-
const key = path.match(/^\/api\/v1\/providers\/([^/]+)\/keys\/([^/]+)$/)
|
|
491
|
-
if (key && request.method === 'DELETE')
|
|
492
|
-
return json(await this.removeKey(actor, decodeURIComponent(key[1] ?? ''), decodeURIComponent(key[2] ?? '')))
|
|
493
|
-
const enabled = path.match(/^\/api\/v1\/providers\/([^/]+)\/enabled$/)
|
|
494
|
-
if (enabled && request.method === 'POST') {
|
|
495
|
-
const value: unknown = await request.json()
|
|
496
|
-
if (!record(value) || Object.keys(value).length !== 1 || typeof value.enabled !== 'boolean')
|
|
497
|
-
throw badRequest('enabled', 'invalid')
|
|
498
|
-
return json(await this.setEnabled(actor, decodeURIComponent(enabled[1] ?? ''), value.enabled))
|
|
499
|
-
}
|
|
500
|
-
const provider = path.match(/^\/api\/v1\/providers\/([^/]+)$/)
|
|
501
|
-
if (provider && request.method === 'DELETE')
|
|
502
|
-
return json(await this.remove(actor, decodeURIComponent(provider[1] ?? '')))
|
|
503
|
-
throw missing('route')
|
|
443
|
+
return handleProviderRequest(this, request, context, this.options.authorizeReveal)
|
|
504
444
|
}
|
|
505
445
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type ModelFilter, type ModelOption } from '@ouraihub/aether-contracts'
|
|
2
|
+
import type { Catalog } from './catalog.js'
|
|
3
|
+
|
|
4
|
+
export function modelOptions(catalog: Catalog, filter: ModelFilter): ModelOption[] {
|
|
5
|
+
return catalog.providers
|
|
6
|
+
.filter((provider) => provider.enabled)
|
|
7
|
+
.flatMap((provider) =>
|
|
8
|
+
provider.endpoints.flatMap((endpoint) =>
|
|
9
|
+
endpoint.models
|
|
10
|
+
.filter(
|
|
11
|
+
(model) =>
|
|
12
|
+
(filter.modelType === undefined || model.modelType === filter.modelType) &&
|
|
13
|
+
(filter.supportsTools === undefined || model.supportsTools === filter.supportsTools),
|
|
14
|
+
)
|
|
15
|
+
.map((model) => ({
|
|
16
|
+
endpointId: endpoint.id,
|
|
17
|
+
modelId: model.id,
|
|
18
|
+
label: `${provider.name} · ${endpoint.slug} · ${model.id}`,
|
|
19
|
+
name: model.name,
|
|
20
|
+
modelType: model.modelType,
|
|
21
|
+
supportsTools: model.supportsTools,
|
|
22
|
+
reasoning: model.reasoning,
|
|
23
|
+
nativeWebSearch: model.nativeWebSearch,
|
|
24
|
+
canVision: model.canVision,
|
|
25
|
+
})),
|
|
26
|
+
),
|
|
27
|
+
)
|
|
28
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { EndStatus, KitError } from '@ouraihub/aether-contracts'
|
|
2
|
+
import { safeError } from '@ouraihub/aether-core/error'
|
|
3
|
+
import { charCount } from '@ouraihub/aether-core/guard'
|
|
4
|
+
import { renderSystem, validate } from '@ouraihub/aether-core/engine'
|
|
5
|
+
import { trimHistory } from '@ouraihub/aether-core/history'
|
|
6
|
+
import type { HistoryTurn, RunInput, RunSummary } from '@ouraihub/aether-core/types'
|
|
7
|
+
|
|
8
|
+
export const emptySummary = (status: EndStatus, error?: KitError): RunSummary => ({
|
|
9
|
+
status,
|
|
10
|
+
messages: [],
|
|
11
|
+
usage: {
|
|
12
|
+
inputTokens: null,
|
|
13
|
+
outputTokens: null,
|
|
14
|
+
cacheReadTokens: null,
|
|
15
|
+
cacheWriteTokens: null,
|
|
16
|
+
reasoningTokens: null,
|
|
17
|
+
},
|
|
18
|
+
cost: null,
|
|
19
|
+
contextUsage: null,
|
|
20
|
+
exhausted: false,
|
|
21
|
+
...(error ? { error } : {}),
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
/** Final snapshots never copy arbitrary host/upstream exception text or details. */
|
|
25
|
+
export const publicError = (error: unknown): KitError => ({
|
|
26
|
+
code: safeError(error).code,
|
|
27
|
+
message: 'Run could not be completed',
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
export function assemble(input: RunInput, history: readonly HistoryTurn[]): RunInput {
|
|
31
|
+
const checked = validate({ ...input, history: [] })
|
|
32
|
+
const systemChars = charCount(renderSystem(checked.system, checked.permissionMode === 'plan'))
|
|
33
|
+
const available =
|
|
34
|
+
checked.budget.contextWindow -
|
|
35
|
+
checked.budget.maxOutput -
|
|
36
|
+
Math.ceil((systemChars + charCount(checked.input)) / 3)
|
|
37
|
+
return {
|
|
38
|
+
...checked,
|
|
39
|
+
history:
|
|
40
|
+
checked.purpose === 'writing'
|
|
41
|
+
? []
|
|
42
|
+
: trimHistory(history, available, checked.connection.canVision),
|
|
43
|
+
}
|
|
44
|
+
}
|