@ouraihub/aether-worker 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +789 -0
- package/migrations/0001.sql +22 -0
- package/package.json +38 -3
- package/src/app.ts +363 -0
- package/src/cloudflare.ts +49 -0
- package/src/context.ts +55 -0
- package/src/conversations/action.ts +257 -0
- package/src/conversations/index.ts +480 -0
- package/src/conversations/model.ts +427 -0
- package/src/conversations/routes.ts +101 -0
- package/src/coordinator-helpers.ts +131 -0
- package/src/coordinator.ts +488 -0
- package/src/credentials.ts +171 -0
- package/src/files.ts +117 -0
- package/src/host.ts +25 -0
- package/src/identity.ts +227 -0
- package/src/images/index.ts +73 -0
- package/src/index.ts +30 -0
- package/src/logger.ts +98 -0
- package/src/mcp/auth.ts +398 -0
- package/src/mcp/client.ts +491 -0
- package/src/mcp/common.ts +124 -0
- package/src/mcp/index.ts +292 -0
- package/src/mcp/oauth.ts +343 -0
- package/src/mcp/types.ts +81 -0
- package/src/outbound.ts +320 -0
- package/src/providers/catalog.ts +63 -0
- package/src/providers/http.ts +88 -0
- package/src/providers/index.ts +445 -0
- 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 +476 -0
- package/src/search/index.ts +497 -0
- package/src/skills/index.ts +1 -0
- package/src/skills/persistence.ts +153 -0
- package/src/skills/service.ts +250 -0
- package/src/storage.ts +293 -0
- package/src/testing/fakes.ts +97 -0
- package/README.md +0 -3
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isContract,
|
|
3
|
+
type Endpoint,
|
|
4
|
+
type EndpointKeySave,
|
|
5
|
+
type Model,
|
|
6
|
+
type ModelFilter,
|
|
7
|
+
type ModelOption,
|
|
8
|
+
type ModelRef,
|
|
9
|
+
type ModelType,
|
|
10
|
+
type ProbeResult,
|
|
11
|
+
type ProbeTarget,
|
|
12
|
+
type Provider,
|
|
13
|
+
type ProviderSave,
|
|
14
|
+
} from '@ouraihub/aether-contracts'
|
|
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'
|
|
18
|
+
import {
|
|
19
|
+
prepare,
|
|
20
|
+
validateHeaders,
|
|
21
|
+
validateModel,
|
|
22
|
+
validateUrl,
|
|
23
|
+
apiBase,
|
|
24
|
+
charCount,
|
|
25
|
+
type EndpointSnapshot,
|
|
26
|
+
} from '@ouraihub/aether-core/providers/connection'
|
|
27
|
+
import type { Connection } from '@ouraihub/aether-core/types'
|
|
28
|
+
import type { OutboundHttp } from '@ouraihub/aether-core/ports'
|
|
29
|
+
import { ownerKey, type Actor, type WorkerContext } from '../context.js'
|
|
30
|
+
import type { CredentialPort, CredentialScope, SealedCredential } from '../credentials.js'
|
|
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'
|
|
35
|
+
|
|
36
|
+
export interface ProviderServiceOptions {
|
|
37
|
+
documents: DocumentStore
|
|
38
|
+
credentials: CredentialPort
|
|
39
|
+
outbound: OutboundHttp
|
|
40
|
+
authorizeReveal: (request: Request, context: WorkerContext) => Promise<void>
|
|
41
|
+
}
|
|
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])
|
|
47
|
+
export class ProviderService {
|
|
48
|
+
private readonly cursors = new Map<string, number>()
|
|
49
|
+
private readonly options: ProviderServiceOptions
|
|
50
|
+
constructor(options: ProviderServiceOptions) {
|
|
51
|
+
this.options = options
|
|
52
|
+
}
|
|
53
|
+
private scope(actor: Actor, keyId: string): CredentialScope {
|
|
54
|
+
return {
|
|
55
|
+
applicationId: actor.applicationId,
|
|
56
|
+
tenantId: actor.tenantId,
|
|
57
|
+
subjectId: actor.subjectId,
|
|
58
|
+
purpose: 'provider',
|
|
59
|
+
recordId: keyId,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
private async load(actor: Actor): Promise<{ document: StoredDocument | null; catalog: Catalog }> {
|
|
63
|
+
const document = await this.options.documents.get(actor, 'providers', 'catalog')
|
|
64
|
+
return {
|
|
65
|
+
document,
|
|
66
|
+
catalog: document
|
|
67
|
+
? parseCatalog(document.value)
|
|
68
|
+
: { providers: [], secrets: [], inferred: [] },
|
|
69
|
+
}
|
|
70
|
+
}
|
|
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
|
+
)
|
|
83
|
+
return this.list(actor)
|
|
84
|
+
}
|
|
85
|
+
private endpoint(
|
|
86
|
+
catalog: Catalog,
|
|
87
|
+
endpointId: string,
|
|
88
|
+
): { provider: Provider; endpoint: Endpoint } {
|
|
89
|
+
for (const provider of catalog.providers) {
|
|
90
|
+
const endpoint = provider.endpoints.find((item) => item.id === endpointId)
|
|
91
|
+
if (endpoint) return { provider, endpoint }
|
|
92
|
+
}
|
|
93
|
+
throw missing('endpoint')
|
|
94
|
+
}
|
|
95
|
+
private async open(actor: Actor, secret: Secret): Promise<string> {
|
|
96
|
+
const bytes = await this.options.credentials.open(
|
|
97
|
+
this.scope(actor, secret.keyId),
|
|
98
|
+
secret.sealed,
|
|
99
|
+
)
|
|
100
|
+
try {
|
|
101
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(bytes)
|
|
102
|
+
} finally {
|
|
103
|
+
bytes.fill(0)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async list(actor: Actor): Promise<Provider[]> {
|
|
107
|
+
const { catalog } = await this.load(actor)
|
|
108
|
+
for (const provider of catalog.providers)
|
|
109
|
+
for (const endpoint of provider.endpoints)
|
|
110
|
+
for (const key of endpoint.keys) {
|
|
111
|
+
const secret = catalog.secrets.find(
|
|
112
|
+
(item) => item.endpointId === endpoint.id && item.keyId === key.id,
|
|
113
|
+
)
|
|
114
|
+
let plain: string | null = null
|
|
115
|
+
if (secret)
|
|
116
|
+
try {
|
|
117
|
+
plain = await this.open(actor, secret)
|
|
118
|
+
} catch {
|
|
119
|
+
/* Unreadable credentials remain visible but cannot execute. */
|
|
120
|
+
}
|
|
121
|
+
key.readable = plain !== null
|
|
122
|
+
key.masked =
|
|
123
|
+
plain === null
|
|
124
|
+
? null
|
|
125
|
+
: plain.length > 8
|
|
126
|
+
? `${plain.slice(0, 4)}…${plain.slice(-4)}`
|
|
127
|
+
: '********'
|
|
128
|
+
}
|
|
129
|
+
return catalog.providers
|
|
130
|
+
}
|
|
131
|
+
async save(actor: Actor, input: ProviderSave): Promise<Provider[]> {
|
|
132
|
+
if (!isContract('ProviderSave', input)) throw badRequest('provider', 'invalid')
|
|
133
|
+
const { document, catalog } = await this.load(actor)
|
|
134
|
+
const name = input.name.trim()
|
|
135
|
+
if (!name || charCount(name) > 80) throw badRequest('name', 'invalid')
|
|
136
|
+
if (input.notes !== undefined && charCount(input.notes) > 10000)
|
|
137
|
+
throw badRequest('notes', 'too_long', 10000)
|
|
138
|
+
if (input.endpoints.length > 20) throw badRequest('endpoints', 'too_many', 20)
|
|
139
|
+
if (input.presetId && !input.id) {
|
|
140
|
+
if (catalog.providers.some((item) => item.presetId === input.presetId))
|
|
141
|
+
return this.list(actor)
|
|
142
|
+
const adopted = catalog.providers.find((item) => item.name === name && item.presetId === null)
|
|
143
|
+
if (adopted) {
|
|
144
|
+
adopted.presetId = input.presetId
|
|
145
|
+
return this.commit(actor, document, catalog)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const old = input.id ? catalog.providers.find((item) => item.id === input.id) : undefined
|
|
149
|
+
if (input.id && !old) throw missing('provider')
|
|
150
|
+
if (input.presetId && old && input.presetId !== old.presetId)
|
|
151
|
+
throw badRequest('presetId', 'immutable')
|
|
152
|
+
const provider: Provider = {
|
|
153
|
+
id: old?.id ?? crypto.randomUUID(),
|
|
154
|
+
name,
|
|
155
|
+
enabled: old?.enabled ?? !input.presetId,
|
|
156
|
+
notes: input.notes ?? old?.notes ?? '',
|
|
157
|
+
presetId: old?.presetId ?? input.presetId ?? null,
|
|
158
|
+
endpoints: [],
|
|
159
|
+
}
|
|
160
|
+
if (input.enabled !== undefined && !(!old && input.presetId)) provider.enabled = input.enabled
|
|
161
|
+
const slugs = new Set<string>()
|
|
162
|
+
const endpointIds = new Set<string>()
|
|
163
|
+
const inferred = new Set(catalog.inferred)
|
|
164
|
+
for (const saved of input.endpoints) {
|
|
165
|
+
if (!/^[a-z0-9-]{1,40}$/.test(saved.slug) || slugs.has(saved.slug))
|
|
166
|
+
throw badRequest('slug', 'invalid')
|
|
167
|
+
slugs.add(saved.slug)
|
|
168
|
+
const previous = saved.id ? old?.endpoints.find((item) => item.id === saved.id) : undefined
|
|
169
|
+
if (saved.id && !previous) throw missing('endpoint')
|
|
170
|
+
const id = previous?.id ?? crypto.randomUUID()
|
|
171
|
+
if (endpointIds.has(id)) throw badRequest('endpoints', 'duplicate')
|
|
172
|
+
endpointIds.add(id)
|
|
173
|
+
validateUrl('baseUrl', saved.baseUrl)
|
|
174
|
+
const override = (field: 'imageBaseUrl' | 'imageEditBaseUrl'): string | null => {
|
|
175
|
+
const value = saved[field] === undefined ? (previous?.[field] ?? null) : saved[field]
|
|
176
|
+
if (value) validateUrl(field, value)
|
|
177
|
+
return value?.trim() || null
|
|
178
|
+
}
|
|
179
|
+
if (saved.models.length > 1000) throw badRequest('models', 'too_many', 1000)
|
|
180
|
+
const models: Model[] = []
|
|
181
|
+
const modelIds = new Set<string>()
|
|
182
|
+
for (const savedModel of saved.models) {
|
|
183
|
+
if (modelIds.has(savedModel.id)) throw badRequest('models', 'duplicate')
|
|
184
|
+
modelIds.add(savedModel.id)
|
|
185
|
+
const prior = previous?.models.find((item) => item.id === savedModel.id)
|
|
186
|
+
const key = reasoningKey(id, savedModel.id)
|
|
187
|
+
const infer = savedModel.reasoning === undefined && (!prior || inferred.has(key))
|
|
188
|
+
const reasoning =
|
|
189
|
+
savedModel.reasoning === undefined
|
|
190
|
+
? infer
|
|
191
|
+
? savedModel.modelType === 'chat'
|
|
192
|
+
? inferReasoning(savedModel.id, saved.protocol, saved.baseUrl, savedModel.maxOutput)
|
|
193
|
+
: null
|
|
194
|
+
: (prior?.reasoning ?? null)
|
|
195
|
+
: savedModel.reasoning
|
|
196
|
+
if (infer) inferred.add(key)
|
|
197
|
+
else inferred.delete(key)
|
|
198
|
+
const model: Model = {
|
|
199
|
+
...savedModel,
|
|
200
|
+
name: savedModel.name?.trim() || null,
|
|
201
|
+
groupName: savedModel.groupName?.trim() || null,
|
|
202
|
+
reasoning,
|
|
203
|
+
}
|
|
204
|
+
validateModel(model, saved.protocol)
|
|
205
|
+
models.push(model)
|
|
206
|
+
}
|
|
207
|
+
const endpoint: Endpoint = {
|
|
208
|
+
id,
|
|
209
|
+
slug: saved.slug,
|
|
210
|
+
baseUrl: saved.baseUrl.trim(),
|
|
211
|
+
protocol: saved.protocol,
|
|
212
|
+
keys: previous?.keys ?? [],
|
|
213
|
+
keyStrategy: 'round_robin',
|
|
214
|
+
probeUrl: `${apiBase(saved.baseUrl)}/models`,
|
|
215
|
+
models,
|
|
216
|
+
imageBaseUrl: override('imageBaseUrl'),
|
|
217
|
+
imageEditBaseUrl: override('imageEditBaseUrl'),
|
|
218
|
+
headers: validateHeaders(saved.headers ?? previous?.headers ?? {}),
|
|
219
|
+
}
|
|
220
|
+
provider.endpoints.push(endpoint)
|
|
221
|
+
}
|
|
222
|
+
catalog.providers = old
|
|
223
|
+
? catalog.providers.map((item) => (item.id === old.id ? provider : item))
|
|
224
|
+
: [...catalog.providers, provider]
|
|
225
|
+
const allEndpoints = new Set(
|
|
226
|
+
catalog.providers.flatMap((item) => item.endpoints.map((endpoint) => endpoint.id)),
|
|
227
|
+
)
|
|
228
|
+
catalog.secrets = catalog.secrets.filter((item) => allEndpoints.has(item.endpointId))
|
|
229
|
+
catalog.inferred = [...inferred].filter((key) =>
|
|
230
|
+
catalog.providers.some((item) =>
|
|
231
|
+
item.endpoints.some((endpoint) =>
|
|
232
|
+
endpoint.models.some((model) => reasoningKey(endpoint.id, model.id) === key),
|
|
233
|
+
),
|
|
234
|
+
),
|
|
235
|
+
)
|
|
236
|
+
return this.commit(actor, document, catalog)
|
|
237
|
+
}
|
|
238
|
+
async remove(actor: Actor, id: string): Promise<Provider[]> {
|
|
239
|
+
const { document, catalog } = await this.load(actor)
|
|
240
|
+
const provider = catalog.providers.find((item) => item.id === id)
|
|
241
|
+
if (!provider) throw missing('provider')
|
|
242
|
+
if (provider.presetId) throw badRequest('provider', 'preset')
|
|
243
|
+
catalog.providers = catalog.providers.filter((item) => item.id !== id)
|
|
244
|
+
const ids = new Set(provider.endpoints.map((endpoint) => endpoint.id))
|
|
245
|
+
catalog.secrets = catalog.secrets.filter((item) => !ids.has(item.endpointId))
|
|
246
|
+
catalog.inferred = catalog.inferred.filter(
|
|
247
|
+
(key) =>
|
|
248
|
+
!provider.endpoints.some((endpoint) =>
|
|
249
|
+
endpoint.models.some((model) => reasoningKey(endpoint.id, model.id) === key),
|
|
250
|
+
),
|
|
251
|
+
)
|
|
252
|
+
return this.commit(actor, document, catalog)
|
|
253
|
+
}
|
|
254
|
+
async setEnabled(actor: Actor, id: string, enabled: boolean): Promise<Provider[]> {
|
|
255
|
+
const { document, catalog } = await this.load(actor)
|
|
256
|
+
const provider = catalog.providers.find((item) => item.id === id)
|
|
257
|
+
if (!provider) throw missing('provider')
|
|
258
|
+
provider.enabled = enabled
|
|
259
|
+
return this.commit(actor, document, catalog)
|
|
260
|
+
}
|
|
261
|
+
async saveKey(actor: Actor, input: EndpointKeySave): Promise<Provider[]> {
|
|
262
|
+
if (!isContract('EndpointKeySave', input)) throw badRequest('key', 'invalid')
|
|
263
|
+
const { document, catalog } = await this.load(actor)
|
|
264
|
+
const { endpoint } = this.endpoint(catalog, input.endpointId)
|
|
265
|
+
const old = input.id ? endpoint.keys.find((item) => item.id === input.id) : undefined
|
|
266
|
+
if (input.id && !old) throw missing('key')
|
|
267
|
+
if (!old && endpoint.keys.length >= 16) throw badRequest('keys', 'too_many', 16)
|
|
268
|
+
if (!old && input.key === undefined) throw badRequest('apiKey', 'empty')
|
|
269
|
+
const label = input.label?.trim() ?? old?.label ?? ''
|
|
270
|
+
if (charCount(label) > 40) throw badRequest('label', 'too_long', 40)
|
|
271
|
+
const id = old?.id ?? crypto.randomUUID()
|
|
272
|
+
if (input.key !== undefined) {
|
|
273
|
+
const plain = input.key.trim()
|
|
274
|
+
if (!plain) throw badRequest('apiKey', 'empty')
|
|
275
|
+
if (
|
|
276
|
+
new TextEncoder().encode(plain).length > 4096 ||
|
|
277
|
+
[...plain].some(
|
|
278
|
+
(character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127,
|
|
279
|
+
)
|
|
280
|
+
)
|
|
281
|
+
throw badRequest('apiKey', 'too_long', 4096)
|
|
282
|
+
for (const secret of catalog.secrets)
|
|
283
|
+
if (secret.endpointId === endpoint.id && secret.keyId !== id) {
|
|
284
|
+
let existing: string
|
|
285
|
+
try {
|
|
286
|
+
existing = await this.open(actor, secret)
|
|
287
|
+
} catch {
|
|
288
|
+
throw new KitFailure('needs_config', 'Credential cannot be read', {
|
|
289
|
+
reason: 'key_unreadable',
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
if (existing === plain) throw badRequest('apiKey', 'duplicate')
|
|
293
|
+
}
|
|
294
|
+
const bytes = new TextEncoder().encode(plain)
|
|
295
|
+
let sealed: SealedCredential
|
|
296
|
+
try {
|
|
297
|
+
sealed = await this.options.credentials.seal(this.scope(actor, id), bytes)
|
|
298
|
+
} finally {
|
|
299
|
+
bytes.fill(0)
|
|
300
|
+
}
|
|
301
|
+
catalog.secrets = catalog.secrets.filter((item) => item.keyId !== id)
|
|
302
|
+
catalog.secrets.push({ endpointId: endpoint.id, keyId: id, sealed })
|
|
303
|
+
}
|
|
304
|
+
const key = {
|
|
305
|
+
id,
|
|
306
|
+
label,
|
|
307
|
+
enabled: input.enabled ?? old?.enabled ?? true,
|
|
308
|
+
masked: null,
|
|
309
|
+
readable: true,
|
|
310
|
+
exhaustedUntil: input.restore ? null : (old?.exhaustedUntil ?? null),
|
|
311
|
+
exhaustedReason: input.restore ? null : (old?.exhaustedReason ?? null),
|
|
312
|
+
}
|
|
313
|
+
endpoint.keys = old
|
|
314
|
+
? endpoint.keys.map((item) => (item.id === id ? key : item))
|
|
315
|
+
: [...endpoint.keys, key]
|
|
316
|
+
return this.commit(actor, document, catalog)
|
|
317
|
+
}
|
|
318
|
+
async removeKey(actor: Actor, endpointId: string, keyId: string): Promise<Provider[]> {
|
|
319
|
+
const { document, catalog } = await this.load(actor)
|
|
320
|
+
const { endpoint } = this.endpoint(catalog, endpointId)
|
|
321
|
+
if (!endpoint.keys.some((item) => item.id === keyId)) throw missing('key')
|
|
322
|
+
endpoint.keys = endpoint.keys.filter((item) => item.id !== keyId)
|
|
323
|
+
catalog.secrets = catalog.secrets.filter(
|
|
324
|
+
(item) => !(item.keyId === keyId && item.endpointId === endpointId),
|
|
325
|
+
)
|
|
326
|
+
return this.commit(actor, document, catalog)
|
|
327
|
+
}
|
|
328
|
+
async revealKey(actor: Actor, keyId: string): Promise<string> {
|
|
329
|
+
const { catalog } = await this.load(actor)
|
|
330
|
+
const secret = catalog.secrets.find((item) => item.keyId === keyId)
|
|
331
|
+
if (!secret) throw missing('key')
|
|
332
|
+
try {
|
|
333
|
+
return await this.open(actor, secret)
|
|
334
|
+
} catch {
|
|
335
|
+
throw new KitFailure('needs_config', 'Credential cannot be read', {
|
|
336
|
+
reason: 'key_unreadable',
|
|
337
|
+
})
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
private async snapshotFrom(
|
|
341
|
+
actor: Actor,
|
|
342
|
+
catalog: Catalog,
|
|
343
|
+
endpointId: string,
|
|
344
|
+
keyId?: string,
|
|
345
|
+
): Promise<EndpointSnapshot> {
|
|
346
|
+
const { endpoint } = this.endpoint(catalog, endpointId)
|
|
347
|
+
const chosen =
|
|
348
|
+
keyId === undefined
|
|
349
|
+
? endpoint.keys.filter((key) => key.enabled)
|
|
350
|
+
: endpoint.keys.filter((key) => key.id === keyId)
|
|
351
|
+
if (keyId !== undefined && !chosen.length) throw missing('key')
|
|
352
|
+
const keys: string[] = []
|
|
353
|
+
for (const key of chosen) {
|
|
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
|
+
})
|
|
361
|
+
try {
|
|
362
|
+
keys.push(await this.open(actor, secret))
|
|
363
|
+
} catch {
|
|
364
|
+
throw new KitFailure('needs_config', 'Credential cannot be read', {
|
|
365
|
+
reason: 'key_unreadable',
|
|
366
|
+
})
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (keyId === undefined && keys.length) {
|
|
370
|
+
const cursorKey = JSON.stringify([ownerKey(actor), endpointId])
|
|
371
|
+
const start = (this.cursors.get(cursorKey) ?? 0) % keys.length
|
|
372
|
+
this.cursors.set(cursorKey, (start + 1) % keys.length)
|
|
373
|
+
keys.push(...keys.splice(0, start))
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
endpointId,
|
|
377
|
+
baseUrl: endpoint.baseUrl,
|
|
378
|
+
protocol: endpoint.protocol,
|
|
379
|
+
keys,
|
|
380
|
+
headers: validateHeaders(endpoint.headers),
|
|
381
|
+
imageBaseUrl: endpoint.imageBaseUrl,
|
|
382
|
+
imageEditBaseUrl: endpoint.imageEditBaseUrl,
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
async endpointSnapshot(actor: Actor, endpointId: string): Promise<EndpointSnapshot> {
|
|
386
|
+
const { catalog } = await this.load(actor)
|
|
387
|
+
return this.snapshotFrom(actor, catalog, endpointId)
|
|
388
|
+
}
|
|
389
|
+
async resolve(actor: Actor, reference: ModelRef, requiredType?: ModelType): Promise<Connection> {
|
|
390
|
+
const { catalog } = await this.load(actor)
|
|
391
|
+
const provider = catalog.providers.find(
|
|
392
|
+
(item) =>
|
|
393
|
+
item.enabled && item.endpoints.some((endpoint) => endpoint.id === reference.endpointId),
|
|
394
|
+
)
|
|
395
|
+
const endpoint = provider?.endpoints.find((item) => item.id === reference.endpointId)
|
|
396
|
+
const model = endpoint?.models.find((item) => item.id === reference.modelId)
|
|
397
|
+
if (!endpoint || !model) throw missing('model')
|
|
398
|
+
if (requiredType !== undefined && model.modelType !== requiredType)
|
|
399
|
+
throw badRequest('model', 'unsupported_type')
|
|
400
|
+
prepare({
|
|
401
|
+
endpoint: {
|
|
402
|
+
endpointId: endpoint.id,
|
|
403
|
+
baseUrl: endpoint.baseUrl,
|
|
404
|
+
protocol: endpoint.protocol,
|
|
405
|
+
keys: [],
|
|
406
|
+
headers: validateHeaders(endpoint.headers),
|
|
407
|
+
imageBaseUrl: endpoint.imageBaseUrl,
|
|
408
|
+
imageEditBaseUrl: endpoint.imageEditBaseUrl,
|
|
409
|
+
},
|
|
410
|
+
model,
|
|
411
|
+
})
|
|
412
|
+
return prepare({ endpoint: await this.snapshotFrom(actor, catalog, endpoint.id), model })
|
|
413
|
+
}
|
|
414
|
+
async modelOptions(actor: Actor, filter: ModelFilter = {}): Promise<ModelOption[]> {
|
|
415
|
+
const { catalog } = await this.load(actor)
|
|
416
|
+
return modelOptions(catalog, filter)
|
|
417
|
+
}
|
|
418
|
+
async probe(actor: Actor, target: ProbeTarget, signal: AbortSignal): Promise<ProbeResult> {
|
|
419
|
+
if (!isContract('ProbeTarget', target)) throw badRequest('target', 'invalid')
|
|
420
|
+
const { catalog } = await this.load(actor)
|
|
421
|
+
let endpoint: EndpointSnapshot
|
|
422
|
+
if (target.kind === 'saved')
|
|
423
|
+
endpoint = await this.snapshotFrom(actor, catalog, target.endpointId)
|
|
424
|
+
else {
|
|
425
|
+
if (target.keyId !== undefined && target.endpointId === undefined) throw missing('key')
|
|
426
|
+
endpoint = target.endpointId
|
|
427
|
+
? {
|
|
428
|
+
...(await this.snapshotFrom(actor, catalog, target.endpointId, target.keyId)),
|
|
429
|
+
baseUrl: target.baseUrl,
|
|
430
|
+
protocol: target.protocol,
|
|
431
|
+
}
|
|
432
|
+
: {
|
|
433
|
+
endpointId: null,
|
|
434
|
+
baseUrl: target.baseUrl,
|
|
435
|
+
protocol: target.protocol,
|
|
436
|
+
keys: [],
|
|
437
|
+
headers: {},
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return probeEndpoint(this.options.outbound, { endpoint, signal })
|
|
441
|
+
}
|
|
442
|
+
async handle(request: Request, context: WorkerContext): Promise<Response | null> {
|
|
443
|
+
return handleProviderRequest(this, request, context, this.options.authorizeReveal)
|
|
444
|
+
}
|
|
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
|
+
}
|