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.97",
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.56"
30
+ "acptoapi": "^1.0.61"
31
31
  },
32
32
  "optionalDependencies": {
33
33
  "@libsql/darwin-arm64": "0.3.19",
@@ -1,44 +1,44 @@
1
- import { resolveCallLLM } from '../../src/agent/llm_resolver.js'
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
- const data = flattenForOpenAI()
12
- if (data.length === 0) data.push({ id: 'freddie/auto', object: 'model', created: Math.floor(Date.now() / 1000), owned_by: 'freddie' })
13
- res.json({ object: 'list', data })
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
- let provider, mdl
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
- res.setHeader('content-type', 'text/event-stream')
30
- res.setHeader('cache-control', 'no-cache')
31
- res.write(`data: ${JSON.stringify({ id, object: 'chat.completion.chunk', created, model: model || 'freddie/auto', choices: [{ index: 0, delta: { role: 'assistant', content: out.content || '' }, finish_reason: null }] })}\n\n`)
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({ id, object: 'chat.completion', created, model: model || 'freddie/auto', choices: [choice], usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 } })
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 { getStatus } from '../../src/agent/model-sampler.js'
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) => res.json(getConfigValue('agent.model_queues', {}) || {}))
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 { resolveKey } from './credential_sources.js'
6
- import { matrixUsable } from './model-matrix.js'
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 ACP_BACKENDS = {
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
- function tryParseJson(s) { try { return typeof s === 'string' ? JSON.parse(s) : (s || {}) } catch { return {} } }
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
- async function hasKey(provider) {
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 defaultModel(provider) {
126
- return DEFAULTS[provider] || ''
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
- async function tryChain(entries, input, model) {
130
- const errors = []
131
- for (const pref of entries) {
132
- const p = pref.provider; const m = pref.model || model || input.model || (DEFAULTS[p] || '')
133
- if (!await hasKey(p) || !isAvailable(p)) continue
134
- try { return await sdkChat(p, m, input) } catch (e) { markFailed(p); errors.push(`${p}: ${e.message}`) }
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
- if (errors.length) throw new Error(`chain exhausted: ${errors.join('; ')}`)
137
- throw new Error('chain empty: no available providers')
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 mdl = model || input.model
143
- const queueMatch = typeof mdl === 'string' && /^queue\//.test(mdl)
144
- if (queueMatch) {
145
- const name = mdl.slice('queue/'.length)
146
- const queues = getConfigValue('agent.model_queues', {}) || {}
147
- const entries = Array.isArray(queues[name]) ? queues[name] : null
148
- if (!entries || entries.length === 0) throw new Error(`queue not found or empty: ${name}`)
149
- return await tryChain(entries, input, undefined)
150
- }
151
- const explicitProvider = provider || input.provider
152
-
153
- if (explicitProvider && await hasKey(explicitProvider)) {
154
- const m = model || input.model || defaultModel(explicitProvider)
155
- if (!isAvailable(explicitProvider)) {
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
  }