freddie 0.0.97 → 0.0.99
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "freddie",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.99",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Open JS agent harness built on pi-mono, floosie, xstate, and anentrypoint-design",
|
|
6
6
|
"bin": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"xstate": "^5.31.0",
|
|
28
28
|
"zod": "^4.0.0",
|
|
29
29
|
"anentrypoint-design": "^0.0.94",
|
|
30
|
-
"acptoapi": "^1.0.
|
|
30
|
+
"acptoapi": "^1.0.61"
|
|
31
31
|
},
|
|
32
32
|
"optionalDependencies": {
|
|
33
33
|
"@libsql/darwin-arm64": "0.3.19",
|
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createRequire } from 'module'
|
|
2
|
+
import { getConfigValue } from '../../src/config.js'
|
|
3
|
+
import { MATRIX_FILE } from '../../src/agent/model-matrix.js'
|
|
2
4
|
import { flattenForOpenAI } from '../../src/agent/model-discovery.js'
|
|
3
5
|
import { logger } from '../../src/observability/log.js'
|
|
4
6
|
|
|
7
|
+
const _require = createRequire(import.meta.url)
|
|
8
|
+
const sdk = _require('acptoapi')
|
|
5
9
|
const log = logger('gui-llm-passthrough')
|
|
6
10
|
|
|
11
|
+
function matrixSource() { return process.env.FREDDIE_MATRIX_URL || MATRIX_FILE }
|
|
12
|
+
|
|
7
13
|
export default {
|
|
8
14
|
name: 'gui-llm-passthrough', surfaces: 'gui',
|
|
9
15
|
register({ gui }) {
|
|
10
|
-
gui.route('GET', '/v1/models', (_, res) => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
gui.route('GET', '/v1/models', async (_, res) => {
|
|
17
|
+
try {
|
|
18
|
+
const rows = await sdk.listAllModelsAndQueues({ queuesMap: getConfigValue('agent.model_queues', {}) || {}, matrixSource: matrixSource() })
|
|
19
|
+
const local = flattenForOpenAI()
|
|
20
|
+
const seen = new Set(rows.map(r => r.id))
|
|
21
|
+
for (const r of local) if (!seen.has(r.id)) rows.push(r)
|
|
22
|
+
if (rows.length === 0) rows.push({ id: 'freddie/auto', object: 'model', created: Math.floor(Date.now() / 1000), owned_by: 'freddie' })
|
|
23
|
+
res.json({ object: 'list', data: rows })
|
|
24
|
+
} catch (e) { log.error('list-models-failed', { error: String(e.message || e) }); res.json({ object: 'list', data: flattenForOpenAI() }) }
|
|
14
25
|
})
|
|
15
26
|
gui.route('POST', '/v1/chat/completions', async (req, res) => {
|
|
16
27
|
const { model, messages, tools, stream } = req.body || {}
|
|
17
28
|
if (!Array.isArray(messages) || messages.length === 0) return res.status(400).json({ error: { message: 'messages required' } })
|
|
18
29
|
try {
|
|
19
|
-
|
|
20
|
-
if (typeof model === 'string' && model.includes('/')) {
|
|
21
|
-
const idx = model.indexOf('/'); provider = model.slice(0, idx); mdl = model.slice(idx + 1)
|
|
22
|
-
}
|
|
23
|
-
const call = resolveCallLLM({ provider, model: mdl })
|
|
24
|
-
const out = await call({ messages, tools: tools?.map(t => ({ name: t.function?.name, description: t.function?.description, parameters: t.function?.parameters })) || [], model: mdl })
|
|
25
|
-
const id = 'chatcmpl-' + Math.random().toString(36).slice(2, 12)
|
|
26
|
-
const created = Math.floor(Date.now() / 1000)
|
|
27
|
-
const choice = { index: 0, message: { role: 'assistant', content: out.content || '', ...(out.tool_calls?.length ? { tool_calls: out.tool_calls.map(tc => ({ id: tc.id, type: 'function', function: { name: tc.name, arguments: JSON.stringify(tc.arguments || {}) } })) } : {}) }, finish_reason: 'stop' }
|
|
30
|
+
const out = await sdk.chat({ model: model || 'freddie/auto', messages, tools, queuesMap: getConfigValue('agent.model_queues', {}) || {}, matrixSource: matrixSource(), output: 'openai' })
|
|
28
31
|
if (stream) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
const id = 'chatcmpl-' + Math.random().toString(36).slice(2, 12)
|
|
33
|
+
const created = Math.floor(Date.now() / 1000)
|
|
34
|
+
const content = out?.choices?.[0]?.message?.content || ''
|
|
35
|
+
res.setHeader('content-type', 'text/event-stream'); res.setHeader('cache-control', 'no-cache')
|
|
36
|
+
res.write(`data: ${JSON.stringify({ id, object: 'chat.completion.chunk', created, model: model || 'freddie/auto', choices: [{ index: 0, delta: { role: 'assistant', content }, finish_reason: null }] })}\n\n`)
|
|
32
37
|
res.write(`data: ${JSON.stringify({ id, object: 'chat.completion.chunk', created, model: model || 'freddie/auto', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] })}\n\n`)
|
|
33
|
-
res.write('data: [DONE]\n\n')
|
|
34
|
-
res.end()
|
|
35
|
-
return
|
|
38
|
+
res.write('data: [DONE]\n\n'); res.end(); return
|
|
36
39
|
}
|
|
37
|
-
res.json(
|
|
38
|
-
} catch (e) {
|
|
39
|
-
log.error('chat-completions-failed', { error: String(e.message || e) })
|
|
40
|
-
res.status(500).json({ error: { message: String(e.message || e), type: 'upstream_error' } })
|
|
41
|
-
}
|
|
40
|
+
res.json(out)
|
|
41
|
+
} catch (e) { log.error('chat-completions-failed', { error: String(e.message || e) }); res.status(500).json({ error: { message: String(e.message || e), type: 'upstream_error' } }) }
|
|
42
42
|
})
|
|
43
43
|
},
|
|
44
44
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { createRequire } from 'module'
|
|
1
2
|
import { discoverAndPersist, listKnownProviders } from '../../src/agent/model-discovery.js'
|
|
2
|
-
import { PROVIDER_KEYS, DEFAULTS } from '../../src/agent/llm_resolver.js'
|
|
3
3
|
import { getConfigValue, saveConfigValue } from '../../src/config.js'
|
|
4
|
-
import {
|
|
4
|
+
import { MATRIX_FILE } from '../../src/agent/model-matrix.js'
|
|
5
5
|
import fs from 'node:fs'
|
|
6
6
|
import path from 'node:path'
|
|
7
7
|
import { spawn } from 'node:child_process'
|
|
8
|
+
const _require = createRequire(import.meta.url)
|
|
9
|
+
const { PROVIDER_KEYS, PROVIDER_DEFAULTS: DEFAULTS, getStatus, peekStatus, listAllQueues } = _require('acptoapi')
|
|
8
10
|
|
|
9
11
|
const MATRIX_PATH = path.resolve(new URL('.', import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1'), '..', '..', '.gm', 'model-availability.json')
|
|
10
12
|
let _rebuildInFlight = null
|
|
@@ -18,7 +20,19 @@ export default {
|
|
|
18
20
|
try { const provider = req.body?.provider; const result = await discoverAndPersist({ provider }); res.json(result) }
|
|
19
21
|
catch (e) { res.status(500).json({ error: String(e.message || e) }) }
|
|
20
22
|
})
|
|
21
|
-
gui.route('GET', '/api/models/queues', (_, res) =>
|
|
23
|
+
gui.route('GET', '/api/models/queues', (_, res) => {
|
|
24
|
+
const local = getConfigValue('agent.model_queues', {}) || {}
|
|
25
|
+
try {
|
|
26
|
+
const all = listAllQueues({ queuesMap: local })
|
|
27
|
+
const merged = { ...local }
|
|
28
|
+
for (const q of all) if (!merged[q.name]) merged[q.name] = q.links.map(m => ({ model: m, source: q.source }))
|
|
29
|
+
res.json(merged)
|
|
30
|
+
} catch { res.json(local) }
|
|
31
|
+
})
|
|
32
|
+
gui.route('GET', '/api/models/sampler/peek/:provider', (req, res) => {
|
|
33
|
+
try { res.json(peekStatus(req.params.provider)) }
|
|
34
|
+
catch (e) { res.status(500).json({ error: String(e.message || e) }) }
|
|
35
|
+
})
|
|
22
36
|
gui.route('POST', '/api/models/queues', (req, res) => {
|
|
23
37
|
const { name, entries } = req.body || {}
|
|
24
38
|
if (!name || !Array.isArray(entries)) return res.status(400).json({ error: 'name and entries[] required' })
|
|
@@ -2,11 +2,9 @@ import { createRequire } from 'module'
|
|
|
2
2
|
import { listAllProfiles } from '../../src/commands/profile.js'
|
|
3
3
|
import { COMMAND_REGISTRY } from '../../src/commands/registry.js'
|
|
4
4
|
import { getFreddieHome } from '../../src/home.js'
|
|
5
|
-
import { PROVIDER_KEYS, DEFAULTS } from '../../src/agent/llm_resolver.js'
|
|
6
|
-
import { getStatus } from '../../src/agent/model-sampler.js'
|
|
7
5
|
import { resolveKey } from '../../src/agent/credential_sources.js'
|
|
8
6
|
const _require = createRequire(import.meta.url)
|
|
9
|
-
const { probeModels, getCachedModels } = _require('acptoapi')
|
|
7
|
+
const { probeModels, getCachedModels, PROVIDER_KEYS, PROVIDER_DEFAULTS: DEFAULTS, getStatus, peekStatus } = _require('acptoapi')
|
|
10
8
|
export default {
|
|
11
9
|
name: 'gui-profiles-commands-health', surfaces: 'gui',
|
|
12
10
|
register({ gui }) {
|
|
@@ -1,198 +1,60 @@
|
|
|
1
1
|
import { createRequire } from 'module'
|
|
2
|
-
import { callLLM as acptoapiCall, isReachable as acptoapiReachable } from './acptoapi-bridge.js'
|
|
3
|
-
import { isAvailable, markFailed, getStatus } from './model-sampler.js'
|
|
4
2
|
import { getConfigValue } from '../config.js'
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
3
|
+
import { MATRIX_FILE } from './model-matrix.js'
|
|
4
|
+
import { callLLM as bridgeCall, isReachable as bridgeReachable } from './acptoapi-bridge.js'
|
|
7
5
|
export { matrixUsable } from './model-matrix.js'
|
|
8
6
|
|
|
9
7
|
const _require = createRequire(import.meta.url)
|
|
10
8
|
const sdk = _require('acptoapi')
|
|
11
|
-
const { streamClaude, CLAUDE_DEFAULT } = _require('acptoapi/lib/claude-client')
|
|
12
|
-
|
|
13
9
|
export const PROVIDER_KEYS = sdk.PROVIDER_KEYS
|
|
14
10
|
export const DEFAULTS = sdk.PROVIDER_DEFAULTS
|
|
15
11
|
|
|
16
|
-
const
|
|
17
|
-
kilo: { base: 'http://localhost:4780', providerID: 'kilo', defaultModel: 'openrouter/free' },
|
|
18
|
-
opencode: { base: 'http://localhost:4790', providerID: 'opencode', defaultModel: 'minimax-m2.5-free' },
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function claudeCliChat(model, input) {
|
|
22
|
-
const userMsg = input.messages.filter(m => m.role === 'user').slice(-1)[0]?.content
|
|
23
|
-
const systemMsg = input.messages.filter(m => m.role === 'system').map(m => m.content).join('\n\n') || undefined
|
|
24
|
-
const prompt = typeof userMsg === 'string' ? userMsg : JSON.stringify(userMsg || '')
|
|
25
|
-
let content = ''; const ctrl = new AbortController(); const t = setTimeout(() => ctrl.abort(), 120000)
|
|
26
|
-
try {
|
|
27
|
-
for await (const ev of streamClaude({ prompt, model: model || CLAUDE_DEFAULT, systemPrompt: systemMsg, signal: ctrl.signal })) {
|
|
28
|
-
if (ev.type === 'assistant' && Array.isArray(ev.message?.content)) for (const part of ev.message.content) { if (part.type === 'text' && part.text) content += part.text }
|
|
29
|
-
if (ev.type === 'result' && typeof ev.result === 'string') content = ev.result
|
|
30
|
-
}
|
|
31
|
-
} finally { clearTimeout(t) }
|
|
32
|
-
return { content: content.trim(), tool_calls: [], raw: { provider: 'claude-cli', model } }
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function acpChat(prefix, model, input) {
|
|
36
|
-
const b = ACP_BACKENDS[prefix]; if (!b) throw new Error(`unknown acp backend: ${prefix}`)
|
|
37
|
-
const sessRes = await fetch(`${b.base}/session`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}', signal: AbortSignal.timeout(5000) })
|
|
38
|
-
if (!sessRes.ok) throw new Error(`ACP ${prefix} /session ${sessRes.status}`)
|
|
39
|
-
const sessionId = (await sessRes.json()).id
|
|
40
|
-
const userMsg = input.messages.filter(m => m.role === 'user').slice(-1)[0]?.content || ''
|
|
41
|
-
const body = { parts: [{ type: 'text', text: String(userMsg) }], model: { providerID: b.providerID, modelID: model || b.defaultModel } }
|
|
42
|
-
const evRes = await fetch(`${b.base}/event`, { method: 'GET', signal: AbortSignal.timeout(120000) })
|
|
43
|
-
if (!evRes.ok) throw new Error(`ACP ${prefix} /event ${evRes.status}`)
|
|
44
|
-
const msgRes = await fetch(`${b.base}/session/${sessionId}/message`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), signal: AbortSignal.timeout(120000) })
|
|
45
|
-
if (!msgRes.ok) throw new Error(`ACP ${prefix} /message ${msgRes.status}: ${(await msgRes.text()).slice(0,200)}`)
|
|
46
|
-
let content = ''; let sawAssistantText = false
|
|
47
|
-
const reader = evRes.body.getReader(); const dec = new TextDecoder(); let buf = ''
|
|
48
|
-
while (true) {
|
|
49
|
-
const { value, done } = await reader.read(); if (done) break
|
|
50
|
-
buf += dec.decode(value, { stream: true }); let idx
|
|
51
|
-
while ((idx = buf.indexOf('\n\n')) >= 0) {
|
|
52
|
-
const raw = buf.slice(0, idx); buf = buf.slice(idx + 2)
|
|
53
|
-
if (!raw.startsWith('data: ')) continue
|
|
54
|
-
try { const ev = JSON.parse(raw.slice(6))
|
|
55
|
-
if (ev.properties?.sessionID && ev.properties.sessionID !== sessionId) continue
|
|
56
|
-
if (ev.type === 'message.part.updated' && ev.properties?.part?.type === 'text' && ev.properties.part.text) { content = ev.properties.part.text; sawAssistantText = true }
|
|
57
|
-
if (ev.type === 'session.error') throw new Error(`ACP ${prefix} session.error: ${JSON.stringify(ev.properties?.error || {}).slice(0,200)}`)
|
|
58
|
-
if (ev.type === 'session.idle') return { content: content.trim(), tool_calls: [], raw: { provider: prefix, model } }
|
|
59
|
-
} catch (e) { if (/session.error/.test(e.message)) throw e }
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return { content: content.trim(), tool_calls: [], raw: { provider: prefix, model } }
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function toOpenAITools(schemas) {
|
|
66
|
-
if (!schemas?.length) return undefined
|
|
67
|
-
return schemas.map(s => ({ type: 'function', function: { name: s.name, description: s.description || '', parameters: s.parameters || { type: 'object', properties: {} } } }))
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function toOpenAIMessages(messages) {
|
|
71
|
-
return messages.map(m => {
|
|
72
|
-
if (m.role === 'assistant' && Array.isArray(m.tool_calls) && m.tool_calls.length) {
|
|
73
|
-
return { role: 'assistant', content: m.content || '', tool_calls: m.tool_calls.map(tc => ({ id: tc.id, type: 'function', function: { name: tc.name || tc.function?.name, arguments: typeof (tc.arguments || tc.function?.arguments) === 'string' ? (tc.arguments || tc.function?.arguments) : JSON.stringify(tc.arguments || tc.function?.arguments || {}) } })) }
|
|
74
|
-
}
|
|
75
|
-
if (m.role === 'tool') return { role: 'tool', tool_call_id: m.tool_call_id, content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content) }
|
|
76
|
-
return m
|
|
77
|
-
})
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async function directOpenAICompatChat(url, apiKey, model, messages, tools) {
|
|
81
|
-
const body = { model, messages: toOpenAIMessages(messages), ...(tools?.length ? { tools } : {}) }
|
|
82
|
-
const res = await fetch(url, {
|
|
83
|
-
method: 'POST',
|
|
84
|
-
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
|
|
85
|
-
body: JSON.stringify(body),
|
|
86
|
-
signal: AbortSignal.timeout(120000),
|
|
87
|
-
})
|
|
88
|
-
if (!res.ok) { const t = await res.text(); throw new Error(`${res.status} ${t.slice(0, 200)}`) }
|
|
89
|
-
return res.json()
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async function sdkChat(provider, model, input) {
|
|
93
|
-
if (provider === 'claude-cli') return await claudeCliChat(model, input)
|
|
94
|
-
if (provider === 'kilo' || provider === 'opencode') return await acpChat(provider, model, input)
|
|
95
|
-
const { resolveModel } = sdk
|
|
96
|
-
const r = resolveModel(`${provider}/${model}`)
|
|
97
|
-
const resolved = await resolveKey(provider).catch(() => ({ value: null }))
|
|
98
|
-
const apiKey = resolved.value || (r.env ? process.env[r.env] : undefined)
|
|
99
|
-
const openaiTools = toOpenAITools(input.tools)
|
|
100
|
-
let result
|
|
101
|
-
if (r.provider === 'openai-compat') {
|
|
102
|
-
result = await directOpenAICompatChat(r.url, apiKey, r.model, input.messages, openaiTools)
|
|
103
|
-
} else {
|
|
104
|
-
const { buffer: sdkBuffer } = sdk
|
|
105
|
-
result = await sdkBuffer({ from: null, to: 'openai', provider: r.provider, model: r.model, messages: toOpenAIMessages(input.messages), apiKey, ...(openaiTools ? { tools: openaiTools } : {}) })
|
|
106
|
-
}
|
|
107
|
-
const choice = result?.choices?.[0]?.message || {}
|
|
108
|
-
const content = typeof choice.content === 'string' ? choice.content : ''
|
|
109
|
-
const tool_calls = Array.isArray(choice.tool_calls)
|
|
110
|
-
? choice.tool_calls.map(tc => ({ id: tc.id, name: tc.function?.name, arguments: tryParseJson(tc.function?.arguments) }))
|
|
111
|
-
: []
|
|
112
|
-
return { content, tool_calls, raw: result }
|
|
113
|
-
}
|
|
12
|
+
const toTools = s => s?.length ? s.map(t => ({ type: 'function', function: { name: t.name, description: t.description || '', parameters: t.parameters || { type: 'object', properties: {} } } })) : undefined
|
|
114
13
|
|
|
115
|
-
|
|
14
|
+
const toMsgs = ms => ms.map(m => {
|
|
15
|
+
if (m.role === 'assistant' && Array.isArray(m.tool_calls) && m.tool_calls.length) return { role: 'assistant', content: m.content || '', tool_calls: m.tool_calls.map(tc => ({ id: tc.id, type: 'function', function: { name: tc.name || tc.function?.name, arguments: typeof (tc.arguments || tc.function?.arguments) === 'string' ? (tc.arguments || tc.function?.arguments) : JSON.stringify(tc.arguments || tc.function?.arguments || {}) } })) }
|
|
16
|
+
if (m.role === 'tool') return { role: 'tool', tool_call_id: m.tool_call_id, content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content) }
|
|
17
|
+
return m
|
|
18
|
+
})
|
|
116
19
|
|
|
117
|
-
|
|
118
|
-
if (provider === 'claude-cli' || provider === 'kilo' || provider === 'opencode') return true
|
|
119
|
-
const resolved = await resolveKey(provider).catch(() => ({ value: null }))
|
|
120
|
-
if (!resolved.value) return false
|
|
121
|
-
if (provider === 'cloudflare' && !process.env.CLOUDFLARE_ACCOUNT_ID) return false
|
|
122
|
-
return true
|
|
123
|
-
}
|
|
20
|
+
const tryJson = s => { try { return typeof s === 'string' ? JSON.parse(s) : (s || {}) } catch { return {} } }
|
|
124
21
|
|
|
125
|
-
function
|
|
126
|
-
|
|
22
|
+
function adapt(result) {
|
|
23
|
+
const c = result?.choices?.[0]?.message || {}
|
|
24
|
+
return { content: typeof c.content === 'string' ? c.content : '', tool_calls: Array.isArray(c.tool_calls) ? c.tool_calls.map(tc => ({ id: tc.id, name: tc.function?.name, arguments: tryJson(tc.function?.arguments) })) : [], raw: result }
|
|
127
25
|
}
|
|
128
26
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
27
|
+
function buildModel({ provider, model, inputModel }) {
|
|
28
|
+
if (provider) return `${provider}/${model || DEFAULTS[provider] || ''}`.replace(/\/$/, '')
|
|
29
|
+
if (model) return model
|
|
30
|
+
if (inputModel) return inputModel
|
|
31
|
+
const pref = getConfigValue('agent.model_preference', [])
|
|
32
|
+
if (Array.isArray(pref) && pref.length) {
|
|
33
|
+
const links = pref.map(p => `${p.provider}/${p.model || DEFAULTS[p.provider] || ''}`.replace(/\/$/, '')).filter(s => s.includes('/'))
|
|
34
|
+
if (links.length) return links.join(', ')
|
|
135
35
|
}
|
|
136
|
-
|
|
137
|
-
|
|
36
|
+
const auto = sdk.buildAutoChain(undefined)
|
|
37
|
+
const keyed = Array.isArray(auto) ? auto.filter(l => { const p = l.model.split('/')[0]; const env = PROVIDER_KEYS[p]; return env && process.env[env] }) : []
|
|
38
|
+
if (keyed.length) return keyed.map(l => l.model).join(', ')
|
|
39
|
+
return null
|
|
138
40
|
}
|
|
139
41
|
|
|
140
42
|
export function resolveCallLLM({ provider, model } = {}) {
|
|
141
43
|
return async (input) => {
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return await
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const status = getStatus().map(s => `${s.provider}(ok=${s.ok},fails=${s.failCount})`).join(', ')
|
|
157
|
-
throw new Error(`provider ${explicitProvider} is in backoff | sampler: ${status}`)
|
|
158
|
-
}
|
|
159
|
-
try {
|
|
160
|
-
return await sdkChat(explicitProvider, m, input)
|
|
161
|
-
} catch (e) {
|
|
162
|
-
markFailed(explicitProvider)
|
|
163
|
-
throw e
|
|
164
|
-
}
|
|
44
|
+
const m = buildModel({ provider, model, inputModel: input.model })
|
|
45
|
+
if (!m) {
|
|
46
|
+
const status = sdk.getStatus().map(s => `${s.provider}(ok=${s.ok},fails=${s.failCount})`).join(', ')
|
|
47
|
+
throw new Error('no LLM backend reachable: set a provider API key or start acptoapi (http://127.0.0.1:4800/v1)' + (status ? ' | sampler: ' + status : ''))
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const isSimple = typeof m === 'string' && !m.includes(',') && !/^queue\//.test(m)
|
|
51
|
+
if (isSimple && await bridgeReachable()) return await bridgeCall({ ...input, model: m })
|
|
52
|
+
const r = await sdk.chat({ model: m, messages: toMsgs(input.messages), tools: toTools(input.tools), queuesMap: getConfigValue('agent.model_queues', {}) || {}, matrixSource: process.env.FREDDIE_MATRIX_URL || MATRIX_FILE, onFallback: input.onFallback, output: 'openai' })
|
|
53
|
+
return adapt(r)
|
|
54
|
+
} catch (e) {
|
|
55
|
+
if (/queue not found or empty/i.test(e.message)) throw e
|
|
56
|
+
if (e.chainHistory || /All chain links failed|chain\(\) requires/i.test(e.message)) throw new Error(`chain exhausted: ${(e.attempted || []).map(a => `${a.model}:${a.reason || 'ok'}`).join('; ') || e.message}`)
|
|
57
|
+
throw e
|
|
165
58
|
}
|
|
166
|
-
|
|
167
|
-
if (await acptoapiReachable()) {
|
|
168
|
-
return await acptoapiCall({ ...input, model: model || input.model })
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const preference = getConfigValue('agent.model_preference', [])
|
|
172
|
-
if (Array.isArray(preference) && preference.length > 0) {
|
|
173
|
-
try { return await tryChain(preference, input, model) } catch (e) { if (!/chain empty/.test(e.message)) throw e }
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const links = sdk.buildAutoChain(model || input.model)
|
|
177
|
-
const availableLinks = (await Promise.all(links.map(async l => {
|
|
178
|
-
const prefix = l.model.split('/')[0]
|
|
179
|
-
if (!(await hasKey(prefix)) || !isAvailable(prefix)) return null
|
|
180
|
-
const mu = matrixUsable(prefix, l.model.replace(/^[^/]+\//, ''))
|
|
181
|
-
if (mu === false) return null
|
|
182
|
-
return l
|
|
183
|
-
}))).filter(Boolean)
|
|
184
|
-
|
|
185
|
-
for (const link of availableLinks) {
|
|
186
|
-
const prefix = link.model.split('/')[0]
|
|
187
|
-
const m = link.model.replace(/^[^/]+\//, '')
|
|
188
|
-
try {
|
|
189
|
-
return await sdkChat(prefix, m, input)
|
|
190
|
-
} catch (e) {
|
|
191
|
-
markFailed(prefix)
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const status = getStatus().map(s => `${s.provider}(ok=${s.ok},fails=${s.failCount})`).join(', ')
|
|
196
|
-
throw new Error('no LLM backend reachable: set a provider API key or start acptoapi (http://127.0.0.1:4800/v1)' + (status ? ' | sampler: ' + status : ''))
|
|
197
59
|
}
|
|
198
60
|
}
|