@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/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 = ['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens', 'reasoningTokens'] as const
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 = pricing.tiers.findLast((candidate) => candidate.minInputTokens <= inputTokens) ?? pricing.tiers[0]
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
@@ -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 ? value : null
9
+ typeof value === 'number' && Number.isInteger(value) && value >= 0 && value <= 0xffff_ffff
10
+ ? value
11
+ : null
10
12
 
11
- const text = (value: unknown): Decoded => (typeof value === 'string' && value ? { text: value } : null)
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 = { protocol: this.protocol, endpointId: this.endpointId, modelId: this.modelId, value }
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 = id == null ? -1 : this.reasoning.findIndex((item) => at(item.value, 'id') === id)
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({ key, id: typeof id === 'string' ? id : '', name: typeof name === 'string' ? name : '', args: '' })
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) throw new KitFailure('upstream', 'Tool arguments limit exceeded')
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 = tokens(at(raw, 'cache_creation_input_tokens')) ?? 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) this.append(index, JSON.stringify(input))
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') block.signature = String(block.signature ?? '') + fragment
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 = tokens(at(raw, 'prompt_tokens_details', 'cached_tokens')) ?? 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 { reasoning: thinking, ...(typeof content === 'string' && content ? { text: content } : {}) }
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') this.keep(at(v, 'item'))
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') this.append(at(v, 'item_id'), at(v, '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)) for (const item of output) if (at(item, 'type') === 'reasoning') this.keep(item)
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'))
@@ -1,4 +1,9 @@
1
- import type { ImagePart, Protocol, ReasoningConfig, ReasoningEffort } from '@ouraihub/aether-contracts'
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(protocol: Protocol, text: string, images: readonly ImagePart[] | undefined): unknown {
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 [{ type: 'text', text }, ...images.map((image) => ({ type: 'image_url', image_url: { url: url(image) } }))]
55
- return [{ type: 'input_text', text }, ...images.map((image) => ({ type: 'input_image', image_url: url(image) }))]
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({ role: 'user', content: userContent(request.protocol, message.user, message.images) })
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) => (block.value && typeof block.value === 'object' ? [block.value] : []))
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) blocks.push({ type: 'tool_use', id: call.id, name: call.name, input: call.args ?? {} })
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 = { model, max_tokens: maxOutput, stream: true, stream_options: { include_usage: true }, messages }
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) ? { summary: 'auto' } : {}
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(({ name, description, parameters }) =>
175
- anthropic
176
- ? { name, description, input_schema: parameters }
177
- : responses
178
- ? { type: 'function', name, description, parameters }
179
- : { type: 'function', function: { name, description, parameters } },
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') throw new Error('Native search is unsupported on Chat Completions')
183
- tools.push(anthropic ? { type: 'web_search_20250305', name: 'web_search', max_uses: 5 } : { type: 'web_search' })
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(input: Readonly<Record<string, string | undefined>>): Record<string, string> {
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 (budget == null || !Number.isSafeInteger(budget) || budget < 1024 || budget >= (maxOutput ?? 4096))
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)) throw badRequest('reasoning', 'invalid_budget')
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) validateReasoning(model.reasoning, protocol, model.modelType, model.maxOutput)
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() ? { imageEditBaseUrl: apiBase(endpoint.imageEditBaseUrl) } : {}),
219
+ ...(endpoint.imageEditBaseUrl?.trim()
220
+ ? { imageEditBaseUrl: apiBase(endpoint.imageEditBaseUrl) }
221
+ : {}),
211
222
  }
212
223
  }
@@ -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-specific metadata is needed.
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) => ['6', '7', '8'].some((minor) => matches(`claude-${family}-4-${minor}`)))
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 switch reasoning with their own fields (docs/28 RT4); a relay serving the same model keeps the OpenAI dialect. */
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 => domains.some((domain) => host === domain || host.endsWith(`.${domain}`))
167
- const starts = (...prefixes: string[]): boolean => prefixes.some((prefix) => name.startsWith(prefix))
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') && !name.includes('-code')
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
  }
@@ -1,11 +1,18 @@
1
- import type { ProbeError, ProbeErrorKind, ProbeResult, ProbedModel } from '@ouraihub/aether-contracts'
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 ? Reflect.get(value, name) : undefined
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) ? 'providers.hint.check_network' : null,
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', { field: 'baseUrl', reason: 'invalid_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)) return failure('missing_key')
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') return failure('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
- ['git_destructive', /\bgit\s+(?:reset\s+--hard|clean\b[^\n]*-[^\s]*f|push\b[^\n]*(?:--force|-f\b)|checkout\s+--)/i],
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
- ['global_install', /\b(?:npm|pnpm|yarn)\b[^\n]*(?:--global|\s-g\b)|\bpip\b[^\n]*\binstall\b(?![^\n]*--target)/i],
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 (/\b(?:os\.(?:remove|unlink|rmdir)|shutil\.rmtree|fs\.(?:rm|unlink|rmdir)|Deno\.remove)\b/.test(script))
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 ['minLength', 'maxLength', 'minItems', 'maxItems', 'minProperties', 'maxProperties'])
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
- !(typeof schema[key] === 'number' && Number.isSafeInteger(schema[key]) && schema[key] >= 0)
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 ['minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf'])
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)) return false
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 (schema.format !== undefined && (typeof schema.format !== 'string' || !Object.hasOwn(format, schema.format)))
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) return false
155
- if (schema.$ref !== undefined && (typeof schema.$ref !== 'string' || !inspect(resolve(schema.$ref), depth + 1)))
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)) return false
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
@@ -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((key: unknown) => typeof key !== 'string' || !key.trim() || key.length > 4096)
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(abort, cap === 'fetch' ? 20000 : provider.id === 'exa-mcp' ? 25000 : 15000)
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(controller.signal.aborted ? aborted() : new KitFailure('timeout', 'Search connection timed out'))
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', { status: response.status })
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', { status: response.status })
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', { status: response.status })
146
+ throw new KitFailure('upstream', 'Search provider rejected request', {
147
+ status: response.status,
148
+ })
134
149
  return text
135
150
  },
136
151
  }
@@ -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 { checkBlacklist, cutoff, processHits, validatePreferences, validateQuery, validateTarget } from './policy.js'
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', { status: response.status })
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 {