233 lines
6.8 KiB
JavaScript
233 lines
6.8 KiB
JavaScript
/** OpenAI-compatible chat/completions HTTP + SSE (preamble for /bin/agent). */
|
|
|
|
/**
|
|
* @param {Record<string, unknown>} ctx
|
|
* @returns {typeof fetch | null}
|
|
*/
|
|
function bareAgentResolveFetch(ctx) {
|
|
if (typeof ctx.httpFetch === 'function')
|
|
return /** @type {typeof fetch} */ (ctx.httpFetch.bind(ctx))
|
|
const bare = ctx.bare && typeof ctx.bare === 'object' ? ctx.bare : null
|
|
let f =
|
|
bare && typeof bare.fetch === 'function'
|
|
? bare.fetch
|
|
: bare &&
|
|
bare.default &&
|
|
typeof bare.default === 'object' &&
|
|
typeof bare.default.fetch === 'function'
|
|
? bare.default.fetch
|
|
: null
|
|
if (typeof f === 'function') return /** @type {typeof fetch} */ (f.bind(bare))
|
|
if (typeof globalThis.fetch === 'function')
|
|
return globalThis.fetch.bind(globalThis)
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* @param {string} base
|
|
*/
|
|
function bareAgentNormalizeBaseUrl(base) {
|
|
let s = String(base || '').trim()
|
|
while (s.endsWith('/')) s = s.slice(0, -1)
|
|
return s
|
|
}
|
|
|
|
/**
|
|
* @param {Record<string, unknown>} obj
|
|
* @param {string} key
|
|
*/
|
|
function bareAgentDeepGet(obj, key) {
|
|
const parts = key.split('.')
|
|
let cur = obj
|
|
for (const p of parts) {
|
|
if (cur == null || typeof cur !== 'object') return undefined
|
|
cur = /** @type {Record<string, unknown>} */ (cur)[p]
|
|
}
|
|
return cur
|
|
}
|
|
|
|
/**
|
|
* Stream chat/completions; invoke onEvent for each parsed chunk.
|
|
* @param {{
|
|
* fetchFn: typeof fetch,
|
|
* url: string,
|
|
* headers: Record<string, string>,
|
|
* body: Record<string, unknown>,
|
|
* signal?: AbortSignal | null,
|
|
* onEvent: (ev: Record<string, unknown>) => void
|
|
* }} opts
|
|
*/
|
|
async function bareAgentStreamChatCompletions(opts) {
|
|
const { fetchFn, url, headers, body, signal, onEvent } = opts
|
|
const res = await fetchFn(url, {
|
|
method: 'POST',
|
|
headers,
|
|
body: JSON.stringify(body),
|
|
signal: signal || undefined
|
|
})
|
|
if (!res.ok) {
|
|
let errText = ''
|
|
try {
|
|
errText = await res.text()
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
throw new Error('HTTP ' + res.status + ' ' + errText.slice(0, 800))
|
|
}
|
|
const stream = res.body
|
|
if (!stream || typeof stream.getReader !== 'function') {
|
|
throw new Error('agent: response body is not a readable stream')
|
|
}
|
|
const reader = stream.getReader()
|
|
const dec = new TextDecoder()
|
|
let buf = ''
|
|
let emittedShape = false
|
|
try {
|
|
for (;;) {
|
|
const { done, value } = await reader.read()
|
|
if (done) break
|
|
buf += dec.decode(value, { stream: true })
|
|
const sp = bareAgentSplitSseLines(buf)
|
|
buf = sp.rest
|
|
for (const line of sp.lines) {
|
|
if (!line.trim()) continue
|
|
if (line.startsWith(':')) continue
|
|
if (!line.startsWith('data:')) continue
|
|
const payload = line.slice(5).replace(/^\s/, '')
|
|
const parsed = bareAgentParseSseDataPayload(payload)
|
|
if (parsed.kind === 'done') {
|
|
onEvent({ type: 'sse_done' })
|
|
continue
|
|
}
|
|
if (parsed.kind !== 'json' || !parsed.value || typeof parsed.value !== 'object')
|
|
continue
|
|
const j = /** @type {Record<string, unknown>} */ (parsed.value)
|
|
if (!emittedShape) {
|
|
emittedShape = true
|
|
onEvent({
|
|
type: 'response_shape_keys',
|
|
keys: Object.keys(j).slice(0, 24)
|
|
})
|
|
}
|
|
if (typeof j.type === 'string') {
|
|
if (j.type === 'response.reasoning_summary_text.delta' && typeof j.delta === 'string') {
|
|
onEvent({ type: 'delta_reasoning', reasoning: j.delta })
|
|
}
|
|
if (j.type === 'response.output_text.delta' && typeof j.delta === 'string') {
|
|
onEvent({ type: 'delta_content', content: j.delta })
|
|
}
|
|
if (
|
|
j.type === 'response.function_call_arguments.delta' &&
|
|
typeof j.delta === 'string'
|
|
) {
|
|
onEvent({
|
|
type: 'delta_tool_calls',
|
|
tool_calls: [{ index: 0, function: { arguments: j.delta } }]
|
|
})
|
|
}
|
|
}
|
|
const choices = bareAgentDeepGet(j, 'choices')
|
|
const ch0 =
|
|
Array.isArray(choices) && choices[0] && typeof choices[0] === 'object'
|
|
? /** @type {Record<string, unknown>} */ (choices[0])
|
|
: null
|
|
const delta =
|
|
ch0 && typeof ch0.delta === 'object'
|
|
? /** @type {Record<string, unknown>} */ (ch0.delta)
|
|
: null
|
|
const finishReason =
|
|
typeof ch0?.finish_reason === 'string' ? ch0.finish_reason : ''
|
|
const usage =
|
|
typeof j.usage === 'object' && j.usage ? j.usage : undefined
|
|
|
|
if (usage) {
|
|
onEvent({ type: 'usage', usage })
|
|
}
|
|
|
|
if (delta) {
|
|
const c = delta.content
|
|
if (typeof c === 'string' && c.length) {
|
|
onEvent({
|
|
type: 'delta_content',
|
|
content: c
|
|
})
|
|
}
|
|
const toolCalls = delta.tool_calls
|
|
if (toolCalls !== undefined)
|
|
onEvent({
|
|
type: 'delta_tool_calls',
|
|
tool_calls: toolCalls
|
|
})
|
|
const rc = delta.reasoning_content
|
|
if (typeof rc === 'string' && rc.length) {
|
|
onEvent({
|
|
type: 'delta_reasoning',
|
|
reasoning: rc
|
|
})
|
|
}
|
|
const r = delta.reasoning
|
|
if (typeof r === 'string' && r.length) {
|
|
onEvent({
|
|
type: 'delta_reasoning',
|
|
reasoning: r
|
|
})
|
|
} else if (Array.isArray(r)) {
|
|
for (const chunk of r) {
|
|
if (!chunk || typeof chunk !== 'object') continue
|
|
const ro = /** @type {Record<string, unknown>} */ (chunk)
|
|
const tx =
|
|
typeof ro.text === 'string'
|
|
? ro.text
|
|
: typeof ro.content === 'string'
|
|
? ro.content
|
|
: ''
|
|
if (tx) {
|
|
onEvent({
|
|
type: 'delta_reasoning',
|
|
reasoning: tx
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (finishReason)
|
|
onEvent({
|
|
type: 'finish_reason',
|
|
finish_reason: finishReason
|
|
})
|
|
}
|
|
}
|
|
} finally {
|
|
try {
|
|
reader.releaseLock()
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Non-streaming completion (same endpoint, stream:false).
|
|
*/
|
|
async function bareAgentCompleteOnce(opts) {
|
|
const { fetchFn, url, headers, body, signal } = opts
|
|
const res = await fetchFn(url, {
|
|
method: 'POST',
|
|
headers,
|
|
body: JSON.stringify({ ...body, stream: false }),
|
|
signal: signal || undefined
|
|
})
|
|
if (!res.ok) {
|
|
let errText = ''
|
|
try {
|
|
errText = await res.text()
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
throw new Error('HTTP ' + res.status + ' ' + errText.slice(0, 800))
|
|
}
|
|
const j = /** @type {Record<string, unknown>} */ (await res.json())
|
|
return j
|
|
}
|