@ouraihub/aether-core 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 +2 -2
- package/src/engine/index.ts +95 -129
- package/src/engine/machine.ts +322 -0
- package/src/engine/model.ts +28 -16
- package/src/engine/prepare.ts +120 -34
- package/src/engine/tools.ts +59 -142
- package/src/error.ts +7 -2
- package/src/guard.ts +3 -1
- package/src/history.ts +9 -2
- package/src/images/index.ts +212 -39
- package/src/images/validation.ts +276 -0
- package/src/mcp/index.ts +83 -245
- package/src/mcp/protocol.ts +207 -0
- package/src/mcp/types.ts +49 -0
- package/src/pricing.ts +10 -2
- package/src/protocol/decoder.ts +39 -14
- package/src/protocol/request.ts +52 -17
- package/src/providers/connection.ts +16 -5
- package/src/providers/infer.ts +11 -6
- package/src/providers/probe.ts +20 -6
- package/src/risk.ts +13 -3
- package/src/schema.ts +34 -8
- package/src/search/execution.ts +21 -6
- package/src/search/index.ts +11 -2
- package/src/search/policy.ts +43 -10
- package/src/search/vendor.ts +42 -9
- package/src/skills/content.ts +13 -3
- package/src/skills/index.ts +6 -1
- package/src/skills/package.ts +29 -7
- package/src/skills/service.ts +54 -13
- package/src/skills/validation.ts +38 -9
package/src/pricing.ts
CHANGED
|
@@ -6,7 +6,13 @@ export const emptyUsage = (): TokenUsage => ({
|
|
|
6
6
|
cacheWriteTokens: null,
|
|
7
7
|
reasoningTokens: null,
|
|
8
8
|
})
|
|
9
|
-
const USAGE_KEYS = [
|
|
9
|
+
const USAGE_KEYS = [
|
|
10
|
+
'inputTokens',
|
|
11
|
+
'outputTokens',
|
|
12
|
+
'cacheReadTokens',
|
|
13
|
+
'cacheWriteTokens',
|
|
14
|
+
'reasoningTokens',
|
|
15
|
+
] as const
|
|
10
16
|
export function addUsage(total: TokenUsage, next: TokenUsage): void {
|
|
11
17
|
for (const key of USAGE_KEYS) {
|
|
12
18
|
const left = total[key],
|
|
@@ -20,7 +26,9 @@ export function cost(pricing: ModelPricing | null, rounds: readonly TokenUsage[]
|
|
|
20
26
|
for (const round of rounds) {
|
|
21
27
|
const inputTokens = round.inputTokens
|
|
22
28
|
if (inputTokens === null || round.outputTokens === null) return null
|
|
23
|
-
const tier =
|
|
29
|
+
const tier =
|
|
30
|
+
pricing.tiers.findLast((candidate) => candidate.minInputTokens <= inputTokens) ??
|
|
31
|
+
pricing.tiers[0]
|
|
24
32
|
if (!tier) return null
|
|
25
33
|
const read = round.cacheReadTokens ?? 0,
|
|
26
34
|
write = round.cacheWriteTokens ?? 0
|
package/src/protocol/decoder.ts
CHANGED
|
@@ -6,9 +6,12 @@ type Usage = TokenUsage
|
|
|
6
6
|
export type Decoded = { text?: string; reasoning?: string; error?: string } | null
|
|
7
7
|
|
|
8
8
|
const tokens = (value: unknown): number | null =>
|
|
9
|
-
typeof value === 'number' && Number.isInteger(value) && value >= 0 && value <= 0xffff_ffff
|
|
9
|
+
typeof value === 'number' && Number.isInteger(value) && value >= 0 && value <= 0xffff_ffff
|
|
10
|
+
? value
|
|
11
|
+
: null
|
|
10
12
|
|
|
11
|
-
const text = (value: unknown): Decoded =>
|
|
13
|
+
const text = (value: unknown): Decoded =>
|
|
14
|
+
typeof value === 'string' && value ? { text: value } : null
|
|
12
15
|
|
|
13
16
|
const error = (value: unknown): Decoded => {
|
|
14
17
|
const message = at(value, 'message')
|
|
@@ -58,22 +61,34 @@ export class Decoder {
|
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
private keep(value: unknown): void {
|
|
61
|
-
const block = {
|
|
64
|
+
const block = {
|
|
65
|
+
protocol: this.protocol,
|
|
66
|
+
endpointId: this.endpointId,
|
|
67
|
+
modelId: this.modelId,
|
|
68
|
+
value,
|
|
69
|
+
}
|
|
62
70
|
const id = at(value, 'id')
|
|
63
|
-
const existing =
|
|
71
|
+
const existing =
|
|
72
|
+
id == null ? -1 : this.reasoning.findIndex((item) => at(item.value, 'id') === id)
|
|
64
73
|
if (existing < 0) this.reasoning.push(block)
|
|
65
74
|
else this.reasoning[existing] = block
|
|
66
75
|
}
|
|
67
76
|
|
|
68
77
|
private start(key: unknown, id: unknown, name: unknown): void {
|
|
69
78
|
if (this.calls.length >= 128) throw new KitFailure('upstream', 'Tool call limit exceeded')
|
|
70
|
-
this.calls.push({
|
|
79
|
+
this.calls.push({
|
|
80
|
+
key,
|
|
81
|
+
id: typeof id === 'string' ? id : '',
|
|
82
|
+
name: typeof name === 'string' ? name : '',
|
|
83
|
+
args: '',
|
|
84
|
+
})
|
|
71
85
|
}
|
|
72
86
|
|
|
73
87
|
private append(key: unknown, fragment: unknown): void {
|
|
74
88
|
const call = this.calls.find((item) => item.key === key)
|
|
75
89
|
if (call && typeof fragment === 'string') {
|
|
76
|
-
if (call.args.length + fragment.length > 262144)
|
|
90
|
+
if (call.args.length + fragment.length > 262144)
|
|
91
|
+
throw new KitFailure('upstream', 'Tool arguments limit exceeded')
|
|
77
92
|
call.args += fragment
|
|
78
93
|
}
|
|
79
94
|
}
|
|
@@ -109,7 +124,8 @@ export class Decoder {
|
|
|
109
124
|
this.baseInput = tokens(at(raw, 'input_tokens')) ?? this.baseInput
|
|
110
125
|
usage.outputTokens = tokens(at(raw, 'output_tokens')) ?? usage.outputTokens
|
|
111
126
|
usage.cacheReadTokens = tokens(at(raw, 'cache_read_input_tokens')) ?? usage.cacheReadTokens
|
|
112
|
-
usage.cacheWriteTokens =
|
|
127
|
+
usage.cacheWriteTokens =
|
|
128
|
+
tokens(at(raw, 'cache_creation_input_tokens')) ?? usage.cacheWriteTokens
|
|
113
129
|
usage.reasoningTokens = tokens(at(raw, 'reasoning_tokens')) ?? usage.reasoningTokens
|
|
114
130
|
if (this.baseInput !== null)
|
|
115
131
|
usage.inputTokens = Math.min(
|
|
@@ -128,7 +144,8 @@ export class Decoder {
|
|
|
128
144
|
if (at(block, 'type') === 'tool_use') {
|
|
129
145
|
this.start(index, at(block, 'id'), at(block, 'name'))
|
|
130
146
|
const input = at(block, 'input')
|
|
131
|
-
if (isRecord(input) && Object.keys(input).length)
|
|
147
|
+
if (isRecord(input) && Object.keys(input).length)
|
|
148
|
+
this.append(index, JSON.stringify(input))
|
|
132
149
|
}
|
|
133
150
|
if (
|
|
134
151
|
index !== null &&
|
|
@@ -157,7 +174,8 @@ export class Decoder {
|
|
|
157
174
|
}
|
|
158
175
|
if (block && at(delta, 'type') === 'signature_delta') {
|
|
159
176
|
const fragment = at(delta, 'signature')
|
|
160
|
-
if (typeof fragment === 'string')
|
|
177
|
+
if (typeof fragment === 'string')
|
|
178
|
+
block.signature = String(block.signature ?? '') + fragment
|
|
161
179
|
}
|
|
162
180
|
} else if (kind === 'content_block_stop') {
|
|
163
181
|
const index = Number(at(v, 'index')),
|
|
@@ -179,7 +197,8 @@ export class Decoder {
|
|
|
179
197
|
if (raw && typeof raw === 'object') {
|
|
180
198
|
usage.inputTokens = tokens(at(raw, 'prompt_tokens')) ?? usage.inputTokens
|
|
181
199
|
usage.outputTokens = tokens(at(raw, 'completion_tokens')) ?? usage.outputTokens
|
|
182
|
-
usage.cacheReadTokens =
|
|
200
|
+
usage.cacheReadTokens =
|
|
201
|
+
tokens(at(raw, 'prompt_tokens_details', 'cached_tokens')) ?? usage.cacheReadTokens
|
|
183
202
|
usage.cacheWriteTokens =
|
|
184
203
|
tokens(at(raw, 'prompt_tokens_details', 'cache_write_tokens')) ?? usage.cacheWriteTokens
|
|
185
204
|
usage.reasoningTokens =
|
|
@@ -206,14 +225,19 @@ export class Decoder {
|
|
|
206
225
|
}
|
|
207
226
|
const content = at(v, 'choices', 0, 'delta', 'content')
|
|
208
227
|
if (typeof thinking === 'string' && thinking)
|
|
209
|
-
return {
|
|
228
|
+
return {
|
|
229
|
+
reasoning: thinking,
|
|
230
|
+
...(typeof content === 'string' && content ? { text: content } : {}),
|
|
231
|
+
}
|
|
210
232
|
return text(at(v, 'choices', 0, 'delta', 'content'))
|
|
211
233
|
}
|
|
212
|
-
if (kind === 'response.output_item.done' && at(v, 'item', 'type') === 'reasoning')
|
|
234
|
+
if (kind === 'response.output_item.done' && at(v, 'item', 'type') === 'reasoning')
|
|
235
|
+
this.keep(at(v, 'item'))
|
|
213
236
|
// `call_id` (not the item id) is what `function_call_output` must echo.
|
|
214
237
|
if (kind === 'response.output_item.added' && at(v, 'item', 'type') === 'function_call')
|
|
215
238
|
this.start(at(v, 'item', 'id'), at(v, 'item', 'call_id'), at(v, 'item', 'name'))
|
|
216
|
-
if (kind === 'response.function_call_arguments.delta')
|
|
239
|
+
if (kind === 'response.function_call_arguments.delta')
|
|
240
|
+
this.append(at(v, 'item_id'), at(v, 'delta'))
|
|
217
241
|
if (kind === 'response.reasoning_summary_text.delta') {
|
|
218
242
|
const fragment = at(v, 'delta')
|
|
219
243
|
if (typeof fragment === 'string') return { reasoning: fragment }
|
|
@@ -228,7 +252,8 @@ export class Decoder {
|
|
|
228
252
|
usage.cacheWriteTokens = tokens(at(raw, 'input_tokens_details', 'cache_write_tokens'))
|
|
229
253
|
usage.reasoningTokens = tokens(at(raw, 'output_tokens_details', 'reasoning_tokens'))
|
|
230
254
|
const output = at(v, 'response', 'output')
|
|
231
|
-
if (Array.isArray(output))
|
|
255
|
+
if (Array.isArray(output))
|
|
256
|
+
for (const item of output) if (at(item, 'type') === 'reasoning') this.keep(item)
|
|
232
257
|
this.done = true
|
|
233
258
|
this.truncated = kind === 'response.incomplete'
|
|
234
259
|
} else if (kind === 'response.failed') return error(at(v, 'response', 'error'))
|
package/src/protocol/request.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ImagePart,
|
|
3
|
+
Protocol,
|
|
4
|
+
ReasoningConfig,
|
|
5
|
+
ReasoningEffort,
|
|
6
|
+
} from '@ouraihub/aether-contracts'
|
|
2
7
|
import type { Message, ToolSpec } from '../types.js'
|
|
3
8
|
import { at, isRecord } from '../guard.js'
|
|
4
9
|
|
|
@@ -39,7 +44,11 @@ export interface ModelRequest {
|
|
|
39
44
|
turn?: readonly Message[]
|
|
40
45
|
}
|
|
41
46
|
|
|
42
|
-
function userContent(
|
|
47
|
+
function userContent(
|
|
48
|
+
protocol: Protocol,
|
|
49
|
+
text: string,
|
|
50
|
+
images: readonly ImagePart[] | undefined,
|
|
51
|
+
): unknown {
|
|
43
52
|
if (!images?.length) return text
|
|
44
53
|
const url = (image: ImagePart): string => `data:${image.mime};base64,${image.data}`
|
|
45
54
|
if (protocol === 'anthropic-messages')
|
|
@@ -51,8 +60,14 @@ function userContent(protocol: Protocol, text: string, images: readonly ImagePar
|
|
|
51
60
|
{ type: 'text', text },
|
|
52
61
|
]
|
|
53
62
|
if (protocol === 'openai-chat')
|
|
54
|
-
return [
|
|
55
|
-
|
|
63
|
+
return [
|
|
64
|
+
{ type: 'text', text },
|
|
65
|
+
...images.map((image) => ({ type: 'image_url', image_url: { url: url(image) } })),
|
|
66
|
+
]
|
|
67
|
+
return [
|
|
68
|
+
{ type: 'input_text', text },
|
|
69
|
+
...images.map((image) => ({ type: 'input_image', image_url: url(image) })),
|
|
70
|
+
]
|
|
56
71
|
}
|
|
57
72
|
|
|
58
73
|
export function requestBody(request: ModelRequest): Record<string, unknown> {
|
|
@@ -61,7 +76,10 @@ export function requestBody(request: ModelRequest): Record<string, unknown> {
|
|
|
61
76
|
responses = request.protocol === 'openai-responses'
|
|
62
77
|
const encode = (message: Message): void => {
|
|
63
78
|
if ('user' in message) {
|
|
64
|
-
messages.push({
|
|
79
|
+
messages.push({
|
|
80
|
+
role: 'user',
|
|
81
|
+
content: userContent(request.protocol, message.user, message.images),
|
|
82
|
+
})
|
|
65
83
|
return
|
|
66
84
|
}
|
|
67
85
|
if ('tool' in message) {
|
|
@@ -95,9 +113,12 @@ export function requestBody(request: ModelRequest): Record<string, unknown> {
|
|
|
95
113
|
block.modelId === request.model,
|
|
96
114
|
)
|
|
97
115
|
if (anthropic) {
|
|
98
|
-
const blocks = reasoning.flatMap((block) =>
|
|
116
|
+
const blocks = reasoning.flatMap((block) =>
|
|
117
|
+
block.value && typeof block.value === 'object' ? [block.value] : [],
|
|
118
|
+
)
|
|
99
119
|
if (assistant.text) blocks.push({ type: 'text', text: assistant.text })
|
|
100
|
-
for (const call of calls)
|
|
120
|
+
for (const call of calls)
|
|
121
|
+
blocks.push({ type: 'tool_use', id: call.id, name: call.name, input: call.args ?? {} })
|
|
101
122
|
if (blocks.length) messages.push({ role: 'assistant', content: blocks })
|
|
102
123
|
} else if (responses) {
|
|
103
124
|
for (const block of reasoning) if (isRecord(block.value)) messages.push(block.value)
|
|
@@ -148,7 +169,13 @@ export function requestBody(request: ModelRequest): Record<string, unknown> {
|
|
|
148
169
|
}
|
|
149
170
|
} else if (request.protocol === 'openai-chat') {
|
|
150
171
|
if (system) messages.unshift({ role: 'system', content: system })
|
|
151
|
-
body = {
|
|
172
|
+
body = {
|
|
173
|
+
model,
|
|
174
|
+
max_tokens: maxOutput,
|
|
175
|
+
stream: true,
|
|
176
|
+
stream_options: { include_usage: true },
|
|
177
|
+
messages,
|
|
178
|
+
}
|
|
152
179
|
const dialect = request.reasoning?.dialect
|
|
153
180
|
if (effort !== 'default' && dialect === 'thinking-type') {
|
|
154
181
|
body.thinking = { type: effort === 'none' ? 'disabled' : 'enabled' }
|
|
@@ -159,7 +186,9 @@ export function requestBody(request: ModelRequest): Record<string, unknown> {
|
|
|
159
186
|
} else if (effort !== 'default') body.reasoning_effort = effort
|
|
160
187
|
} else {
|
|
161
188
|
// Third-party Responses servers may reject `summary` with 400.
|
|
162
|
-
const summary = /^https:\/\/api\.openai\.com(?:\/|$)/.test(request.baseUrl)
|
|
189
|
+
const summary = /^https:\/\/api\.openai\.com(?:\/|$)/.test(request.baseUrl)
|
|
190
|
+
? { summary: 'auto' }
|
|
191
|
+
: {}
|
|
163
192
|
body = {
|
|
164
193
|
model,
|
|
165
194
|
max_output_tokens: maxOutput,
|
|
@@ -171,16 +200,22 @@ export function requestBody(request: ModelRequest): Record<string, unknown> {
|
|
|
171
200
|
...(effort === 'default' ? {} : { reasoning: { effort, ...summary } }),
|
|
172
201
|
}
|
|
173
202
|
}
|
|
174
|
-
const tools: Record<string, unknown>[] = (request.tools ?? []).map(
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
203
|
+
const tools: Record<string, unknown>[] = (request.tools ?? []).map(
|
|
204
|
+
({ name, description, parameters }) =>
|
|
205
|
+
anthropic
|
|
206
|
+
? { name, description, input_schema: parameters }
|
|
207
|
+
: responses
|
|
208
|
+
? { type: 'function', name, description, parameters }
|
|
209
|
+
: { type: 'function', function: { name, description, parameters } },
|
|
180
210
|
)
|
|
181
211
|
if (request.nativeWebSearch) {
|
|
182
|
-
if (request.protocol === 'openai-chat')
|
|
183
|
-
|
|
212
|
+
if (request.protocol === 'openai-chat')
|
|
213
|
+
throw new Error('Native search is unsupported on Chat Completions')
|
|
214
|
+
tools.push(
|
|
215
|
+
anthropic
|
|
216
|
+
? { type: 'web_search_20250305', name: 'web_search', max_uses: 5 }
|
|
217
|
+
: { type: 'web_search' },
|
|
218
|
+
)
|
|
184
219
|
}
|
|
185
220
|
if (tools.length) body.tools = tools
|
|
186
221
|
return body
|
|
@@ -56,7 +56,9 @@ export function validateUrl(field: string, raw: string): void {
|
|
|
56
56
|
)
|
|
57
57
|
throw badRequest(field, 'invalid_url')
|
|
58
58
|
}
|
|
59
|
-
export function validateHeaders(
|
|
59
|
+
export function validateHeaders(
|
|
60
|
+
input: Readonly<Record<string, string | undefined>>,
|
|
61
|
+
): Record<string, string> {
|
|
60
62
|
const entries = Object.entries(input)
|
|
61
63
|
if (entries.length > 20) throw badRequest('headers', 'too_many')
|
|
62
64
|
const headers: Record<string, string> = {}
|
|
@@ -117,10 +119,16 @@ export function validateReasoning(
|
|
|
117
119
|
if (!fits) throw badRequest('reasoning', 'protocol_mismatch')
|
|
118
120
|
const budget = config.budgetTokens
|
|
119
121
|
if (config.dialect === 'anthropic-budget') {
|
|
120
|
-
if (
|
|
122
|
+
if (
|
|
123
|
+
budget == null ||
|
|
124
|
+
!Number.isSafeInteger(budget) ||
|
|
125
|
+
budget < 1024 ||
|
|
126
|
+
budget >= (maxOutput ?? 4096)
|
|
127
|
+
)
|
|
121
128
|
throw badRequest('reasoning', 'invalid_budget')
|
|
122
129
|
} else if (config.dialect === 'qwen') {
|
|
123
|
-
if (budget != null && (!Number.isSafeInteger(budget) || budget < 1))
|
|
130
|
+
if (budget != null && (!Number.isSafeInteger(budget) || budget < 1))
|
|
131
|
+
throw badRequest('reasoning', 'invalid_budget')
|
|
124
132
|
} else if (budget != null) throw badRequest('reasoning', 'unexpected_budget')
|
|
125
133
|
}
|
|
126
134
|
export function validatePricing(pricing: ModelPricing | null, modelType: string): void {
|
|
@@ -164,7 +172,8 @@ export function validateModel(model: Model, protocol: Protocol): void {
|
|
|
164
172
|
if (value !== null && charCount(value.trim()) > 80) throw badRequest(field, 'too_long', 80)
|
|
165
173
|
if (model.nativeWebSearch && (model.modelType !== 'chat' || protocol === 'openai-chat'))
|
|
166
174
|
throw badRequest('nativeWebSearch', 'unsupported_protocol')
|
|
167
|
-
if (model.reasoning)
|
|
175
|
+
if (model.reasoning)
|
|
176
|
+
validateReasoning(model.reasoning, protocol, model.modelType, model.maxOutput)
|
|
168
177
|
validatePricing(model.pricing, model.modelType)
|
|
169
178
|
}
|
|
170
179
|
/** Prepares one immutable-by-ownership snapshot; network consumers still enforce address policy. */
|
|
@@ -207,6 +216,8 @@ export function prepare(snapshot: ConnectionSnapshot): Connection {
|
|
|
207
216
|
pricing: model?.pricing ?? null,
|
|
208
217
|
nativeWebSearch: model?.nativeWebSearch ?? false,
|
|
209
218
|
...(endpoint.imageBaseUrl?.trim() ? { imageBaseUrl: apiBase(endpoint.imageBaseUrl) } : {}),
|
|
210
|
-
...(endpoint.imageEditBaseUrl?.trim()
|
|
219
|
+
...(endpoint.imageEditBaseUrl?.trim()
|
|
220
|
+
? { imageEditBaseUrl: apiBase(endpoint.imageEditBaseUrl) }
|
|
221
|
+
: {}),
|
|
211
222
|
}
|
|
212
223
|
}
|
package/src/providers/infer.ts
CHANGED
|
@@ -71,7 +71,7 @@ export function inferModel(modelId: string): Inferred {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
// ponytail: bounded known-family rules; add a catalog only when provider
|
|
74
|
+
// ponytail: bounded known-family rules; add a catalog only when provider metadata is needed.
|
|
75
75
|
export function inferReasoning(
|
|
76
76
|
modelId: string,
|
|
77
77
|
protocol: Protocol,
|
|
@@ -96,7 +96,9 @@ export function inferReasoning(
|
|
|
96
96
|
const adaptive =
|
|
97
97
|
newest ||
|
|
98
98
|
families.some((family) => matches(`claude-${family}-5`)) ||
|
|
99
|
-
['opus', 'sonnet'].some((family) =>
|
|
99
|
+
['opus', 'sonnet'].some((family) =>
|
|
100
|
+
['6', '7', '8'].some((minor) => matches(`claude-${family}-4-${minor}`)),
|
|
101
|
+
)
|
|
100
102
|
if (adaptive)
|
|
101
103
|
return {
|
|
102
104
|
dialect: 'anthropic-adaptive',
|
|
@@ -160,11 +162,13 @@ export function inferReasoning(
|
|
|
160
162
|
return null
|
|
161
163
|
}
|
|
162
164
|
|
|
163
|
-
/** Official vendor endpoints
|
|
165
|
+
/** Official vendor endpoints use their own reasoning fields; relays keep the OpenAI dialect. */
|
|
164
166
|
function vendorReasoning(name: string, baseUrl: string): ReasoningConfig | null {
|
|
165
167
|
const host = (baseUrl.split('://').at(-1) ?? '').split(/[/:?]/)[0]?.toLowerCase() ?? ''
|
|
166
|
-
const on = (...domains: string[]): boolean =>
|
|
167
|
-
|
|
168
|
+
const on = (...domains: string[]): boolean =>
|
|
169
|
+
domains.some((domain) => host === domain || host.endsWith(`.${domain}`))
|
|
170
|
+
const starts = (...prefixes: string[]): boolean =>
|
|
171
|
+
prefixes.some((prefix) => name.startsWith(prefix))
|
|
168
172
|
const thinking = (efforts: ReasoningConfig['efforts']): ReasoningConfig => ({
|
|
169
173
|
dialect: 'thinking-type',
|
|
170
174
|
efforts,
|
|
@@ -181,7 +185,8 @@ function vendorReasoning(name: string, baseUrl: string): ReasoningConfig | null
|
|
|
181
185
|
if (on('volces.com')) return starts('doubao-seed') ? toggle : null
|
|
182
186
|
if (on('moonshot.cn', 'moonshot.ai')) {
|
|
183
187
|
if (starts('kimi-k3')) return thinking(['none', 'low', 'high', 'max'])
|
|
184
|
-
return starts('kimi-k2-5', 'kimi-k2-6', 'kimi-k2-7', 'kimi-k2-8', 'kimi-k2-9') &&
|
|
188
|
+
return starts('kimi-k2-5', 'kimi-k2-6', 'kimi-k2-7', 'kimi-k2-8', 'kimi-k2-9') &&
|
|
189
|
+
!name.includes('-code')
|
|
185
190
|
? toggle
|
|
186
191
|
: null
|
|
187
192
|
}
|
package/src/providers/probe.ts
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ProbeError,
|
|
3
|
+
ProbeErrorKind,
|
|
4
|
+
ProbeResult,
|
|
5
|
+
ProbedModel,
|
|
6
|
+
} from '@ouraihub/aether-contracts'
|
|
2
7
|
import type { OutboundHttp } from '../ports.js'
|
|
3
8
|
import { cancelled, excerpt, KitFailure } from '../error.js'
|
|
4
9
|
import { apiBase, authHeaders, prepare, type EndpointSnapshot } from './connection.js'
|
|
5
10
|
import { inferModel, inferReasoning } from './infer.js'
|
|
6
11
|
|
|
7
12
|
function at(value: unknown, name: string): unknown {
|
|
8
|
-
return value !== null && typeof value === 'object' && name in value
|
|
13
|
+
return value !== null && typeof value === 'object' && name in value
|
|
14
|
+
? Reflect.get(value, name)
|
|
15
|
+
: undefined
|
|
9
16
|
}
|
|
10
17
|
async function readBody(response: Response, limit: number): Promise<string | null> {
|
|
11
18
|
if (Number(response.headers.get('content-length') ?? 0) > limit) {
|
|
@@ -51,15 +58,21 @@ export async function probe(outbound: OutboundHttp, input: ProbeInput): Promise<
|
|
|
51
58
|
status: null,
|
|
52
59
|
message: null,
|
|
53
60
|
hint:
|
|
54
|
-
kind === 'connect' && /^\[.*\]$|^\d+\.\d+\.\d+\.\d+$/.test(hostname)
|
|
61
|
+
kind === 'connect' && /^\[.*\]$|^\d+\.\d+\.\d+\.\d+$/.test(hostname)
|
|
62
|
+
? 'providers.hint.check_network'
|
|
63
|
+
: null,
|
|
55
64
|
})
|
|
56
65
|
let hostname: string
|
|
57
66
|
try {
|
|
58
67
|
hostname = new URL(probeUrl).hostname
|
|
59
68
|
} catch {
|
|
60
|
-
throw new KitFailure('bad_request', 'Invalid probe URL', {
|
|
69
|
+
throw new KitFailure('bad_request', 'Invalid probe URL', {
|
|
70
|
+
field: 'baseUrl',
|
|
71
|
+
reason: 'invalid_url',
|
|
72
|
+
})
|
|
61
73
|
}
|
|
62
|
-
if (!connection.keys.length && !['localhost', '127.0.0.1', '[::1]'].includes(hostname))
|
|
74
|
+
if (!connection.keys.length && !['localhost', '127.0.0.1', '[::1]'].includes(hostname))
|
|
75
|
+
return failure('missing_key')
|
|
63
76
|
let timeout = AbortSignal.timeout(20_000)
|
|
64
77
|
const keys = connection.keys.length ? connection.keys : [null]
|
|
65
78
|
let used: string | null = null
|
|
@@ -157,7 +170,8 @@ export async function probe(outbound: OutboundHttp, input: ProbeInput): Promise<
|
|
|
157
170
|
} catch (error) {
|
|
158
171
|
if (input.signal.aborted) throw cancelled()
|
|
159
172
|
if (timeout.aborted) return failure('timeout')
|
|
160
|
-
if (error instanceof KitFailure && error.details?.reason === 'blocked_address')
|
|
173
|
+
if (error instanceof KitFailure && error.details?.reason === 'blocked_address')
|
|
174
|
+
return failure('blocked_address')
|
|
161
175
|
return failure('connect')
|
|
162
176
|
}
|
|
163
177
|
}
|
package/src/risk.ts
CHANGED
|
@@ -24,10 +24,16 @@ export function shellRisk(command: string): Risk | null {
|
|
|
24
24
|
['elevation', /(?:^|[\s;&|])(sudo|doas|runas)(?:\s|$)|start-process\b.*-verb\s+runas/i],
|
|
25
25
|
['delete', /(?:^|[\s;&|])(rm|rmdir|del|erase|remove-item)(?:\s|$)/i],
|
|
26
26
|
['disk', /(?:^|[\s;&|])(mkfs(?:\.\w+)?|fdisk|diskpart|format)(?:\s|$)|\bdd\b.*\bof=/i],
|
|
27
|
-
[
|
|
27
|
+
[
|
|
28
|
+
'git_destructive',
|
|
29
|
+
/\bgit\s+(?:reset\s+--hard|clean\b[^\n]*-[^\s]*f|push\b[^\n]*(?:--force|-f\b)|checkout\s+--)/i,
|
|
30
|
+
],
|
|
28
31
|
['permissions', /\b(?:chmod|chown|icacls|set-acl)\b/i],
|
|
29
32
|
['publish', /\b(?:npm|pnpm|yarn|cargo|dotnet)\s+(?:publish|nuget\s+push)\b/i],
|
|
30
|
-
[
|
|
33
|
+
[
|
|
34
|
+
'global_install',
|
|
35
|
+
/\b(?:npm|pnpm|yarn)\b[^\n]*(?:--global|\s-g\b)|\bpip\b[^\n]*\binstall\b(?![^\n]*--target)/i,
|
|
36
|
+
],
|
|
31
37
|
['home_path', /(?:^|\s)~(?:[\\/]|$)|\$(?:HOME|USERPROFILE)\b|%(?:USERPROFILE|HOMEPATH)%/i],
|
|
32
38
|
['process_spawn', /\$\(|`[^`]+`|\b(?:eval|exec|cmd\s+\/c|powershell\s+-|bash\s+-c|sh\s+-c)\b/i],
|
|
33
39
|
]
|
|
@@ -40,7 +46,11 @@ export function scriptRisk(language: ScriptLanguage, script: string): Risk | nul
|
|
|
40
46
|
return { reason: 'process_spawn' }
|
|
41
47
|
if (/\b(?:requests\.|urllib\.|httpx\.|fetch\s*\(|https?\.(?:request|get)|socket\.)/.test(script))
|
|
42
48
|
return { reason: 'network' }
|
|
43
|
-
if (
|
|
49
|
+
if (
|
|
50
|
+
/\b(?:os\.(?:remove|unlink|rmdir)|shutil\.rmtree|fs\.(?:rm|unlink|rmdir)|Deno\.remove)\b/.test(
|
|
51
|
+
script,
|
|
52
|
+
)
|
|
53
|
+
)
|
|
44
54
|
return { reason: 'file_delete' }
|
|
45
55
|
return null
|
|
46
56
|
}
|
package/src/schema.ts
CHANGED
|
@@ -121,13 +121,30 @@ export const validateArguments: ArgumentsValidator = (root, args) => {
|
|
|
121
121
|
schema.type.every((type: unknown) => typeof type === 'string' && TYPES.includes(type)))
|
|
122
122
|
)
|
|
123
123
|
return false
|
|
124
|
-
for (const key of [
|
|
124
|
+
for (const key of [
|
|
125
|
+
'minLength',
|
|
126
|
+
'maxLength',
|
|
127
|
+
'minItems',
|
|
128
|
+
'maxItems',
|
|
129
|
+
'minProperties',
|
|
130
|
+
'maxProperties',
|
|
131
|
+
])
|
|
125
132
|
if (
|
|
126
133
|
schema[key] !== undefined &&
|
|
127
|
-
!(
|
|
134
|
+
!(
|
|
135
|
+
typeof schema[key] === 'number' &&
|
|
136
|
+
Number.isSafeInteger(schema[key]) &&
|
|
137
|
+
schema[key] >= 0
|
|
138
|
+
)
|
|
128
139
|
)
|
|
129
140
|
return false
|
|
130
|
-
for (const key of [
|
|
141
|
+
for (const key of [
|
|
142
|
+
'minimum',
|
|
143
|
+
'maximum',
|
|
144
|
+
'exclusiveMinimum',
|
|
145
|
+
'exclusiveMaximum',
|
|
146
|
+
'multipleOf',
|
|
147
|
+
])
|
|
131
148
|
if (
|
|
132
149
|
schema[key] !== undefined &&
|
|
133
150
|
!(
|
|
@@ -146,13 +163,21 @@ export const validateArguments: ArgumentsValidator = (root, args) => {
|
|
|
146
163
|
)
|
|
147
164
|
)
|
|
148
165
|
return false
|
|
149
|
-
if (schema.enum !== undefined && !(Array.isArray(schema.enum) && schema.enum.length > 0))
|
|
166
|
+
if (schema.enum !== undefined && !(Array.isArray(schema.enum) && schema.enum.length > 0))
|
|
167
|
+
return false
|
|
150
168
|
if (schema.uniqueItems !== undefined && typeof schema.uniqueItems !== 'boolean') return false
|
|
151
169
|
if (schema.pattern !== undefined && !regex(schema.pattern)) return false
|
|
152
|
-
if (
|
|
170
|
+
if (
|
|
171
|
+
schema.format !== undefined &&
|
|
172
|
+
(typeof schema.format !== 'string' || !Object.hasOwn(format, schema.format))
|
|
173
|
+
)
|
|
153
174
|
return false
|
|
154
|
-
if (schema.contentEncoding !== undefined || schema.contentMediaType !== undefined)
|
|
155
|
-
|
|
175
|
+
if (schema.contentEncoding !== undefined || schema.contentMediaType !== undefined)
|
|
176
|
+
return false
|
|
177
|
+
if (
|
|
178
|
+
schema.$ref !== undefined &&
|
|
179
|
+
(typeof schema.$ref !== 'string' || !inspect(resolve(schema.$ref), depth + 1))
|
|
180
|
+
)
|
|
156
181
|
return false
|
|
157
182
|
for (const key of ['allOf', 'anyOf', 'oneOf'])
|
|
158
183
|
if (
|
|
@@ -187,7 +212,8 @@ export const validateArguments: ArgumentsValidator = (root, args) => {
|
|
|
187
212
|
if (group === undefined) continue
|
|
188
213
|
if (!isRecord(group)) return false
|
|
189
214
|
for (const [name, child] of Object.entries(group))
|
|
190
|
-
if ((key === 'patternProperties' && !regex(name)) || !inspect(child, depth + 1))
|
|
215
|
+
if ((key === 'patternProperties' && !regex(name)) || !inspect(child, depth + 1))
|
|
216
|
+
return false
|
|
191
217
|
}
|
|
192
218
|
if (schema.dependencies !== undefined) {
|
|
193
219
|
if (!isRecord(schema.dependencies)) return false
|
package/src/search/execution.ts
CHANGED
|
@@ -30,7 +30,9 @@ export async function execute<T>(
|
|
|
30
30
|
validatePreferences(config.preferences)
|
|
31
31
|
if (
|
|
32
32
|
!Array.isArray(config.apiKeys) ||
|
|
33
|
-
config.apiKeys.some(
|
|
33
|
+
config.apiKeys.some(
|
|
34
|
+
(key: unknown) => typeof key !== 'string' || !key.trim() || key.length > 4096,
|
|
35
|
+
)
|
|
34
36
|
)
|
|
35
37
|
throw new KitFailure('needs_config', 'Invalid search credentials', { field: 'keys' })
|
|
36
38
|
if (spec.keyMode === 'required' && !config.apiKeys.length)
|
|
@@ -41,7 +43,10 @@ export async function execute<T>(
|
|
|
41
43
|
const abort = () => controller.abort()
|
|
42
44
|
signal?.addEventListener('abort', abort, { once: true })
|
|
43
45
|
if (signal?.aborted) controller.abort()
|
|
44
|
-
const timer = setTimeout(
|
|
46
|
+
const timer = setTimeout(
|
|
47
|
+
abort,
|
|
48
|
+
cap === 'fetch' ? 20000 : provider.id === 'exa-mcp' ? 25000 : 15000,
|
|
49
|
+
)
|
|
45
50
|
const aborted = () =>
|
|
46
51
|
new KitFailure(
|
|
47
52
|
signal?.aborted ? 'cancelled' : 'timeout',
|
|
@@ -77,7 +82,11 @@ export async function execute<T>(
|
|
|
77
82
|
try {
|
|
78
83
|
const { promise, resolve, reject } = Promise.withResolvers<Response>()
|
|
79
84
|
const onAbort = () =>
|
|
80
|
-
reject(
|
|
85
|
+
reject(
|
|
86
|
+
controller.signal.aborted
|
|
87
|
+
? aborted()
|
|
88
|
+
: new KitFailure('timeout', 'Search connection timed out'),
|
|
89
|
+
)
|
|
81
90
|
connection.signal.addEventListener('abort', onAbort, { once: true })
|
|
82
91
|
const request: RequestInit & { credentials: 'omit' } = {
|
|
83
92
|
...init,
|
|
@@ -101,7 +110,9 @@ export async function execute<T>(
|
|
|
101
110
|
if (length !== null && /^\d+$/u.test(length) && Number(length) > limit) {
|
|
102
111
|
await response.body?.cancel().catch(() => {})
|
|
103
112
|
if (!response.ok)
|
|
104
|
-
throw new KitFailure('upstream', 'Search provider rejected request', {
|
|
113
|
+
throw new KitFailure('upstream', 'Search provider rejected request', {
|
|
114
|
+
status: response.status,
|
|
115
|
+
})
|
|
105
116
|
throw new KitFailure('upstream', 'Search response exceeds size limit', { limit })
|
|
106
117
|
}
|
|
107
118
|
const reader = response.body?.getReader()
|
|
@@ -117,7 +128,9 @@ export async function execute<T>(
|
|
|
117
128
|
if (size > limit) {
|
|
118
129
|
await reader.cancel().catch(() => {})
|
|
119
130
|
if (!response.ok)
|
|
120
|
-
throw new KitFailure('upstream', 'Search provider rejected request', {
|
|
131
|
+
throw new KitFailure('upstream', 'Search provider rejected request', {
|
|
132
|
+
status: response.status,
|
|
133
|
+
})
|
|
121
134
|
throw new KitFailure('upstream', 'Search response exceeds size limit', { limit })
|
|
122
135
|
}
|
|
123
136
|
text += decoder.decode(value, { stream: true })
|
|
@@ -130,7 +143,9 @@ export async function execute<T>(
|
|
|
130
143
|
reader?.releaseLock()
|
|
131
144
|
}
|
|
132
145
|
if (!response.ok)
|
|
133
|
-
throw new KitFailure('upstream', 'Search provider rejected request', {
|
|
146
|
+
throw new KitFailure('upstream', 'Search provider rejected request', {
|
|
147
|
+
status: response.status,
|
|
148
|
+
})
|
|
134
149
|
return text
|
|
135
150
|
},
|
|
136
151
|
}
|
package/src/search/index.ts
CHANGED
|
@@ -2,7 +2,14 @@ import type { SearchPage, SearchResult } from '@ouraihub/aether-contracts'
|
|
|
2
2
|
import { KitFailure } from '../error.js'
|
|
3
3
|
import type { OutboundHttp } from '../ports.js'
|
|
4
4
|
import { execute, type CheckTarget, type ResolvedSearchConfig } from './execution.js'
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
checkBlacklist,
|
|
7
|
+
cutoff,
|
|
8
|
+
processHits,
|
|
9
|
+
validatePreferences,
|
|
10
|
+
validateQuery,
|
|
11
|
+
validateTarget,
|
|
12
|
+
} from './policy.js'
|
|
6
13
|
import { fetchRequest, parsePage, parseSearchResponse, searchRequest } from './vendor.js'
|
|
7
14
|
|
|
8
15
|
export type { CheckTarget, ResolvedSearchConfig } from './execution.js'
|
|
@@ -33,7 +40,9 @@ export async function search(
|
|
|
33
40
|
const response = await execution.request(url, init)
|
|
34
41
|
if (response.status >= 300 && response.status < 400) {
|
|
35
42
|
await response.body?.cancel().catch(() => {})
|
|
36
|
-
throw new KitFailure('upstream', 'Search provider redirects are forbidden', {
|
|
43
|
+
throw new KitFailure('upstream', 'Search provider redirects are forbidden', {
|
|
44
|
+
status: response.status,
|
|
45
|
+
})
|
|
37
46
|
}
|
|
38
47
|
const body = await execution.body(response)
|
|
39
48
|
return {
|