Custom connections are gone: the CLI talks to the LLeMbas server it is logged in to

LLeMbas CLI 2.0 is LLeMbas-server-specific (CLI rule 2). Removed: the
anthropic, gemini, ollama and responses clients, context discovery and
llama-swap unloading (discover.ts), the Connection schema with its dialect,
auth, key_cmd, discover, unload_url and one_model_at_a_time, the model
`fallback` (it only ever moved to another server), effort_map and cache, and
the multi-connection refs. Kept: openai-chat, http, sse, think, effort,
tokens and all of src/lembas.

- connections.yaml holds the login (`type: webui`, as lembas login writes
  it). A custom entry from 1.x is read past with a warning that says how to
  log in, so an old file still starts; with several logins left, the latest
  is used and the others named.
- A model's ref is the id the server serves it under; the older forms
  (`<login>/<id>`, `<login>/<model>`) still resolve and config.yaml is
  rewritten once. Without a login there is no model, and the error (and the
  first-run screen, now the sign-in alone) says `lembas login <address>`.
- A login to another server signs this machine out of the previous one.
- The window is the server's word (no discovery); a capacity group comes
  from the server's `capacity.group` alone; embeddings use the server's
  /v1/embeddings.
- `lembas models` lists the server's models; install.sh's starter files, the
  README and the JSON schemas say the same.

Tests: every fixture is a login now (the fake server lists models, answers
other GETs with 404, and can hand one model to another script, as a server
with several backends); the dialect suites and the cross-connection tests are
gone with their code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
HomerandClaude Opus 5.5 committed 2026-10-27 21:00:05 +00:00
1 parent f20e8c4307
commit 28ff205ed0
80 files changed
+587 -2575

No files matched your search

+7 -10
View File
@@ -34,14 +34,11 @@ function pair(): [Transport, Transport] {
return [side(a, b), side(b, a)]
}
/** The parent's model on `f`; a second connection `g` for a subagent's model, when given. */
function config(url: string, childUrl?: string) {
/** The parent's model `f/m`; `g/m` for a subagent, when its own script is given (the fake server
* routes it there). */
function config(url: string, child?: boolean) {
mkdirSync(paths.config, { recursive: true })
writeFileSync(
join(paths.config, "connections.yaml"),
`connections:\n f:\n dialect: openai-chat\n base_url: ${url}\n models: { m: {} }\n${childUrl ? ` g:\n dialect: openai-chat\n base_url: ${childUrl}\n models: { m: {} }\n` : ""}`,
{ mode: 0o600 },
)
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n type: webui\n url: ${url}\n models: { f/m: {}${child ? ", g/m: {}" : ""} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), "model: f/m\nmode: auto\n")
}
@@ -145,14 +142,14 @@ test("job/stop stops a command and answers once it has; the -32602 cases: a comm
}, SLOW)
test("a background subagent: its transcript as history turns; send to a finished one starts it again under the same id", async () => {
const child = (other = fakeProvider([{ chunks: [delta({ content: "calc.py:2 divides by len(v) - 1." })] }, { chunks: [delta({ content: "The tests cover it." })] }]))
fake = fakeProvider([
{ chunks: [toolCall(0, "t1", "task", JSON.stringify({ description: "find the bug", prompt: "Find why average is wrong", model: "g/m", background: true }))] },
{ chunks: [delta({ content: "Started it." })] },
{ chunks: [delta({ content: "Got the report." })] },
{ chunks: [delta({ content: "Got the second report." })] },
])
other = fakeProvider([{ chunks: [delta({ content: "calc.py:2 divides by len(v) - 1." })] }, { chunks: [delta({ content: "The tests cover it." })] }])
config(fake.url, other.url)
], { route: (model) => (model.startsWith("g/") ? child : undefined) })
config(fake.url, true)
const { c, events, sid } = await client()
await c.request("session/prompt", { sessionId: sid, prompt: [{ type: "text", text: "go" }] })
expect(await until(() => events.some((e) => e.type === "job" && e.state === "end"))).toBe(true)
+4 -4
View File
@@ -40,7 +40,7 @@ function pair(): [Transport, Transport] {
function config(url: string, extra = "", model = "{}") {
mkdirSync(paths.config, { recursive: true })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n dialect: openai-chat\n base_url: ${url}\n models: { m: ${model} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n type: webui\n url: ${url}\n models: { f/m: ${model} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), `model: f/m\n${extra}`)
}
@@ -535,7 +535,7 @@ test("answered at the terminal first: the web UI's card is settled", async () =>
test("the model event: said whenever the model or effort changes, forwarded to LLeMbas", async () => {
fake = fakeProvider([])
mkdirSync(paths.config, { recursive: true })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n example:\n dialect: openai-chat\n base_url: ${fake.url}\n models: { a: { efforts: [low, high] }, b: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n example:\n type: webui\n url: ${fake.url}\n models: { example/a: { efforts: [low, high] }, example/b: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), "model: example/a\n")
const root = project()
const { c, events } = await client(limitsFor(root), { instance: "example" })
@@ -771,13 +771,13 @@ test("a command's mode from the web is clamped to remote.max_mode in a terminal
test("a command naming a model gives the session its model and its effort back afterwards", async () => {
fake = fakeProvider([{ chunks: [delta({ content: "ok" })] }])
mkdirSync(paths.config, { recursive: true })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n dialect: openai-chat\n base_url: ${fake.url}\n models: { m: { efforts: [low, high], effort: low }, other: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n type: webui\n url: ${fake.url}\n models: { f/m: { efforts: [low, high], effort: low }, f/other: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), "model: f/m\n")
const root = project({ ".agent/config.yaml": "", ".agent/commands/quick.md": "---\nmodel: f/other\n---\nQuick." })
const { c } = await client(limitsFor(root))
const s: any = await c.request("session/new", { cwd: root, _meta: { effort: "high" } })
await c.request("session/prompt", { sessionId: s.sessionId, prompt: [{ type: "text", text: "/quick" }] })
expect(fake.requests[0].model).toBe("other")
expect(fake.requests[0].model).toBe("f/other")
expect(await c.request<any>("_lembas/configure", { sessionId: s.sessionId })).toEqual({ model: "f/m", effort: "high" })
})
+5 -5
View File
@@ -36,7 +36,7 @@ function pair(): [Transport, Transport] {
function config(url: string, extra = "") {
mkdirSync(paths.config, { recursive: true })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n dialect: openai-chat\n base_url: ${url}\n models: { m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n type: webui\n url: ${url}\n models: { f/m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), `model: f/m\n${extra}`)
}
@@ -210,19 +210,19 @@ test("_lembas/directories is for the link only: an editor's ACP session has no r
test("the chat's model and effort: named by the instance, at the start and between prompts", async () => {
fake = fakeProvider([{ chunks: [delta({ content: "one" })] }, { chunks: [delta({ content: "two" })] }])
mkdirSync(paths.config, { recursive: true })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n example:\n dialect: openai-chat\n base_url: ${fake.url}\n models: { bonsai: {}, deepseek-flash: { efforts: [low, medium, high] } }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), "model: example/bonsai\n")
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n example:\n type: webui\n url: ${fake.url}\n models: { bonsai: {}, deepseek-flash: { efforts: [low, medium, high] } }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), "model: bonsai\n")
const root = project()
const { c } = await client({ roots: [root], maxMode: "auto", approvalTimeoutMs: 50, requireTrust: true }, undefined, "example")
// Not the device's own default: the model the chat was started on there.
const s: any = await c.request("session/new", { cwd: root, _meta: { mode: "auto", lembas_model: "deepseek-flash", effort: "high" } })
expect(s._meta.lembas).toMatchObject({ model: "example/deepseek-flash", effort: "high" })
expect(s._meta.lembas).toMatchObject({ model: "deepseek-flash", effort: "high" })
await c.request("session/prompt", { sessionId: s.sessionId, prompt: [{ type: "text", text: "hi" }] })
expect(fake.requests[0].model).toBe("deepseek-flash")
expect(JSON.stringify(fake.requests[0])).toContain('"reasoning_effort":"high"')
// Changed in the web UI between two prompts.
const changed: any = await c.request("_lembas/configure", { sessionId: s.sessionId, model: "bonsai", effort: "off" })
expect(changed).toEqual({ model: "example/bonsai", effort: "off" })
expect(changed).toEqual({ model: "bonsai", effort: "off" })
await c.request("session/prompt", { sessionId: s.sessionId, prompt: [{ type: "text", text: "again" }] })
expect(fake.requests.at(-1).model).toBe("bonsai")
expect(JSON.stringify(fake.requests.at(-1))).not.toContain("reasoning_effort")
-115
View File
@@ -1,115 +0,0 @@
import { afterEach, describe, expect, test } from "bun:test"
import { readFileSync } from "node:fs"
import { join } from "node:path"
import { AnthropicClient, toAnthropicMessages } from "../src/provider/anthropic.ts"
import type { Message, ResolvedModel, StreamEvent } from "../src/provider/types.ts"
import { fakeProvider, type Fake } from "./fake-provider.ts"
let fake: Fake | undefined
afterEach(() => fake?.stop())
const model = (url: string, spec: ResolvedModel["spec"] = {}): ResolvedModel => ({
ref: "a/m",
connectionName: "a",
id: "m",
spec,
connection: { dialect: "anthropic", base_url: url, api_key: "k", models: {} },
})
const sse = (events: Record<string, unknown>[]) => events.map((e) => `event: ${e.type}\ndata: ${JSON.stringify(e)}\n\n`).join("")
async function run(c: AnthropicClient, messages: Message[] = [{ role: "user", parts: [{ type: "text", text: "hi" }] }], effort: any = null) {
const ev: StreamEvent[] = []
for await (const e of c.stream({ system: "sys", messages, tools: [{ name: "read", description: "read", parameters: { type: "object" } }], effort })) ev.push(e)
return ev
}
describe("anthropic dialect", () => {
test("a real vLLM stream: thinking with its signature, then a tool call", async () => {
fake = fakeProvider([{ chunks: [], raw: readFileSync(join(import.meta.dir, "fixtures/sse/vllm-anthropic-thinking-tool.sse"), "utf8") }])
const ev = await run(new AnthropicClient(model(fake.url.replace(/\/v1$/, ""))))
const fin = ev.find((e) => e.type === "finish")!
if (fin.type !== "finish") throw new Error()
expect(fin.reason).toBe("tool_calls")
const thinking = fin.message.parts.find((p) => p.type === "reasoning")
expect(thinking?.type === "reasoning" && thinking.signature).toBeTruthy()
expect(fin.message.parts.some((p) => p.type === "tool_call")).toBe(true)
expect(ev.some((e) => e.type === "reasoning")).toBe(true)
})
test("request: headers, max_tokens above the thinking budget, no sampling with thinking, cache breakpoints", async () => {
fake = fakeProvider([{ chunks: [], raw: sse([{ type: "message_start", message: { usage: { input_tokens: 5 } } }, { type: "message_stop" }]) }])
const c = new AnthropicClient(model(fake.url, { max_output: 4000, temperature: 0.5, cache: true, effort_map: { high: 10000 } }))
await run(c, undefined, "high")
const r = fake.requests[0]
expect(r.thinking).toEqual({ type: "enabled", budget_tokens: 10000 })
expect(r.max_tokens).toBeGreaterThan(10000)
expect(r.temperature).toBeUndefined()
expect(r.system[0].cache_control).toEqual({ type: "ephemeral" })
expect(r.tools.at(-1).cache_control).toEqual({ type: "ephemeral" })
expect(r.messages.at(-1).content.at(-1).cache_control).toEqual({ type: "ephemeral" })
})
test("stream: text, redacted thinking, tool input in pieces, cache usage, stop reasons", async () => {
fake = fakeProvider([
{
chunks: [],
raw: sse([
{ type: "message_start", message: { usage: { input_tokens: 10, cache_read_input_tokens: 90, output_tokens: 1 } } },
{ type: "content_block_start", index: 0, content_block: { type: "redacted_thinking", data: "OPAQUE" } },
{ type: "content_block_stop", index: 0 },
{ type: "content_block_start", index: 1, content_block: { type: "text", text: "" } },
{ type: "content_block_delta", index: 1, delta: { type: "text_delta", text: "Reading." } },
{ type: "content_block_stop", index: 1 },
{ type: "content_block_start", index: 2, content_block: { type: "tool_use", id: "toolu_1", name: "read", input: {} } },
{ type: "content_block_delta", index: 2, delta: { type: "input_json_delta", partial_json: '{"pa' } },
{ type: "content_block_delta", index: 2, delta: { type: "input_json_delta", partial_json: 'th":"a"}' } },
{ type: "content_block_stop", index: 2 },
{ type: "message_delta", delta: { stop_reason: "tool_use" }, usage: { output_tokens: 42 } },
{ type: "message_stop" },
]),
},
])
const ev = await run(new AnthropicClient(model(fake.url)))
const fin = ev.find((e) => e.type === "finish")
expect(fin?.type === "finish" && fin.message.parts).toEqual([
{ type: "reasoning", text: "", opaque: { redacted: "OPAQUE" } },
{ type: "text", text: "Reading." },
{ type: "tool_call", id: "toolu_1", name: "read", args: '{"path":"a"}' },
])
expect(ev.find((e) => e.type === "usage")).toEqual({ type: "usage", usage: { input: 100, output: 42, cached: 90 } })
expect(fake.requests[0]).toMatchObject({ model: "m", stream: true })
})
test("overloaded before any output is retried once", async () => {
fake = fakeProvider([
{ chunks: [], raw: sse([{ type: "message_start", message: { usage: { input_tokens: 1 } } }, { type: "error", error: { type: "overloaded_error", message: "Overloaded" } }]) },
{ chunks: [], raw: sse([{ type: "content_block_start", index: 0, content_block: { type: "text", text: "ok" } }, { type: "content_block_stop", index: 0 }, { type: "message_stop" }]) },
])
const ev = await run(new AnthropicClient(model(fake.url)))
expect(ev.some((e) => e.type === "notice" && e.message.includes("Overloaded"))).toBe(true)
const fin = ev.find((e) => e.type === "finish")
expect(fin?.type === "finish" && fin.message.parts).toEqual([{ type: "text", text: "ok" }])
})
test("history: tool results grouped as a user turn, ids made safe, thinking only with a signature and only while thinking", () => {
const msgs: Message[] = [
{ role: "user", parts: [{ type: "text", text: "go" }] },
{ role: "assistant", parts: [{ type: "reasoning", text: "unsigned (from another provider)" }, { type: "reasoning", text: "signed", signature: "SIG" }, { type: "tool_call", id: "call:1", name: "read", args: '{"path":"a"}' }, { type: "tool_call", id: "call:2", name: "read", args: "{}" }] },
{ role: "tool", callId: "call:1", name: "read", content: "A" },
{ role: "tool", callId: "call:2", name: "read", content: "", isError: true },
{ role: "user", parts: [{ type: "text", text: "and?" }] },
]
const on = toAnthropicMessages(msgs, false, true)
expect(on.map((m) => m.role)).toEqual(["user", "assistant", "user"])
expect(on[1]!.content).toEqual([
{ type: "thinking", thinking: "signed", signature: "SIG" },
{ type: "tool_use", id: "call_1", name: "read", input: { path: "a" } },
{ type: "tool_use", id: "call_2", name: "read", input: {} },
])
expect(on[2]!.content).toEqual([
{ type: "tool_result", tool_use_id: "call_1", content: "A" },
{ type: "tool_result", tool_use_id: "call_2", content: "(empty)", is_error: true },
{ type: "text", text: "and?" },
])
expect(toAnthropicMessages(msgs, false, false)[1]!.content.some((b) => b.type === "thinking")).toBe(false)
})
})
+1 -1
View File
@@ -16,7 +16,7 @@ afterEach(() => fake?.stop())
function app(edit: string, config = "") {
fake = fakeProvider([{ chunks: [toolCall(0, "b1", "bash", '{"command":"echo one"}')] }, { chunks: [delta({ content: "ok" }, "stop")] }])
mkdirSync(paths.config, { recursive: true })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n dialect: openai-chat\n base_url: ${fake.url}\n models: { m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n type: webui\n url: ${fake.url}\n models: { f/m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), `model: f/m\ntitles: prompt\n${config}`)
return createApp({ cwd: mkdtempSync(join(tmpdir(), "ph-approve-")), mode: "manual", store: false, snapshots: false, asker: { ask: async (): Promise<AskReply> => ({ kind: "once", command: edit }) } })
}
+1 -1
View File
@@ -33,7 +33,7 @@ describe("the project board", () => {
{ chunks: [delta({ content: "next" })] },
])
mkdirSync(paths.config, { recursive: true })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n dialect: openai-chat\n base_url: ${fake.url}\n models: { m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n type: webui\n url: ${fake.url}\n models: { f/m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), "model: f/m\n")
const cwd = mkdtempSync(join(tmpdir(), "ph-board-"))
mkdirSync(join(cwd, ".agent"))
+1 -1
View File
@@ -16,7 +16,7 @@ afterEach(() => fake?.stop())
function app(limits: string, script: Parameters<typeof fakeProvider>[0], ask: () => Promise<AskReply> = async () => ({ kind: "once" })) {
fake = fakeProvider(script)
mkdirSync(paths.config, { recursive: true })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n dialect: openai-chat\n base_url: ${fake.url}\n models: { m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n type: webui\n url: ${fake.url}\n models: { f/m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), `model: f/m\ntitles: prompt\nlimits: { ${limits} }\n`)
const cwd = mkdtempSync(join(tmpdir(), "ph-budget-"))
writeFileSync(join(cwd, "big.txt"), "x".repeat(5000) + "\n")
+7 -16
View File
@@ -14,24 +14,15 @@ import { delta, fakeProvider, toolCall, type Fake } from "./fake-provider.ts"
let fake: Fake | undefined
afterEach(() => fake?.stop())
const model = (conn: string, id: string, o: { one?: boolean; single?: boolean } = {}): ResolvedModel => ({
const model = (conn: string, id: string, o: { single?: boolean } = {}): ResolvedModel => ({
ref: `${conn}/${id}`,
connectionName: conn,
connection: { dialect: "openai-chat", base_url: "http://x/v1", models: {}, ...(o.one ? { one_model_at_a_time: true } : {}) } as ResolvedModel["connection"],
id,
connectionName: "ai",
connection: { base_url: "http://x/v1", models: {} },
id: `${conn}/${id}`,
spec: o.single ? { single_session: true } : {},
})
test("one model at a time: another model of the same server is refused, the session's own is not", () => {
const main = model("swap", "qwen", { one: true })
expect(capacityRefusal(main, model("swap", "gemma", { one: true }))).toContain("holds one model at a time")
expect(capacityRefusal(main, main)).toBe("")
expect(capacityRefusal(main, model("cloud", "big"))).toBe("")
// Without the flag, as before.
expect(capacityRefusal(model("swap", "qwen"), model("swap", "gemma"))).toBe("")
})
test("a model's group: models one connection serves from several servers are told apart", () => {
test("a model's group (the server's capacity.group): another model of the same GPU is refused, the session's own is not", () => {
const grouped = (id: string, group?: string): ResolvedModel => ({ ...model("ai", id), spec: group ? { group } : {} })
const main = grouped("qwen", "gpu")
expect(capacityRefusal(main, grouped("gemma", "gpu"))).toContain("holds one model at a time")
@@ -55,14 +46,14 @@ test("task on a model the server cannot hold beside the session's is refused, an
mkdirSync(paths.config, { recursive: true })
writeFileSync(
join(paths.config, "connections.yaml"),
`connections:\n f:\n dialect: openai-chat\n base_url: ${fake.url}\n one_model_at_a_time: true\n models: { m: {}, other: {} }\n`,
`connections:\n f:\n type: webui\n url: ${fake.url}\n models: { f/m: { group: gpu }, f/other: { group: gpu } }\n`,
{ mode: 0o600 },
)
writeFileSync(join(paths.config, "config.yaml"), "model: f/m\n")
const a = createApp({ cwd: mkdtempSync(join(tmpdir(), "ph-cap-")), mode: "edit", store: false, asker: { ask: async (): Promise<AskReply> => ({ kind: "once" }) } })
await a.engine.prompt("get a second opinion")
// Two requests: the session's, then its next step — never one for f/other.
expect(fake.requests.map((r: any) => r.model)).toEqual(["m", "m"])
expect(fake.requests.map((r: any) => r.model)).toEqual(["f/m", "f/m"])
const result = fake.requests[1].messages.at(-1).content as string
expect(result).toContain("holds one model at a time")
expect(result).toContain("Do this part yourself")
+29 -42
View File
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os"
import { join } from "node:path"
import { paths } from "../src/config/paths.ts"
import { loadConfig, resolveKey } from "../src/config/load.ts"
import { resolveModel } from "../src/provider/index.ts"
import { NOT_LOGGED_IN, resolveModel } from "../src/provider/index.ts"
const write = (file: string, text: string) => {
mkdirSync(join(file, ".."), { recursive: true })
@@ -17,7 +17,7 @@ beforeEach(() => {
})
describe("config", () => {
test("connections: substitution, a broken one does not break the others, mode warning", () => {
test("connections: the login, its key from a file; a custom connection is read past with a warning (2.0)", () => {
write(join(paths.config, "keyfile"), "sk-from-file\n")
write(
join(paths.config, "connections.yaml"),
@@ -27,32 +27,40 @@ describe("config", () => {
base_url: http://llm.example/v1
api_key: "{env:PH_TEST_KEY}"
models:
qwen: { context: 131072, max_output: 8192, efforts: [low, high], effort: high }
file:
dialect: openai-chat
base_url: http://x/v1
qwen: { context: 131072 }
example:
type: webui
url: https://ai.example.org
api_key: "{file:${join(paths.config, "keyfile")}}"
models: { a: {} }
broken:
dialect: openai-chat
base_url: http://y/v1
api_key: "{env:PH_MISSING}"
models: { b: {} }
models:
llama/qwen: { context: 131072, max_output: 8192, efforts: [low, high], effort: high }
`,
)
chmodSync(join(paths.config, "connections.yaml"), 0o644)
const l = loadConfig()
expect(l.connections.swap!.api_key).toBe("sk-test")
expect(resolveKey("file", l.connections.file!)).toBe("sk-from-file")
expect(l.broken.broken).toContain("PH_MISSING")
expect(Object.keys(l.connections)).toEqual(["example"])
expect(l.connections.example!.base_url).toBe("https://ai.example.org/v1")
expect(resolveKey("example", l.connections.example!)).toBe("sk-from-file")
expect(l.warnings.some((w) => w.includes("swap is a custom connection") && w.includes("lembas login"))).toBe(true)
expect(l.warnings.some((w) => w.includes("chmod 600"))).toBe(true)
expect(resolveModel(l, "swap/qwen").spec.context).toBe(131072)
expect(() => resolveModel(l, "broken/b")).toThrow("unusable")
expect(() => resolveModel(l, "swap/nope")).toThrow('no model "nope"')
expect(resolveModel(l, "llama/qwen").spec.context).toBe(131072)
// The older form, the login's name in front, names the same model.
expect(resolveModel(l, "example/llama/qwen").ref).toBe("llama/qwen")
// One the server's list does not have yet is asked for, not refused here: the server decides.
expect(resolveModel(l, "llama/new")).toMatchObject({ ref: "llama/new", id: "llama/new", spec: {} })
})
test("key_cmd", () => {
expect(resolveKey("k", { dialect: "openai-chat", base_url: "http://x", key_cmd: "echo sk-cmd", models: {} })).toBe("sk-cmd")
test("no login: no model, and the error says how to log in", () => {
write(join(paths.config, "connections.yaml"), "connections: {}\n")
expect(() => resolveModel(loadConfig(), undefined)).toThrow(NOT_LOGGED_IN)
expect(NOT_LOGGED_IN).toContain("lembas login <address>")
})
test("a login whose key file is gone is unusable, said as such", () => {
write(join(paths.config, "connections.yaml"), `connections:\n example:\n type: webui\n url: https://ai.example.org\n api_key: "{env:PH_MISSING}"\n`)
const l = loadConfig()
expect(l.broken.example).toContain("PH_MISSING")
expect(() => resolveModel(l, undefined)).toThrow("unusable")
})
test("project config: merged when trusted, ignored when not, global-only keys refused", () => {
@@ -76,34 +84,13 @@ describe("config", () => {
})
})
test("effort_map may name only some efforts (zod 4 records keyed by an enum are exhaustive)", async () => {
const { ModelSpec } = await import("../src/config/schema.ts")
expect(ModelSpec.safeParse({ effort_map: { high: 10000 } }).success).toBe(true)
expect(ModelSpec.safeParse({ effort_map: { huge: 1 } }).success).toBe(false)
})
test("a URL may be {env:}/{file:}: accepted as written, and checked once filled in", () => {
process.env.PH_URL = "http://llm.example/v1"
process.env.PH_NOT_URL = "not a url"
write(
join(paths.config, "connections.yaml"),
`connections:
ok: { dialect: openai-chat, base_url: "{env:PH_URL}", models: { m: {} } }
bad: { dialect: openai-chat, base_url: "{env:PH_NOT_URL}", models: { m: {} } }
typo: { dialect: openai-chat, base_url: "{env:PH_URL", models: { m: {} } }
`,
)
write(join(paths.config, "connections.yaml"), "connections: {}\n")
chmodSync(join(paths.config, "connections.yaml"), 0o600)
write(join(paths.config, "config.yaml"), 'search:\n searxng: { base_url: "{env:PH_URL}" }\n firecrawl: { base_url: "{env:PH_NOT_URL}" }\n')
expect(() => loadConfig()).toThrow(/typo\.base_url: Invalid URL/)
write(join(paths.config, "connections.yaml"), `connections:
ok: { dialect: openai-chat, base_url: "{env:PH_URL}", models: { m: {} } }
bad: { dialect: openai-chat, base_url: "{env:PH_NOT_URL}", models: { m: {} } }
`)
chmodSync(join(paths.config, "connections.yaml"), 0o600)
const l = loadConfig()
expect(l.connections.ok!.base_url).toBe("http://llm.example/v1")
expect(l.broken.bad).toContain("base_url: Invalid URL")
expect(l.config.search?.searxng?.base_url).toBe("http://llm.example/v1")
expect(l.config.search?.firecrawl).toBeUndefined()
expect(l.warnings.some((w) => w.includes("firecrawl is off"))).toBe(true)
+3 -21
View File
@@ -5,10 +5,7 @@ import { join } from "node:path"
import { createApp } from "../src/app.ts"
import type { AskReply, Event } from "../src/bus/index.ts"
import { paths } from "../src/config/paths.ts"
import { discoverContext } from "../src/provider/discover.ts"
import { resetLearned } from "../src/provider/learned.ts"
import { OpenAIChatClient } from "../src/provider/openai-chat.ts"
import type { ResolvedModel } from "../src/provider/types.ts"
import { contextBreakdown, renderBreakdown } from "../src/session/context.ts"
import { delta, fakeProvider, toolCall, usage, type Fake } from "./fake-provider.ts"
@@ -18,7 +15,6 @@ afterEach(() => {
resetLearned()
})
const rm = (url: string, id: string): ResolvedModel => ({ ref: `c/${id}`, connectionName: "c", id, spec: {}, connection: { dialect: "openai-chat", base_url: url, models: {} } })
function app(yaml: string, script: Parameters<typeof fakeProvider>[0], config = "") {
fake = fakeProvider(script)
@@ -31,24 +27,10 @@ function app(yaml: string, script: Parameters<typeof fakeProvider>[0], config =
}
describe("context", () => {
test("discovery: the model list first, then llama-server /props through llama-swap's passthrough", async () => {
fake = fakeProvider([])
expect(await discoverContext(rm(fake.url, "m1"), new OpenAIChatClient(rm(fake.url, "m1")))).toBe(32768)
expect(await discoverContext(rm(fake.url, "swapped"), new OpenAIChatClient(rm(fake.url, "swapped")))).toBe(16384)
expect(fake.calls.map((c) => c.path)).toContain("/upstream/swapped/props")
})
test("switching to another connection calls the old one's unload_url", async () => {
const a = app(`connections:\n f:\n dialect: openai-chat\n base_url: URL\n unload_url: URL/../unload\n unload_method: GET\n models: { m: {} }\n g:\n dialect: openai-chat\n base_url: URL\n models: { n: {} }\n`, [])
a.switchModel("g/n")
await Bun.sleep(100)
expect(fake!.calls.some((c) => c.path.endsWith("/unload"))).toBe(true)
})
test("past auto_at: nothing already sent is cut; the conversation is compacted, with a table of the reads", async () => {
// window 16000, auto_at 0.6 → limit 9600. Harness spec 3.0.0 (context.md): old tool outputs are
// never pruned in place — that would make the server read everything after them again.
const a = app(`connections:\n f:\n dialect: openai-chat\n base_url: URL\n models: { m: { context: 16000 } }\n`, [
const a = app(`connections:\n f:\n type: webui\n url: URL\n models: { f/m: { context: 16000 } }\n`, [
{ chunks: [toolCall(0, "c1", "read", '{"path":"big.txt"}'), usage(1000, 10)] },
{ chunks: [toolCall(0, "c2", "read", '{"path":"big2.txt"}'), usage(6600, 10)] },
{ chunks: [delta({ content: "## Summary\nRead two files." })] },
@@ -75,7 +57,7 @@ describe("context", () => {
})
test("too full: compacted mid-task, then told to carry on", async () => {
const a = app(`connections:\n f:\n dialect: openai-chat\n base_url: URL\n models: { m: { context: 4000 } }\n`, [
const a = app(`connections:\n f:\n type: webui\n url: URL\n models: { f/m: { context: 4000 } }\n`, [
{ chunks: [delta({ content: "y".repeat(8000) }), toolCall(0, "c1", "list", "{}"), usage(3900, 2000)] },
{ chunks: [delta({ content: "## What we are doing\nA long task." })] },
{ chunks: [delta({ content: "carried on" }), usage(500, 5)] },
@@ -93,7 +75,7 @@ describe("context", () => {
})
test("/context: parts of the system prompt, tools, the conversation, against the window", async () => {
const a = app(`connections:\n f:\n dialect: openai-chat\n base_url: URL\n models: { m: { context: 32768 } }\n`, [{ chunks: [toolCall(0, "c1", "read", '{"path":"big.txt"}'), usage(1500, 20)] }, { chunks: [delta({ content: "ok" }), usage(3100, 5)] }])
const a = app(`connections:\n f:\n type: webui\n url: URL\n models: { f/m: { context: 32768 } }\n`, [{ chunks: [toolCall(0, "c1", "read", '{"path":"big.txt"}'), usage(1500, 20)] }, { chunks: [delta({ content: "ok" }), usage(3100, 5)] }])
await a.engine.prompt("read it")
const system = a.engine.o.system(a.engine.mode, a.engine.model)
const text = renderBreakdown(contextBreakdown(a.engine, system, [], undefined, a.engine.o.tools))
+2 -2
View File
@@ -19,7 +19,7 @@ function setup(script: Parameters<typeof fakeProvider>[0]) {
mkdirSync(paths.config, { recursive: true })
writeFileSync(
join(paths.config, "connections.yaml"),
`connections:\n fake:\n dialect: openai-chat\n base_url: ${fake.url}\n models:\n m: { context: 32768, max_output: 1000 }\n`,
`connections:\n fake:\n type: webui\n url: ${fake.url}\n models:\n fake/m: { context: 32768, max_output: 1000 }\n`,
{ mode: 0o600 },
)
writeFileSync(join(paths.config, "config.yaml"), "model: fake/m\n")
@@ -113,7 +113,7 @@ describe("the connection timeout", () => {
const dir = setup(script)
writeFileSync(
join(paths.config, "connections.yaml"),
`connections:\n fake:\n dialect: openai-chat\n base_url: ${fake!.url}\n timeout: 0.4\n models:\n m: { context: 32768 }\n`,
`connections:\n fake:\n type: webui\n url: ${fake!.url}\n timeout: 0.4\n models:\n fake/m: { context: 32768 }\n`,
{ mode: 0o600 },
)
return dir
+1 -1
View File
@@ -17,7 +17,7 @@ function setup(script: Parameters<typeof fakeProvider>[0]) {
mkdirSync(paths.config, { recursive: true })
writeFileSync(
join(paths.config, "connections.yaml"),
`connections:\n fake:\n dialect: openai-chat\n base_url: ${fake.url}\n models:\n m: { context: 32768 }\n`,
`connections:\n fake:\n type: webui\n url: ${fake.url}\n models:\n fake/m: { context: 32768 }\n`,
{ mode: 0o600 },
)
writeFileSync(join(paths.config, "config.yaml"), "model: fake/m\n")
+11 -5
View File
@@ -11,8 +11,12 @@ export interface Fake {
stop(): void
}
/** `show`: what Ollama's /api/show answers (404 when not given); it takes no scripted response. */
export function fakeProvider(script: Scripted[], opts: { show?: unknown } = {}): Fake {
/** `route`: another fake that answers for a model (by the id in the request), as one LLeMbas server
* serves models from several backends — the request is passed to it whole, and recorded there. */
/** `served`: what GET /v1/models lists, as a LLeMbas server lists its models (default two
* placeholders). Any other GET — the instance's own description, its personalization — is a 404,
* which a client reads as "this server does not have it". */
export function fakeProvider(script: Scripted[], opts: { route?: (model: string) => Fake | undefined; served?: Record<string, unknown>[] } = {}): Fake {
const requests: any[] = []
const calls: { path: string; headers: Record<string, string> }[] = []
let i = 0
@@ -21,11 +25,13 @@ export function fakeProvider(script: Scripted[], opts: { show?: unknown } = {}):
async fetch(req) {
const url = new URL(req.url)
if (req.method !== "POST" || url.pathname.endsWith("/unload")) calls.push({ path: url.pathname + url.search, headers: Object.fromEntries(req.headers.entries()) })
if (url.pathname.endsWith("/models")) return Response.json({ data: [{ id: "m1", meta: { n_ctx: 32768 } }, { id: "m2", max_model_len: 8192 }] })
if (url.pathname.endsWith("/models")) return Response.json({ data: opts.served ?? [{ id: "m1", meta: { n_ctx: 32768 } }, { id: "m2", max_model_len: 8192 }] })
if (url.pathname.endsWith("/props")) return url.pathname.includes("/upstream/swapped/") ? Response.json({ default_generation_settings: { n_ctx: 16384 } }) : new Response("no", { status: 404 })
if (url.pathname.endsWith("/unload")) return new Response("ok")
if (url.pathname.endsWith("/api/show")) return opts.show ? Response.json(opts.show) : new Response("not found", { status: 404 })
const body = await req.json()
if (req.method === "GET") return new Response("not found", { status: 404 })
const body = (await req.json()) as any
const to = typeof body?.model === "string" ? opts.route?.(body.model) : undefined
if (to) return fetch(to.url.replace(/\/v1$/, "") + url.pathname + url.search, { method: "POST", headers: req.headers, body: JSON.stringify(body) })
requests.push(body)
calls.push({ path: url.pathname + url.search, headers: Object.fromEntries(req.headers.entries()) })
const r = script[i++]
-225
View File
@@ -1,225 +0,0 @@
// Gemini and Ollama are UNTESTED against real servers (the wiki says so). These fixtures follow the
// documented shapes and what Hermes Agent and OpenCode handle.
import { afterEach, describe, expect, test } from "bun:test"
import { DEFAULT_MAX_OUTPUT, GeminiClient, geminiJsonSchema, geminiSchema, SKIP_SIGNATURE, thinkingConfig, toGeminiContents } from "../src/provider/gemini.ts"
import { OllamaClient, parseShow } from "../src/provider/ollama.ts"
import type { Message, ResolvedModel, StreamEvent } from "../src/provider/types.ts"
import { BUILTIN_TOOLS } from "../src/tool/registry.ts"
import { toSpec } from "../src/tool/tool.ts"
import { fakeProvider, type Fake } from "./fake-provider.ts"
let fake: Fake | undefined
afterEach(() => fake?.stop())
const m = (dialect: "gemini" | "ollama", url: string, id: string, spec: ResolvedModel["spec"] = {}): ResolvedModel => ({
ref: `x/${id}`,
connectionName: "x",
id,
spec,
connection: { dialect, base_url: url, api_key: "KEY", models: {} },
})
async function run(c: GeminiClient | OllamaClient, messages: Message[] = [{ role: "user", parts: [{ type: "text", text: "hi" }] }], effort: any = null) {
const ev: StreamEvent[] = []
for await (const e of c.stream({ system: "sys", messages, tools: BUILTIN_TOOLS.map(toSpec).slice(0, 2), effort })) ev.push(e)
return ev
}
const finishOf = (ev: StreamEvent[]) => {
const f = ev.find((e) => e.type === "finish")
if (!f || f.type !== "finish") throw new Error("no finish")
return f
}
describe("gemini", () => {
test("every built-in tool's schema is reduced to what Gemini accepts", () => {
const allowed = new Set(["type", "format", "title", "description", "nullable", "enum", "maxItems", "minItems", "properties", "required", "minProperties", "maxProperties", "minLength", "maxLength", "pattern", "example", "anyOf", "propertyOrdering", "default", "items", "minimum", "maximum"])
const walk = (s: any, path: string) => {
if (!s || typeof s !== "object") return
for (const [k, v] of Object.entries(s)) {
if (k === "properties") for (const [p, sub] of Object.entries(v as object)) walk(sub, `${path}.${p}`)
else {
expect(allowed.has(k) ? k : `${path}: ${k}`).toBe(k)
if (k === "items" || k === "anyOf") walk(v, path)
}
}
}
for (const t of BUILTIN_TOOLS.map(toSpec)) walk(geminiSchema(t.parameters), t.name)
expect(geminiSchema({ type: ["string", "null"], additionalProperties: false })).toEqual({ type: "string", nullable: true })
})
test("subset schema: unions keep every branch, enums become strings, required only names what exists", () => {
expect(geminiSchema({ type: ["array", "string", "null"], items: { type: "string", $comment: "x" }, minItems: 1 })).toEqual({
anyOf: [{ type: "array", items: { type: "string" }, minItems: 1 }, { type: "string" }],
nullable: true,
})
expect(geminiSchema({ type: "integer", enum: [1, 2, 2, null] })).toEqual({ type: "integer", enum: ["1", "2"] })
expect(geminiSchema({ type: "object", properties: { a: { type: "string" } }, required: ["a", "b"] })).toEqual({ type: "object", properties: { a: { type: "string" } }, required: ["a"] })
expect(geminiSchema({ type: "object", required: ["b"] })).toEqual({ type: "object" })
})
test("JSON Schema (v1beta): local $refs inlined with their siblings, $defs and $schema gone; a circular one goes as it is", () => {
const s = { $schema: "x", type: "object", $defs: { P: { type: "string", description: "p" } }, properties: { a: { $ref: "#/$defs/P", description: "mine" } } }
expect(geminiJsonSchema(s)).toEqual({ type: "object", properties: { a: { type: "string", description: "mine" } } })
const loop = { type: "object", $defs: { N: { type: "object", properties: { next: { $ref: "#/$defs/N" } } } }, properties: { n: { $ref: "#/$defs/N" } } }
expect(geminiJsonSchema(loop)).toEqual(loop)
expect(geminiJsonSchema({ type: "object" })).toEqual({ type: "object", properties: {} })
expect(geminiJsonSchema(undefined)).toEqual({ type: "object", properties: {} })
})
test("thinking: budgets for 2.x (2.5 Pro to 32k), levels for 3 fitted to what each model has", () => {
expect(thinkingConfig("gemini-2.5-pro", "max")).toEqual({ thinkingBudget: 32768, includeThoughts: true })
expect(thinkingConfig("models/gemini-2.5-flash", "max")).toEqual({ thinkingBudget: 24576, includeThoughts: true })
expect(thinkingConfig("gemini-2.5-flash", "low", { low: 500 })).toEqual({ thinkingBudget: 500, includeThoughts: true })
const level = (id: string, e: any) => thinkingConfig(id, e).thinkingLevel
expect(level("gemini-3-pro", "minimal")).toBe("low")
expect(level("gemini-3-pro", "medium")).toBe("medium")
expect(level("gemini-3-flash", "minimal")).toBe("minimal")
expect(level("gemini-3.1-pro", "xhigh")).toBe("high")
expect(level("gemma-4-31b-it", "medium")).toBe("high")
})
test("request: URL, key header, system, thinking budget (2.5) or level (3); stream: thoughts, text, a call with its signature", async () => {
const frames = [
{ candidates: [{ content: { role: "model", parts: [{ text: "Let me look.", thought: true }] } }] },
{ candidates: [{ content: { role: "model", parts: [{ text: "Reading the file." }] } }] },
{ candidates: [{ content: { role: "model", parts: [{ functionCall: { name: "read", args: { path: "a.ts" } }, thoughtSignature: "SIG" }] }, finishReason: "STOP" }], usageMetadata: { promptTokenCount: 50, candidatesTokenCount: 10, thoughtsTokenCount: 5, cachedContentTokenCount: 20 } },
]
fake = fakeProvider([{ chunks: frames }, { chunks: frames }])
const base = fake.url.replace(/\/v1$/, "/v1beta")
const ev = await run(new GeminiClient(m("gemini", base, "gemini-2.5-pro", { max_output: 1000 })), undefined, "medium")
expect(fake.calls[0]!.path).toBe("/v1beta/models/gemini-2.5-pro:streamGenerateContent?alt=sse")
expect(fake.calls[0]!.headers["x-goog-api-key"]).toBe("KEY")
expect(fake.calls[0]!.headers.authorization).toBeUndefined()
const r = fake.requests[0]
expect(r.systemInstruction).toEqual({ parts: [{ text: "sys" }] })
expect(r.generationConfig).toEqual({ maxOutputTokens: 1000, thinkingConfig: { thinkingBudget: 8192, includeThoughts: true } })
expect(r.tools[0].functionDeclarations[0].name).toBe("read")
expect(r.tools[0].functionDeclarations[0].parametersJsonSchema.type).toBe("object")
expect(r.tools[0].functionDeclarations[0].parameters).toBeUndefined()
const fin = finishOf(ev)
expect(fin.reason).toBe("tool_calls")
expect(fin.message.parts).toEqual([
{ type: "reasoning", text: "Let me look." },
{ type: "text", text: "Reading the file." },
{ type: "tool_call", id: "gemini_call_0", name: "read", args: '{"path":"a.ts"}', signature: "SIG" },
])
expect(ev.find((e) => e.type === "usage")).toEqual({ type: "usage", usage: { input: 50, output: 15, reasoning: 5, cached: 20 } })
await run(new GeminiClient(m("gemini", base, "gemini-3-pro")), undefined, "high")
expect(fake.requests[1].generationConfig).toEqual({ maxOutputTokens: DEFAULT_MAX_OUTPUT, thinkingConfig: { thinkingLevel: "high", includeThoughts: true } })
})
test("another API version gets the OpenAPI subset as `parameters`", async () => {
fake = fakeProvider([{ chunks: [{ candidates: [{ content: { parts: [{ text: "ok" }] }, finishReason: "STOP" }] }] }])
await run(new GeminiClient(m("gemini", fake.url, "gemini-2.0-flash")))
const d = fake.requests[0].tools[0].functionDeclarations[0]
expect(d.parametersJsonSchema).toBeUndefined()
expect(d.parameters.type).toBe("object")
})
test("stream: a call sent again stays one call; a different one is a new call; Gemini 3 ids are kept", async () => {
const call = (args: unknown, extra: Record<string, unknown> = {}) => ({ candidates: [{ content: { parts: [{ functionCall: { name: "read", args, ...extra } }] } }] })
fake = fakeProvider([
{ chunks: [call({ path: "a", limit: 5 }), call({ limit: 5, path: "a" }), call({ path: "b" }), { candidates: [{ finishReason: "STOP" }] }] },
{ chunks: [call({ path: "c" }, { id: "fc_1" }), { candidates: [{ finishReason: "SAFETY" }] }] },
])
const base = fake.url.replace(/\/v1$/, "/v1beta")
const fin = finishOf(await run(new GeminiClient(m("gemini", base, "gemini-2.5-flash"))))
expect(fin.message.parts).toEqual([
{ type: "tool_call", id: "gemini_call_0", name: "read", args: '{"limit":5,"path":"a"}' },
{ type: "tool_call", id: "gemini_call_1", name: "read", args: '{"path":"b"}' },
])
const fin3 = finishOf(await run(new GeminiClient(m("gemini", base, "gemini-3-pro"))))
expect(fin3.message.parts).toEqual([{ type: "tool_call", id: "fc_1", name: "read", args: '{"path":"c"}' }])
expect(fin3.reason).toBe("tool_calls")
})
test("history: the signature rides back on its call; results answer by name; roles merge", () => {
const c = toGeminiContents(
[
{ role: "user", parts: [{ type: "text", text: "go" }] },
{ role: "assistant", parts: [{ type: "reasoning", text: "x" }, { type: "tool_call", id: "gemini_call_0", name: "read", args: '{"path":"a"}', signature: "SIG" }] },
{ role: "tool", callId: "gemini_call_0", name: "read", content: "A" },
{ role: "user", parts: [{ type: "text", text: "and?" }] },
],
false,
)
expect(c).toEqual([
{ role: "user", parts: [{ text: "go" }] },
{ role: "model", parts: [{ functionCall: { name: "read", args: { path: "a" } }, thoughtSignature: "SIG" }] },
{ role: "user", parts: [{ functionResponse: { name: "read", response: { output: "A" } } }] },
// never folded into the tool result: Gemini 3 would read it as part of it
{ role: "model", parts: [{ text: "[The previous response was interrupted before it completed.]" }] },
{ role: "user", parts: [{ text: "and?" }] },
])
})
test("history for Gemini 3: ids on both sides, the sentinel for an unsigned call, JSON results structured unless they hold a $ref", () => {
const c = toGeminiContents(
[
{ role: "user", parts: [{ type: "text", text: "go" }] },
{ role: "assistant", parts: [{ type: "tool_call", id: "c1", name: "read", args: "{}" }, { type: "tool_call", id: "c2", name: "grep", args: "{}" }] },
{ role: "tool", callId: "c1", name: "read", content: '{"lines": 3}' },
{ role: "tool", callId: "c2", name: "grep", content: '{"$ref": "#/$defs/X"}', isError: false },
],
false,
true,
)
expect(c[1]!.parts[0]).toEqual({ functionCall: { name: "read", args: {}, id: "c1" }, thoughtSignature: SKIP_SIGNATURE })
expect(c[2]).toEqual({
role: "user",
parts: [{ functionResponse: { name: "read", response: { lines: 3 }, id: "c1" } }, { functionResponse: { name: "grep", response: { output: '{"$ref": "#/$defs/X"}' }, id: "c2" } }],
})
})
})
describe("ollama", () => {
const nd = (lines: unknown[]) => lines.map((l) => JSON.stringify(l)).join("\n") + "\n"
test("NDJSON: thinking, text, whole tool calls, counts; num_ctx and think in the request", async () => {
fake = fakeProvider([
{
chunks: [],
raw: nd([
{ message: { role: "assistant", content: "", thinking: "Hmm." }, done: false },
{ message: { role: "assistant", content: "Reading." }, done: false },
{ message: { role: "assistant", content: "", tool_calls: [{ function: { name: "read", arguments: { path: "a.ts" } } }] }, done: false },
{ message: { role: "assistant", content: "" }, done: true, done_reason: "stop", prompt_eval_count: 30, eval_count: 7 },
]),
},
])
const ev = await run(new OllamaClient(m("ollama", fake.url.replace(/\/v1$/, ""), "gpt-oss:20b", { context: 32768, max_output: 2000 })), undefined, "high")
expect(fake.calls[0]!.path).toBe("/api/chat")
expect(fake.requests[0]).toMatchObject({ model: "gpt-oss:20b", stream: true, think: "high", options: { num_ctx: 32768, num_predict: 2000 } })
expect(fake.requests[0].messages[0]).toEqual({ role: "system", content: "sys" })
const fin = finishOf(ev)
expect(fin.message.parts).toEqual([
{ type: "reasoning", text: "Hmm." },
{ type: "text", text: "Reading." },
{ type: "tool_call", id: "ollama_call_0", name: "read", args: '{"path":"a.ts"}' },
])
expect(fin.reason).toBe("tool_calls")
expect(ev.find((e) => e.type === "usage")).toEqual({ type: "usage", usage: { input: 30, output: 7 } })
})
test("think: a switch for other models, off when efforts exist but none is chosen; an error line is retried once", async () => {
fake = fakeProvider([{ chunks: [], raw: nd([{ error: "model runner has unexpectedly stopped" }]) }, { chunks: [], raw: nd([{ message: { content: "ok" }, done: true }]) }, { chunks: [], raw: nd([{ message: { content: "ok" }, done: true }]) }])
const base = fake.url.replace(/\/v1$/, "")
const ev = await run(new OllamaClient(m("ollama", base, "qwen3:8b", { efforts: ["low", "high"] })), undefined, "low")
expect(fake.requests[0].think).toBe(true)
expect(ev.some((e) => e.type === "notice" && e.message.includes("unexpectedly stopped"))).toBe(true)
await run(new OllamaClient(m("ollama", base, "qwen3:8b", { efforts: ["low", "high"] })))
expect(fake.requests[2].think).toBe(false)
})
test("/api/show: no `think` for a model that cannot think; num_ctx from the Modelfile, else the trained maximum", async () => {
const show = { capabilities: ["completion", "tools"], parameters: "stop \"<|im_end|>\"\nnum_ctx 16384", model_info: { "qwen3.context_length": 40960 } }
expect(parseShow(show)).toEqual({ capabilities: ["completion", "tools"], numCtx: 16384, trained: 40960 })
fake = fakeProvider([{ chunks: [], raw: nd([{ message: { content: "ok" }, done: true }]) }], { show })
const base = fake.url.replace(/\/v1$/, "")
const c = new OllamaClient(m("ollama", base, "llama3:8b", { efforts: ["low", "high"] }))
await run(c, undefined, "high")
expect(fake.requests[0].think).toBeUndefined()
expect(fake.requests[0].options.num_ctx).toBe(16384)
expect(await c.contextOf("llama3:8b")).toBe(16384)
expect(parseShow({ model_info: { "llama.context_length": 8192 } })).toEqual({ trained: 8192 })
})
})
+1 -1
View File
@@ -190,7 +190,7 @@ test("every {{variable}} in a description is declared, and none reaches a model"
expect(used.sort()).toEqual(Object.keys(s.variables ?? {}).sort())
}
mkdirSync(paths.config, { recursive: true })
writeFileSync(join(paths.config, "connections.yaml"), "connections:\n f:\n dialect: openai-chat\n base_url: http://127.0.0.1:9/v1\n models: { m: {} }\n", { mode: 0o600 })
writeFileSync(join(paths.config, "connections.yaml"), "connections:\n f:\n type: webui\n url: http://127.0.0.1:9/v1\n models: { f/m: {} }\n", { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), "model: f/m\nlimits: { bash_timeout: 90 }\n")
const app = createApp({ cwd: mkdtempSync(join(tmpdir(), "ph-vars-")), store: false, snapshots: false, asker: { ask: async () => ({ kind: "once" }) } })
for (const t of app.engine.o.tools) {
+1 -1
View File
@@ -18,7 +18,7 @@ afterEach(() => fake?.stop())
function app(script: Parameters<typeof fakeProvider>[0]) {
fake = fakeProvider(script)
mkdirSync(paths.config, { recursive: true })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n dialect: openai-chat\n base_url: ${fake.url}\n models: { m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n type: webui\n url: ${fake.url}\n models: { f/m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), "model: f/m\n")
return createApp({ cwd: mkdtempSync(join(tmpdir(), "ph-hl-")), mode: "auto", store: false, snapshots: false, asker: { ask: async (): Promise<AskReply> => ({ kind: "once" }) } })
}
+1 -1
View File
@@ -38,7 +38,7 @@ function pair(): [Transport, Transport] {
function config(url: string, extra = "") {
mkdirSync(paths.config, { recursive: true })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n dialect: openai-chat\n base_url: ${url}\n models: { m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n type: webui\n url: ${url}\n models: { f/m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), `model: f/m\n${extra}`)
}
+1 -1
View File
@@ -51,7 +51,7 @@ describe("an image counts by its dimensions, never its encoding", () => {
{ chunks: [delta({ content: "a screenshot" }), usage(56_000, 5)] },
])
mkdirSync(paths.config, { recursive: true })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n dialect: openai-chat\n base_url: ${fake.url}\n models:\n m: { vision: true, context: 262144 }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n type: webui\n url: ${fake.url}\n models:\n f/m: { vision: true, context: 262144 }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), "model: f/m\n")
const root = mkdtempSync(join(tmpdir(), "ph-img-"))
writeFileSync(join(root, "shot.png"), png(2560, 1440, 1_500_000))
+1 -1
View File
@@ -33,7 +33,7 @@ describe("images", () => {
function setup(vision: boolean, script: Parameters<typeof fakeProvider>[0]) {
fake = fakeProvider(script)
mkdirSync(paths.config, { recursive: true })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n dialect: openai-chat\n base_url: ${fake.url}\n models:\n m: { vision: ${vision} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n type: webui\n url: ${fake.url}\n models:\n f/m: { vision: ${vision} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), "model: f/m\n")
return createApp({ cwd: project(), mode: "edit", store: false, asker: { ask: async (): Promise<AskReply> => ({ kind: "once" }) } })
}
+1 -1
View File
@@ -17,7 +17,7 @@ afterEach(() => fake?.stop())
function app(url: string) {
mkdirSync(paths.config, { recursive: true })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n dialect: openai-chat\n base_url: ${url}\n models: { m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n type: webui\n url: ${url}\n models: { f/m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), "model: f/m\n")
return createApp({ cwd: mkdtempSync(join(tmpdir(), "ph-wake-")), mode: "auto", store: false, asker: { ask: async (): Promise<AskReply> => ({ kind: "once" }) } })
}
+1 -1
View File
@@ -155,7 +155,7 @@ describe("in an app", () => {
function app(script: Parameters<typeof fakeProvider>[0], store?: Store) {
fake = fakeProvider(script)
mkdirSync(paths.config, { recursive: true })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n dialect: openai-chat\n base_url: ${fake.url}\n models: { m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n type: webui\n url: ${fake.url}\n models: { f/m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), "model: f/m\ntitles: prompt\n")
return createApp({ cwd: dir(), mode: "auto", store: store ?? false, snapshots: false, asker: { ask: async (): Promise<AskReply> => ({ kind: "once" }) } })
}
+14 -16
View File
@@ -25,14 +25,16 @@ test("draft-07, described, and no string held to a format ({env:} stands in for
const props = (schemas.config as any).properties
expect(props.mode.enum).toEqual(["manual", "edit", "auto", "plan"])
expect(props.small_model.description).toContain("session titles")
const kinds = (schemas.connections as any).properties.connections.additionalProperties.anyOf
expect(kinds[0].properties.type.const ?? kinds[0].properties.type.enum[0]).toBe("webui")
expect(kinds[1].properties.base_url.description).toContain("Where the API is")
// 2.0: the login is the only kind of entry.
const login = (schemas.connections as any).properties.connections.additionalProperties
expect(login.properties.type.const ?? login.properties.type.enum[0]).toBe("webui")
expect(login.properties.url.description).toContain("The instance's address")
expect(login.properties.dialect).toBeUndefined()
})
const goodConfig = `
model: local/qwen3
small_model: local/tiny
model: llama/qwen3
small_model: llama/tiny
titles: prompt
mode: edit
icons: plain
@@ -59,19 +61,14 @@ mcp:
`
const goodConnections = `
connections:
local:
dialect: openai-chat
base_url: "{env:LLM_URL}"
example:
type: webui
url: https://ai.example.org
api_key: "{file:~/.config/lembas/lembas/example.key}"
tls: { ca: ~/ca.crt }
quirks: { stream_usage: off }
models:
qwen3: { context: 131072, max_output: 65536, efforts: [low, high], effort: low, vision: true }
anthropic:
dialect: anthropic
base_url: https://api.anthropic.com
api_key: "{env:ANTHROPIC_API_KEY}"
models:
claude: { effort_map: { low: 4000, high: 32000 }, cache: true }
llama/qwen3: { context: 131072, max_output: 65536, efforts: [low, high], effort: low, vision: true }
`
test("what the loader accepts, the schema accepts", () => {
@@ -94,7 +91,8 @@ test("…and what it refuses, the schema marks", () => {
expect(Config.safeParse(bad).success).toBe(false)
expect(checkConfig(bad)).toBe(false)
}
for (const bad of [{ connections: { x: { dialect: "openai", base_url: "http://h/v1" } } }, { connections: { x: { dialect: "anthropic" } } }, { connections: { x: { dialect: "anthropic", base_url: "u", models: { m: { context: -1 } } } } }]) {
// A custom connection (1.x): the loader reads past it with a warning, and the schema marks it.
for (const bad of [{ connections: { x: { dialect: "openai-chat", base_url: "http://h/v1" } } }, { connections: { x: { type: "webui" } } }, { connections: { x: { type: "webui", url: "https://h", models: { m: { context: -1 } } } } }, { connections: { x: { type: "webui", url: "https://h", key_cmd: "pass x" } } }]) {
expect(checkConnections(bad)).toBe(false)
}
})
+3 -3
View File
@@ -51,7 +51,7 @@ test("the instance's library replaces this machine's, under the tools' own names
mkdirSync(join(paths.config, "lembas"), { recursive: true })
writeFileSync(join(paths.config, "lembas.json"), JSON.stringify({ instances: { ai: { base_url: lembas.base, connection: "ai", logged_in_at: "" } } }))
writeFileSync(join(paths.config, "lembas", "ai.key"), "lmb_lib\n", { mode: 0o600 })
writeFileSync(join(paths.config, "connections.yaml"), "connections:\n f:\n dialect: openai-chat\n base_url: http://127.0.0.1:1/v1\n models: { m: {} }\n", { mode: 0o600 })
writeFileSync(join(paths.config, "connections.yaml"), "connections:\n f:\n type: webui\n url: http://127.0.0.1:1/v1\n models: { f/m: {} }\n", { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), "model: f/m\nlibrary: lembas\n")
const app = createApp({ cwd: mkdtempSync(join(tmpdir(), "ph-lib-")), store: false, asker: { ask: async (): Promise<AskReply> => ({ kind: "once" }) } })
await app.mcpReady
@@ -74,7 +74,7 @@ test("a session the client gave a library scope sends its id on every call to th
mkdirSync(join(paths.config, "lembas"), { recursive: true })
writeFileSync(join(paths.config, "lembas.json"), JSON.stringify({ instances: { ai: { base_url: lembas.base, connection: "ai", logged_in_at: "" } } }))
writeFileSync(join(paths.config, "lembas", "ai.key"), "lmb_lib\n", { mode: 0o600 })
writeFileSync(join(paths.config, "connections.yaml"), "connections:\n f:\n dialect: openai-chat\n base_url: http://127.0.0.1:1/v1\n models: { m: {} }\n", { mode: 0o600 })
writeFileSync(join(paths.config, "connections.yaml"), "connections:\n f:\n type: webui\n url: http://127.0.0.1:1/v1\n models: { f/m: {} }\n", { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), "model: f/m\nlibrary: lembas\n")
const app = createApp({ cwd: mkdtempSync(join(tmpdir(), "ph-lib-")), asker: { ask: async (): Promise<AskReply> => ({ kind: "once" }) } })
stops.push(() => void app.close())
@@ -104,7 +104,7 @@ test("a session the client gave a library scope sends its id on every call to th
test("without a login it says so and keeps this machine's library", () => {
writeFileSync(join(paths.config, "lembas.json"), JSON.stringify({ instances: {} }))
writeFileSync(join(paths.config, "connections.yaml"), "connections:\n f:\n dialect: openai-chat\n base_url: http://127.0.0.1:1/v1\n models: { m: {} }\n", { mode: 0o600 })
writeFileSync(join(paths.config, "connections.yaml"), "connections:\n f:\n type: webui\n url: http://127.0.0.1:1/v1\n models: { f/m: {} }\n", { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), "model: f/m\nlibrary: lembas\n")
const app = createApp({ cwd: mkdtempSync(join(tmpdir(), "ph-lib-")), store: false, asker: { ask: async (): Promise<AskReply> => ({ kind: "once" }) } })
expect(app.loaded.warnings.join(" ")).toContain("not logged in")
+4 -4
View File
@@ -187,14 +187,14 @@ test("login writes one webui connection named after the instance, and no models"
const loaded = loadConfig()
const c = loaded.connections.example!
expect(c.dialect).toBe("openai-chat")
expect(c.webui?.url).toBe(fake.url)
expect(c.base_url).toBe(`${fake.url}/v1`)
expect(c.api_key).toBe("lmb_secret")
// The instance's models with its settings, the default first; the group named after the connection.
expect(Object.keys(c.models)).toEqual(["qwen", "gemma"])
expect(c.models.qwen).toEqual({ name: "Qwen", context: 131072, temperature: 0.6, top_p: 0.95, efforts: ["low", "high"], tools: true, group: "example:gpu" })
expect(c.models.gemma).toEqual({ context: 32768, vision: true, single_session: true, group: "example:gpu" })
expect(resolveModel(loaded, undefined).ref).toBe("example/qwen")
expect(resolveModel(loaded, undefined).ref).toBe("qwen")
})
test("a model changed on the instance is here at the next start, without logging in again", async () => {
@@ -205,7 +205,7 @@ test("a model changed on the instance is here at the next start, without logging
expect(r).toEqual([{ name: "example", ok: true, models: 1 }])
const loaded = loadConfig()
expect(Object.keys(loaded.connections.example!.models)).toEqual(["llama"])
expect(resolveModel(loaded, undefined).ref).toBe("example/llama")
expect(resolveModel(loaded, undefined).ref).toBe("llama")
// A model the instance does not list yet is still asked for, not refused here.
expect(resolveModel(loaded, "example/brand-new").id).toBe("brand-new")
})
@@ -266,7 +266,7 @@ test("an older kind of login is replaced: the old connection, its key and its pi
const c = cfg()
// It was the instance's default, so the instance decides it now; the rest follow the new name.
expect(c.model).toBeUndefined()
expect(c.small_model).toBe("example/gemma")
expect(c.small_model).toBe("gemma")
expect(r.changes.join("\n")).toContain("the connection ai from an earlier login is now example")
})
+6 -3
View File
@@ -96,10 +96,13 @@ test("an OpenAI-shaped /embeddings: in order, unit length", async () => {
return Response.json({ data: input.map((_, i) => ({ index: i, embedding: [3, 4] })).reverse() })
},
})
const e = embedderFor({ connections: { emb: { dialect: "openai-chat", base_url: `http://127.0.0.1:${server.port}/v1`, models: {} } } } as never, "emb/nomic")!
// The server's embedding model, by its served id.
const loaded = { connections: { emb: { base_url: `http://127.0.0.1:${server.port}/v1`, models: {}, webui: { url: `http://127.0.0.1:${server.port}`, embeddings: ["llama/nomic"] } } }, refs: [] } as never
const e = embedderFor(loaded, "llama/nomic")!
const [v] = await e.embed(["x", "y"])
expect(Array.from(v!)).toEqual([0.6000000238418579, 0.800000011920929])
expect(() => embedderFor({ connections: { a: { dialect: "anthropic", base_url: "http://x", models: {} } } } as never, "a/b")).toThrow("no embeddings")
// Not logged in: said.
expect(() => embedderFor({ connections: {}, refs: [] } as never, "llama/nomic")).toThrow("not logged in")
})
test("ingest: text and code; HTML as markdown; binaries skipped; long text cut and said; links out of the directory ignored", async () => {
@@ -135,7 +138,7 @@ test("the tools: knowledge_search is offered only with something to search; a pr
{ chunks: [delta({ content: "ok" }, "stop")] },
])
mkdirSync(paths.config, { recursive: true })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n dialect: openai-chat\n base_url: ${fake.url}\n models: { m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n type: webui\n url: ${fake.url}\n models: { f/m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), "model: f/m\ntitles: prompt\n")
const cwd = mkdtempSync(join(tmpdir(), "ph-libtools-"))
mkdirSync(join(cwd, ".agent"))
+5 -5
View File
@@ -34,7 +34,7 @@ function repo(): string {
/** The user's own files: a connection and a config.yaml local mode must not read. */
function userFiles(extraConfig = "") {
mkdirSync(globalPaths.config, { recursive: true })
writeFileSync(join(globalPaths.config, "connections.yaml"), "connections:\n mine:\n dialect: openai-chat\n base_url: http://127.0.0.1:9/v1\n models: { m: {} }\n", { mode: 0o600 })
writeFileSync(join(globalPaths.config, "connections.yaml"), "connections:\n mine:\n type: webui\n url: http://127.0.0.1:9/v1\n models: { mine/m: {} }\n", { mode: 0o600 })
writeFileSync(join(globalPaths.config, "config.yaml"), `model: mine/m\ntheme: forest\n${extraConfig}`)
}
@@ -67,7 +67,7 @@ test("a local directory that came with the repository is refused", () => {
expect(isLocal()).toBe(false)
})
test("the user's config.yaml is not read; their connections are, and a local one wins by name", () => {
test("the user's config.yaml is not read; their login is, and a local one wins by name", () => {
userFiles()
const dir = repo()
enterLocal(dir)
@@ -76,10 +76,10 @@ test("the user's config.yaml is not read; their connections are, and a local one
expect(loaded.config.theme).toBeUndefined()
expect(Object.keys(loaded.connections)).toEqual(["mine"])
writeFileSync(join(paths.config, "config.yaml"), "model: mine/m\n")
writeFileSync(join(paths.config, "connections.yaml"), "connections:\n mine:\n dialect: openai-chat\n base_url: http://127.0.0.1:8/v1\n models: { n: {} }\n", { mode: 0o600 })
writeFileSync(join(paths.config, "connections.yaml"), "connections:\n mine:\n type: webui\n url: http://127.0.0.1:8/v1\n models: { mine/n: {} }\n", { mode: 0o600 })
loaded = loadConfig()
expect(loaded.config.model).toBe("mine/m")
expect(Object.keys(loaded.connections.mine!.models)).toEqual(["n"])
expect(Object.keys(loaded.connections.mine!.models)).toEqual(["mine/n"])
})
test("a login made outside local mode is read, never written or signed out", async () => {
@@ -152,7 +152,7 @@ test("the CLI: -l refuses what it turns off, and a run writes nothing outside th
const home = realpathSync(mkdtempSync(join(tmpdir(), "lembas-local-home-")))
const tmp = realpathSync(mkdtempSync(join(tmpdir(), "lembas-local-tmp-")))
mkdirSync(join(home, ".config/lembas"), { recursive: true })
writeFileSync(join(home, ".config/lembas/connections.yaml"), `connections:\n fake:\n dialect: openai-chat\n base_url: ${fake.url}\n models: { m: {} }\n`, { mode: 0o600 })
writeFileSync(join(home, ".config/lembas/connections.yaml"), `connections:\n fake:\n type: webui\n url: ${fake.url}\n models: { fake/m: {} }\n`, { mode: 0o600 })
writeFileSync(join(home, ".config/lembas/config.yaml"), "model: nowhere/x\n")
const dir = repo()
const env = { ...process.env, LEMBAS_HOME: home, HOME: home, TMPDIR: tmp, XDG_DATA_HOME: join(home, ".local/share"), XDG_STATE_HOME: join(home, ".local/state") }
+1 -1
View File
@@ -181,7 +181,7 @@ describe("through the engine", () => {
{ chunks: [delta({ content: "It said it." }, "stop"), usage(20, 5)] },
])
mkdirSync(paths.config, { recursive: true })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n fake:\n dialect: openai-chat\n base_url: ${fake.url}\n models:\n m: { context: 32768 }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n fake:\n type: webui\n url: ${fake.url}\n models:\n fake/m: { context: 32768 }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), `model: fake/m\nmcp:\n fix:\n command: ["${BUN}", "${SERVER}"]\n`)
const dir = mkdtempSync(join(tmpdir(), "lembas-mcp-"))
const asked: string[] = []
+2 -32
View File
@@ -24,7 +24,7 @@ function setup(script: Parameters<typeof fakeProvider>[0], connections: (url: st
}
const two = (url: string) =>
`connections:\n f:\n dialect: openai-chat\n base_url: ${url}\n models:\n main: {}\n big: { name: Big One, notes: slow but careful; best at reviews }\n`
`connections:\n f:\n type: webui\n url: ${url}\n models:\n f/main: {}\n f/big: { name: Big One, notes: slow but careful; best at reviews }\n`
test("the model is told which other models there are, with their notes", async () => {
const { app } = setup([{ chunks: [delta({ content: "ok" }, "stop")] }], two)
@@ -44,38 +44,8 @@ test("task runs a subagent on the model asked for", async () => {
two,
)
expect(await app.engine.prompt("ask the big one")).toBe("stop")
expect(fake!.requests.map((r) => r.model)).toEqual(["main", "big", "main"])
expect(fake!.requests.map((r) => r.model)).toEqual(["f/main", "f/big", "f/main"])
const result = app.engine.messages.find((m) => m.role === "tool")
expect(result && result.role === "tool" && result.content).toContain("Yes.")
})
test("a server that cannot be reached: the model's fallback takes over, said and kept", async () => {
const { app, events } = setup([{ chunks: [delta({ content: "from the fallback" }, "stop")] }], (url) =>
[
"connections:",
" gone:",
" dialect: openai-chat",
" base_url: http://127.0.0.1:9/v1",
" timeout: 5",
" models:",
" main: { fallback: [f/main] }",
" f:",
" dialect: openai-chat",
` base_url: ${url}`,
" models:",
" main: {}",
"",
].join("\n"),
)
app.settings.set("model", "gone/main")
expect(await app.engine.prompt("hi")).toBe("stop")
expect(app.engine.model.ref).toBe("f/main")
expect(events.some((e) => e.type === "notice" && e.message.includes("switching to f/main, its fallback"))).toBe(true)
expect(events.some((e) => e.type === "setting" && e.key === "model")).toBe(true)
})
test("no fallback: the error stands", async () => {
const { app } = setup([], (url) => `connections:\n gone:\n dialect: openai-chat\n base_url: http://127.0.0.1:9/v1\n models:\n main: {}\n f:\n dialect: openai-chat\n base_url: ${url}\n models:\n main: {}\n`)
app.settings.set("model", "gone/main")
expect(await app.engine.prompt("hi")).toBe("error")
})
+1 -1
View File
@@ -85,7 +85,7 @@ test("a model calling notes with an action still gets its note written, and read
{ chunks: [delta({ content: "ok" }, "stop")] },
])
mkdirSync(paths.config, { recursive: true })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n dialect: openai-chat\n base_url: ${fake.url}\n models: { m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n type: webui\n url: ${fake.url}\n models: { f/m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), "model: f/m\ntitles: prompt\n")
const app = createApp({ cwd: mkdtempSync(join(tmpdir(), "ph-names-")), store: false, snapshots: false, asker: { ask: async () => ({ kind: "once" }) } })
for (const n of app.library.notes(["global"])) app.library.deleteNote(n.id)
+2 -2
View File
@@ -137,7 +137,7 @@ test("logged in, the account's personalization is used instead of the local keys
test("/settings writes the personalization to the account, not to config.yaml", async () => {
inst = instance()
model = fakeProvider([])
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n dialect: openai-chat\n base_url: ${model.url}\n models: { m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n type: webui\n url: ${model.url}\n models: { f/m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), "model: f/m\nlibrary: local\n")
loggedIn(inst)
await fetchWebui("example", inst.url, "lmb_key", undefined)
@@ -188,7 +188,7 @@ test("the library: the account's by default when the login may read it; an expli
test("opted out on the web, or a preset the CLI does not know: setting one key here wipes nothing there", async () => {
inst = instance({ personal: { enabled: false, available: true, personality: "pirate", personality_custom: "Arr.", instructions: "Answer in Slovak." } })
model = fakeProvider([])
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n dialect: openai-chat\n base_url: ${model.url}\n models: { m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n type: webui\n url: ${model.url}\n models: { f/m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), "model: f/m\nlibrary: local\n")
loggedIn(inst)
await fetchWebui("example", inst.url, "lmb_key", undefined)
+1 -1
View File
@@ -68,7 +68,7 @@ test("the engine keeps the plan with the session: said on the bus, stored, back
{ chunks: [delta({ content: "ok" })] },
])
mkdirSync(paths.config, { recursive: true })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n dialect: openai-chat\n base_url: ${fake.url}\n models: { m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n type: webui\n url: ${fake.url}\n models: { f/m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), "model: f/m\ntitles: prompt\n")
const cwd = mkdtempSync(join(tmpdir(), "ph-planstore-"))
const store = new Store(join(cwd, "sessions.db"))
+1 -1
View File
@@ -16,7 +16,7 @@ const RENDERED = "# Fix the average\nObjectives: [o1] Tests pass (open)\n## p1 C
function app(script: Parameters<typeof fakeProvider>[0], plan?: (r: PlanRequest) => Promise<PlanReply>) {
fake = fakeProvider(script)
mkdirSync(paths.config, { recursive: true })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n dialect: openai-chat\n base_url: ${fake.url}\n models: { m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n type: webui\n url: ${fake.url}\n models: { f/m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), "model: f/m\n")
const cwd = mkdtempSync(join(tmpdir(), "ph-plan-"))
Bun.spawnSync(["git", "init", "-q", cwd])
+26 -57
View File
@@ -1,8 +1,8 @@
// Models from a LLeMbas instance named by their provider: `deepseek/deepseek-flash`, not
// `example/deepseek-flash`. The old forms still resolve, a config naming one is rewritten, a
// connection of the user's own wins its name, two instances share a provider by login order, and an
// instance that does not speak protocol 2 (bare ids) keeps `<login>/<id>`. On the link, the model event and the
// announcement say whether the session's model is this link's instance's.
// Models of the LLeMbas server named by the id it serves them under: `deepseek/deepseek-flash`, not
// `example/deepseek-flash`. LLeMbas CLI 2.0 talks to one server, so the id is the ref; the older
// forms still resolve and a config naming one is rewritten; a custom connection beside the login is
// read past. On the link, the model event and the announcement say whether the session's model is
// this link's instance's.
import { afterEach, beforeEach, expect, test } from "bun:test"
import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
@@ -35,27 +35,19 @@ function clean() {
rmSync(join(paths.state, "webui"), { recursive: true, force: true })
}
const conns = (o: Record<string, { webui?: boolean; models: string[] }>) =>
Object.fromEntries(Object.entries(o).map(([n, c]) => [n, { dialect: "openai-chat", base_url: "http://x/v1", models: Object.fromEntries(c.models.map((m) => [m, {}])), ...(c.webui ? { webui: { url: "http://x", v2: true } } : {}) }])) as never
const conns = (o: Record<string, { models: string[] }>) =>
Object.fromEntries(Object.entries(o).map(([n, c]) => [n, { base_url: "http://x/v1", models: Object.fromEntries(c.models.map((m) => [m, {}])), webui: { url: "http://x" } }])) as never
test("refs: by provider; a connection of the user's own wins its name; a bare id keeps the login", () => {
const c = conns({ example: { webui: true, models: ["deepseek/deepseek-flash", "llama/bonsai", "old"] }, llama: { models: ["mine"] } })
const refs = modelRefs(c, ["example"])
expect(refs.map((r) => r.ref)).toEqual(["deepseek/deepseek-flash", "example/llama/bonsai", "example/old", "llama/mine"])
// Every old way of writing them.
expect(findRef(c, refs, "example/deepseek-flash")?.ref).toBe("deepseek/deepseek-flash")
expect(findRef(c, refs, "example/deepseek/deepseek-flash")?.ref).toBe("deepseek/deepseek-flash")
expect(findRef(c, refs, "example/bonsai")?.ref).toBe("example/llama/bonsai")
expect(findRef(c, refs, "llama/mine")).toMatchObject({ connection: "llama", id: "mine" })
expect(findRef(c, refs, "llama/bonsai")).toBeUndefined()
})
test("refs: two instances with one provider — the first logged in to keeps the short form", () => {
const c = conns({ example: { webui: true, models: ["deepseek/a"] }, work: { webui: true, models: ["deepseek/a", "openai/b"] } })
expect(modelRefs(c, ["example", "work"]).map((r) => r.ref)).toEqual(["deepseek/a", "work/deepseek/a", "openai/b"])
expect(modelRefs(c, ["work", "example"]).map((r) => r.ref)).toEqual(["example/deepseek/a", "deepseek/a", "openai/b"])
// A login named after its own provider: the same string either way.
expect(modelRefs(conns({ llama: { webui: true, models: ["llama/bonsai"] } }), ["llama"]).map((r) => r.ref)).toEqual(["llama/bonsai"])
test("refs: the served id; the older forms name the same model", () => {
const c = conns({ example: { models: ["deepseek/deepseek-flash", "llama/bonsai", "old"] } })
const refs = modelRefs(c)
expect(refs.map((r) => r.ref)).toEqual(["deepseek/deepseek-flash", "llama/bonsai", "old"])
expect(findRef(c, refs, "example/deepseek-flash")).toMatchObject({ ref: "deepseek/deepseek-flash", old: true })
expect(findRef(c, refs, "example/deepseek/deepseek-flash")).toMatchObject({ ref: "deepseek/deepseek-flash", old: true })
expect(findRef(c, refs, "example/bonsai")?.ref).toBe("llama/bonsai")
expect(findRef(c, refs, "example/old")?.ref).toBe("old")
expect(findRef(c, refs, "llama/bonsai")?.old).toBeUndefined()
expect(findRef(c, refs, "other/bonsai")).toBeUndefined()
})
/** Logged in to an instance served by `fake`, whose models are `models` (as /v1/models said them;
@@ -94,23 +86,13 @@ test("logged in: the model is deepseek/deepseek-flash everywhere, requests go to
await app.close()
})
test("an instance that does not speak protocol 2 (bare ids): <login>/<id>", () => {
fake = fakeProvider([])
loggedIn(["deepseek-flash"])
writeFileSync(join(paths.config, "config.yaml"), "model: example/deepseek-flash\n")
const loaded = loadConfig()
expect(loaded.refs.map((r) => r.ref)).toEqual(["example/deepseek-flash"])
expect(resolveModel(loaded, undefined).ref).toBe("example/deepseek-flash")
expect(readFileSync(join(paths.config, "config.yaml"), "utf8")).toBe("model: example/deepseek-flash\n")
})
test("a connection of the user's own named like a provider keeps it; the instance's is <login>/<provider>/<model>", () => {
test("a custom connection beside the login is read past, and said (2.0 has none)", () => {
fake = fakeProvider([])
loggedIn(["deepseek/deepseek-flash"], ` deepseek:\n dialect: openai-chat\n base_url: ${"http://127.0.0.1:9/v1"}\n models: { deepseek-flash: {} }\n`)
const loaded = loadConfig()
expect(loaded.refs.map((r) => r.ref)).toEqual(["example/deepseek/deepseek-flash", "deepseek/deepseek-flash"])
expect(resolveModel(loaded, "deepseek/deepseek-flash").connectionName).toBe("deepseek")
expect(resolveModel(loaded, "example/deepseek/deepseek-flash").connectionName).toBe("example")
expect(loaded.refs.map((r) => r.ref)).toEqual(["deepseek/deepseek-flash"])
expect(resolveModel(loaded, "deepseek/deepseek-flash").connectionName).toBe("example")
expect(loaded.warnings.some((w) => w.includes("deepseek is a custom connection"))).toBe(true)
})
function pair(): [Transport, Transport] {
@@ -145,13 +127,13 @@ test("on the link: the instance names its model as served, the session reports t
// The audit of 568923c.
test("only the old <login>/<model> is rewritten, once, in place: a pinned long form, anchors and comments stay", () => {
test("an older form is rewritten, once, in place: anchors and comments stay", () => {
fake = fakeProvider([])
loggedIn(["deepseek/deepseek-flash", "llama/nomic-embed"])
// Pinned on purpose: means what it says, and is left alone.
// The login's name in front: one server, so it is the same model, written the old way.
writeFileSync(join(paths.config, "config.yaml"), "model: example/deepseek/deepseek-flash\n")
expect(loadConfig().config.model).toBe("example/deepseek/deepseek-flash")
expect(readFileSync(join(paths.config, "config.yaml"), "utf8")).toBe("model: example/deepseek/deepseek-flash\n")
expect(loadConfig().config.model).toBe("deepseek/deepseek-flash")
expect(readFileSync(join(paths.config, "config.yaml"), "utf8")).toBe("model: deepseek/deepseek-flash\n")
// The old form behind an anchor, with a comment: the scalar changes, nothing else.
writeFileSync(join(paths.config, "config.yaml"), "model: &m example/deepseek-flash # my pick\nsmall_model: *m\nembedding: example/nomic-embed\n")
const l = loadConfig()
@@ -168,25 +150,12 @@ test("only the old <login>/<model> is rewritten, once, in place: a pinned long f
expect(existsSync(join(paths.state, "migrations.json"))).toBe(true)
})
test("the provider: as the instance says it; from a protocol-2 instance, before the /; else the id is just an id", () => {
fake = fakeProvider([])
// Not protocol 2: an id with a / and no provider is `<login>/<id>`.
loggedIn(["org/model-a"], "", { noProvider: true })
expect(loadConfig().refs.map((r) => r.ref)).toEqual(["example/org/model-a"])
// The instance speaks protocol 2: its ids are `<provider>/<model>`.
loggedIn(["org/model-a"], "", { noProvider: true, protocols: [1, 2] })
expect(loadConfig().refs.map((r) => r.ref)).toEqual(["org/model-a"])
// `provider` said: used, whatever the protocol.
loggedIn(["org/model-a"])
expect(loadConfig().refs.map((r) => r.ref)).toEqual(["org/model-a"])
})
test("a models: override keyed by the old bare name lands on the served model; no phantom appears", () => {
fake = fakeProvider([])
loggedIn(["deepseek/deepseek-flash"], " models: { deepseek-flash: { context: 1000 }, upcoming: { context: 5 } }\n")
writeFileSync(join(paths.config, "config.yaml"), "model: example/deepseek-flash\n")
const l = loadConfig()
expect(l.refs.map((r) => r.ref)).toEqual(["deepseek/deepseek-flash", "example/upcoming"])
expect(l.refs.map((r) => r.ref)).toEqual(["deepseek/deepseek-flash", "upcoming"])
const m = resolveModel(l, l.config.model)
expect(m).toMatchObject({ id: "deepseek/deepseek-flash", spec: { context: 1000 } })
})
+1 -1
View File
@@ -13,7 +13,7 @@ afterEach(() => {
})
function model(url: string, spec: ResolvedModel["spec"] = {}, conn: Partial<Connection> = {}): ResolvedModel {
const connection: Connection = { dialect: "openai-chat", base_url: url, models: { m: spec }, ...conn }
const connection: Connection = { base_url: url, models: { m: spec }, ...conn }
return { ref: `c${Math.random().toString(36).slice(2, 7)}/m`, connectionName: "c", connection, id: "m", spec }
}
+1 -1
View File
@@ -51,7 +51,7 @@ test("the asker is told the purpose, and the tool runs without it", async () =>
{ chunks: [delta({ content: "ok" }, "stop")] },
])
mkdirSync(paths.config, { recursive: true })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n dialect: openai-chat\n base_url: ${fake.url}\n models: { m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n type: webui\n url: ${fake.url}\n models: { f/m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), "model: f/m\n")
const asked: (string | undefined)[] = []
const app = createApp({ cwd: mkdtempSync(join(tmpdir(), "ph-purpose-")), store: false, snapshots: false, asker: { ask: async (r): Promise<AskReply> => (asked.push(r.purpose), { kind: "once" }) } })
+5 -5
View File
@@ -18,7 +18,7 @@ afterEach(() => fake?.stop())
async function replay(name: string) {
fake = fakeProvider([{ chunks: [], raw: fixture(name) }])
const m: ResolvedModel = { ref: "swap/x", connectionName: "swap", id: "x", spec: {}, connection: { dialect: "openai-chat", base_url: fake.url, models: {} } }
const m: ResolvedModel = { ref: "swap/x", connectionName: "swap", id: "x", spec: {}, connection: { base_url: fake.url, models: {} } }
const events: StreamEvent[] = []
for await (const e of new OpenAIChatClient(m).stream({ system: "", messages: [{ role: "user", parts: [{ type: "text", text: "x" }] }], tools: [] }))
events.push(e)
@@ -48,7 +48,7 @@ describe("recorded llama-swap streams", () => {
test("qwen35: an answer given only inside reasoning is followed by one request for the answer", async () => {
fake = fakeProvider([{ chunks: [], raw: fixture("qwen35-answer-only-in-reasoning.sse") }, { chunks: [delta({ content: "The divisor was off by one." })] }])
mkdirSync(paths.config, { recursive: true })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n dialect: openai-chat\n base_url: ${fake.url}\n models: { m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n type: webui\n url: ${fake.url}\n models: { f/m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), "model: f/m\n")
const dir = mkdtempSync(join(tmpdir(), "lembas-replay-"))
const app = createApp({ cwd: dir, mode: "edit", store: false, asker: { ask: async (): Promise<AskReply> => ({ kind: "deny" }) } })
@@ -95,7 +95,7 @@ describe("effort refused inside a 200 stream (llama-swap loading, then the templ
connectionName: "swap",
id: "bonsai",
spec: { efforts: ["low", "medium", "high"] },
connection: { dialect: "openai-chat", base_url: fake.url, models: {} },
connection: { base_url: fake.url, models: {} },
}
const events: StreamEvent[] = []
for await (const e of new OpenAIChatClient(m).stream({ system: "", messages: [{ role: "user", parts: [{ type: "text", text: "x" }] }], tools: [], effort: "high" }))
@@ -111,7 +111,7 @@ describe("effort refused inside a 200 stream (llama-swap loading, then the templ
test("an error after output has started is not retried", async () => {
fake = fakeProvider([{ chunks: [], raw: 'data: {"choices":[{"delta":{"content":"partial"}}]}\n\nerror: {"error":{"code":500,"message":"Unexpected reasoning effort high"}}\n\n' }])
const m: ResolvedModel = { ref: "swap/late", connectionName: "swap", id: "x", spec: { efforts: ["high"] }, connection: { dialect: "openai-chat", base_url: fake.url, models: {} } }
const m: ResolvedModel = { ref: "swap/late", connectionName: "swap", id: "x", spec: { efforts: ["high"] }, connection: { base_url: fake.url, models: {} } }
const run = async () => {
for await (const _ of new OpenAIChatClient(m).stream({ system: "", messages: [], tools: [], effort: "high" })) void _
}
@@ -123,7 +123,7 @@ describe("effort refused inside a 200 stream (llama-swap loading, then the templ
describe("llama-swap unloads the model under a running request", () => {
test("'group: model unloaded' (a string code) is a server error: retried once, and the reply arrives", async () => {
fake = fakeProvider([{ chunks: [], raw: fixture("llama-swap-model-unloaded.sse") }, { chunks: [delta({ content: "second try" })] }])
const m: ResolvedModel = { ref: "swap/x", connectionName: "swap", id: "x", spec: {}, connection: { dialect: "openai-chat", base_url: fake.url, models: {} } }
const m: ResolvedModel = { ref: "swap/x", connectionName: "swap", id: "x", spec: {}, connection: { base_url: fake.url, models: {} } }
const events: StreamEvent[] = []
for await (const e of new OpenAIChatClient(m).stream({ system: "", messages: [{ role: "user", parts: [{ type: "text", text: "x" }] }], tools: [] })) events.push(e)
expect(events.some((e) => e.type === "notice" && e.message.includes("group: model unloaded"))).toBe(true)
-83
View File
@@ -1,83 +0,0 @@
import { afterEach, describe, expect, test } from "bun:test"
import { readFileSync } from "node:fs"
import { join } from "node:path"
import { ResponsesClient, toResponsesInput } from "../src/provider/responses.ts"
import type { Message, ResolvedModel, StreamEvent } from "../src/provider/types.ts"
import { fakeProvider, type Fake } from "./fake-provider.ts"
let fake: Fake | undefined
afterEach(() => fake?.stop())
const model = (url: string, spec: ResolvedModel["spec"] = {}): ResolvedModel => ({ ref: "r/m", connectionName: "r", id: "m", spec, connection: { dialect: "responses", base_url: url, models: {} } })
const sse = (events: Record<string, unknown>[]) => events.map((e) => `data: ${JSON.stringify(e)}\n\n`).join("")
async function run(c: ResponsesClient, messages: Message[] = [{ role: "user", parts: [{ type: "text", text: "hi" }] }], effort: any = null) {
const ev: StreamEvent[] = []
for await (const e of c.stream({ system: "sys", messages, tools: [{ name: "list", description: "d", parameters: { type: "object" } }], effort })) ev.push(e)
return ev
}
describe("responses dialect", () => {
test("a real vLLM stream: reasoning text, then a function call, with usage", async () => {
fake = fakeProvider([{ chunks: [], raw: readFileSync(join(import.meta.dir, "fixtures/sse/vllm-responses-reasoning-tool.sse"), "utf8") }])
const ev = await run(new ResponsesClient(model(fake.url)))
const fin = ev.find((e) => e.type === "finish")!
if (fin.type !== "finish") throw new Error()
expect(fin.reason).toBe("tool_calls")
expect(fin.message.parts[0]).toMatchObject({ type: "reasoning" })
expect(fin.message.parts[1]).toMatchObject({ type: "tool_call", name: "list" })
expect(ev.find((e) => e.type === "usage")).toMatchObject({ usage: { input: 4788, output: 111, reasoning: 21 } })
})
test("request shape; an encrypted reasoning item goes back verbatim on the next turn", async () => {
const reasoningItem = { id: "rs_1", type: "reasoning", summary: [{ type: "summary_text", text: "plan" }], encrypted_content: "ENC" }
fake = fakeProvider([
{
chunks: [],
raw: sse([
{ type: "response.reasoning_summary_text.delta", item_id: "rs_1", delta: "plan" },
{ type: "response.output_item.done", item: reasoningItem },
{ type: "response.output_item.added", item: { type: "function_call", id: "fc_1", call_id: "call_1", name: "list" } },
{ type: "response.function_call_arguments.delta", item_id: "fc_1", delta: "{}" },
{ type: "response.output_item.done", item: { type: "function_call", id: "fc_1", call_id: "call_1", name: "list", arguments: "{}" } },
{ type: "response.completed", response: { status: "completed", usage: { input_tokens: 10, output_tokens: 5, input_tokens_details: { cached_tokens: 4 } } } },
]),
},
{ chunks: [], raw: sse([{ type: "response.output_text.delta", delta: "done" }, { type: "response.completed", response: { status: "completed" } }]) },
])
const c = new ResponsesClient(model(fake.url, { max_output: 999 }))
const first = await run(c, undefined, "high")
const fin = first.find((e) => e.type === "finish")!
if (fin.type !== "finish") throw new Error()
expect(first.find((e) => e.type === "usage")).toEqual({ type: "usage", usage: { input: 10, output: 5, reasoning: undefined, cached: 4 } })
expect(fake.requests[0]).toMatchObject({ model: "m", store: false, instructions: "sys", max_output_tokens: 999, reasoning: { effort: "high", summary: "auto" }, include: ["reasoning.encrypted_content"] })
expect(fake.requests[0].tools[0]).toMatchObject({ type: "function", name: "list" })
await run(c, [{ role: "user", parts: [{ type: "text", text: "hi" }] }, fin.message, { role: "tool", callId: "call_1", name: "list", content: "a.txt" }], "high")
expect(fake.requests[1].input).toEqual([
{ role: "user", content: [{ type: "input_text", text: "hi" }] },
reasoningItem,
{ type: "function_call", call_id: "call_1", name: "list", arguments: "{}" },
{ type: "function_call_output", call_id: "call_1", output: "a.txt" },
])
})
test("a server that does not know `include` gets one retry without it", async () => {
fake = fakeProvider([{ status: 400, body: '{"error":{"message":"Unknown parameter: include"}}' }, { chunks: [], raw: sse([{ type: "response.output_text.delta", delta: "ok" }]) }])
await run(new ResponsesClient(model(fake.url)), undefined, "low")
expect(fake.requests[1].include).toBeUndefined()
expect(fake.requests[1].reasoning).toEqual({ effort: "low", summary: "auto" })
})
test("history from another dialect: reasoning without an item is not sent; images only with vision", () => {
const input = toResponsesInput(
[
{ role: "user", parts: [{ type: "text", text: "see" }, { type: "image", mime: "image/png", data: "AAA" }] },
{ role: "assistant", parts: [{ type: "reasoning", text: "thought elsewhere" }, { type: "text", text: "ok" }] },
],
true,
)
expect(input).toEqual([
{ role: "user", content: [{ type: "input_text", text: "see" }, { type: "input_image", image_url: "data:image/png;base64,AAA" }] },
{ role: "assistant", content: [{ type: "output_text", text: "ok" }] },
])
})
})
+1 -1
View File
@@ -19,7 +19,7 @@ const yes = { ask: async (): Promise<AskReply> => ({ kind: "once" }) }
function setup(script: Parameters<typeof fakeProvider>[0]) {
fake = fakeProvider(script)
mkdirSync(paths.config, { recursive: true })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n fake:\n dialect: openai-chat\n base_url: ${fake.url}\n models:\n m: { context: 32768 }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n fake:\n type: webui\n url: ${fake.url}\n models:\n fake/m: { context: 32768 }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), "model: fake/m\n")
const dir = mkdtempSync(join(tmpdir(), "lembas-results-"))
writeFileSync(join(dir, "a.txt"), "one\ntwo\nthree\n")
+1 -1
View File
@@ -41,7 +41,7 @@ function pair(): [Transport, Transport] {
function config(url: string) {
mkdirSync(paths.config, { recursive: true })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n dialect: openai-chat\n base_url: ${url}\n models: { m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n type: webui\n url: ${url}\n models: { f/m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), "model: f/m\n")
}
+2 -3
View File
@@ -21,7 +21,7 @@ function home(config = "model: f/m\n", script: Parameters<typeof fakeProvider>[0
mkdirSync(paths.config, { recursive: true })
writeFileSync(
join(paths.config, "connections.yaml"),
`connections:\n f:\n dialect: openai-chat\n base_url: ${fake.url}\n models:\n m: { efforts: [low, medium, high], effort: medium }\n big: {}\n`,
`connections:\n f:\n type: webui\n url: ${fake.url}\n models:\n f/m: { efforts: [low, medium, high], effort: medium }\n f/big: {}\n`,
{ mode: 0o600 },
)
writeFileSync(join(paths.config, "config.yaml"), config)
@@ -68,7 +68,6 @@ test("session, global and project scope: what is written where, and where a valu
expect(readFileSync(join(cwd, ".agent/config.yaml"), "utf8")).toContain("auto_at: 0.6")
expect(s.get("compaction.auto_at")).toMatchObject({ value: 0.6, source: "project" })
expect(() => s.set("model", "f/nope")).toThrow()
expect(() => s.set("effort", "max")).toThrow("takes")
expect(() => s.set("update.channel", "beta", "project")).toThrow("can be set for: global")
})
@@ -92,7 +91,7 @@ test("the agent changes a setting through the tool, with approval; a model chang
expect(asked[0]!.always).toEqual(["session effort=*"])
expect(app.engine.model.ref).toBe("f/big")
// The third request went to the new model.
expect(fake!.requests[2]!.model).toBe("big")
expect(fake!.requests[2]!.model).toBe("f/big")
})
test("settings_tool: allow — no asking; but a less strict mode is asked every time, with no 'always'", async () => {
+1 -1
View File
@@ -22,7 +22,7 @@ function setup(script: Parameters<typeof fakeProvider>[0], config = "") {
mkdirSync(paths.config, { recursive: true })
writeFileSync(
join(paths.config, "connections.yaml"),
`connections:\n f:\n dialect: openai-chat\n base_url: ${fake.url}\n models:\n main: { context: 32768 }\n a: {}\n`,
`connections:\n f:\n type: webui\n url: ${fake.url}\n models:\n f/main: { context: 32768 }\n f/a: {}\n`,
{ mode: 0o600 },
)
writeFileSync(join(paths.config, "config.yaml"), `model: f/main\ntitles: prompt\n${config}`)
+11 -12
View File
@@ -13,17 +13,15 @@ let fake: Fake | undefined
let other: Fake | undefined
afterEach(() => (fake?.stop(), other?.stop(), (other = undefined)))
/** Two scripted servers: the parent's model on `f`, the child's on `g` (task's `model: "g/m"`) —
* a shared script would hand a reply to whichever engine asked first. */
/** Two scripts: the parent's model `f/m`, the child's `g/…` (task's `model: "g/m"`), routed by the
* fake server — a shared script would hand a reply to whichever engine asked first. */
function pair(parent: Parameters<typeof fakeProvider>[0], child: Parameters<typeof fakeProvider>[0], o: { oneAtATime?: boolean; config?: string; asker?: Asker } = {}) {
fake = fakeProvider(parent)
other = fakeProvider(child)
const child$ = (other = fakeProvider(child))
fake = fakeProvider(parent, { route: (model) => (model.startsWith("g/") ? child$ : undefined) })
mkdirSync(paths.config, { recursive: true })
writeFileSync(
join(paths.config, "connections.yaml"),
`connections:\n f:\n dialect: openai-chat\n base_url: ${fake.url}\n models: { m: {} }\n g:\n dialect: openai-chat\n base_url: ${other.url}\n${o.oneAtATime ? " one_model_at_a_time: true\n" : ""} models: { m: {}, n: {} }\n`,
{ mode: 0o600 },
)
// One server; its g models answered by the child's script, sharing one GPU when asked to.
const g = o.oneAtATime ? "{ group: gpu }" : "{}"
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n type: webui\n url: ${fake.url}\n models: { f/m: {}, g/m: ${g}, g/n: ${g} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), `model: f/m\n${o.config ?? ""}`)
const cwd = mkdtempSync(join(tmpdir(), "ph-sub-"))
writeFileSync(join(cwd, "calc.py"), "def average(v):\n return sum(v) / (len(v) - 1)\n")
@@ -40,7 +38,7 @@ const settled = async (a: ReturnType<typeof createApp>, n: number) => {
function app(script: Parameters<typeof fakeProvider>[0]) {
fake = fakeProvider(script)
mkdirSync(paths.config, { recursive: true })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n dialect: openai-chat\n base_url: ${fake.url}\n models: { m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n type: webui\n url: ${fake.url}\n models: { f/m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), "model: f/m\n")
const cwd = mkdtempSync(join(tmpdir(), "ph-sub-"))
writeFileSync(join(cwd, "calc.py"), "def average(v):\n return sum(v) / (len(v) - 1)\n")
@@ -176,7 +174,7 @@ test("a worktree subagent cannot touch the main checkout, even unrestricted", as
{ chunks: [delta({ content: "tried" })] },
{ chunks: [delta({ content: "ok" })] },
])
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n dialect: openai-chat\n base_url: ${fake.url}\n models: { m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n type: webui\n url: ${fake.url}\n models: { f/m: {} }\n`, { mode: 0o600 })
for (const args of [["init", "-q", "-b", "main"], ["config", "user.email", "t@e"], ["config", "user.name", "T"], ["config", "commit.gpgsign", "false"], ["add", "calc.py"], ["commit", "-q", "-m", "init"]]) git(root, args)
const b = createApp({ cwd: root, mode: "auto", store: false, asker: { ask: async (): Promise<AskReply> => ({ kind: "once" }) } })
await b.engine.prompt("go")
@@ -300,8 +298,9 @@ describe("background subagents (spec 2.0.0)", () => {
const results = fake!.requests[1].messages.filter((m: any) => m.role === "tool").map((m: any) => m.content as string)
expect(results[0]).toStartWith("[#0001]\nStarted subagent explore as job1")
expect(results[1]).toContain("holds one model at a time, and a subagent is already running on g/m there")
expect(other!.requests.map((r: any) => r.model)).toEqual(["m"])
await settled(a, 3)
for (let i = 0; i < 100 && !other!.requests.length; i++) await Bun.sleep(20)
expect(other!.requests.map((r: any) => r.model)).toEqual(["g/m"])
})
test("a broken agent file is said at start, and the agent is still offered with its body", () => {
+3 -3
View File
@@ -18,7 +18,7 @@ function setup(script: Parameters<typeof fakeProvider>[0], config = "") {
mkdirSync(paths.config, { recursive: true })
writeFileSync(
join(paths.config, "connections.yaml"),
`connections:\n fake:\n dialect: openai-chat\n base_url: ${fake.url}\n models:\n m: { context: 32768 }\n tiny: { context: 8192 }\n`,
`connections:\n fake:\n type: webui\n url: ${fake.url}\n models:\n fake/m: { context: 32768 }\n fake/tiny: { context: 8192 }\n`,
{ mode: 0o600 },
)
writeFileSync(join(paths.config, "config.yaml"), `model: fake/m\n${config}`)
@@ -111,8 +111,8 @@ describe("a session gets a title", () => {
const app = createApp({ cwd: dir, asker: noAsk, store, snapshots: false })
await app.engine.prompt("something")
await until(() => store.session(app.engine.sessionId!)!.title === "Tiny title")
expect(fake!.requests[0].model).toBe("m")
expect(fake!.requests[1].model).toBe("tiny")
expect(fake!.requests[0].model).toBe("fake/m")
expect(fake!.requests[1].model).toBe("fake/tiny")
})
test("a failed title request leaves the prompt's title", async () => {
+1 -1
View File
@@ -31,7 +31,7 @@ const model = (tls?: { ca?: string; insecure?: boolean }): ResolvedModel => ({
connectionName: "t",
id: "m",
spec: {},
connection: { dialect: "openai-chat", base_url: `https://127.0.0.1:${server.port}/v1`, tls, models: {} },
connection: { base_url: `https://127.0.0.1:${server.port}/v1`, tls, models: {} },
})
test("a private CA is refused by default, with advice", async () => {
+1 -1
View File
@@ -39,7 +39,7 @@ test("The CLI's own files under .agent that are links are refused, not written t
test("an untrusted project's task board is neither in the prompt nor writable", async () => {
mkdirSync(paths.config, { recursive: true })
writeFileSync(join(paths.config, "connections.yaml"), "connections:\n f:\n dialect: openai-chat\n base_url: http://127.0.0.1:9/v1\n models: { m: {} }\n", { mode: 0o600 })
writeFileSync(join(paths.config, "connections.yaml"), "connections:\n f:\n type: webui\n url: http://127.0.0.1:9/v1\n models: { f/m: {} }\n", { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), "model: f/m\n")
const root = mkdtempSync(join(tmpdir(), "ph-untrusted-"))
mkdirSync(join(root, ".agent"))
+1 -1
View File
@@ -14,7 +14,7 @@ afterEach(() => setup?.renderer.destroy())
async function boot(cwd: string) {
const fake = fakeProvider([])
mkdirSync(paths.config, { recursive: true })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n fake:\n dialect: openai-chat\n base_url: ${fake.url}\n models: { m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n fake:\n type: webui\n url: ${fake.url}\n models: { fake/m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), "model: fake/m\n")
let quit = 0
setup = await testRender(() => <Boot options={{ cwd, store: false }} onQuit={() => quit++} />, { width: 100, height: 30 })
+1 -1
View File
@@ -16,7 +16,7 @@ export async function tui(script: Scripted[], size = { width: 100, height: 30 },
mkdirSync(paths.config, { recursive: true })
writeFileSync(
join(paths.config, "connections.yaml"),
`connections:\n fake:\n dialect: openai-chat\n base_url: ${fake.url}\n models:\n coder: { context: 32768, efforts: [low, high], effort: low }\n other: { context: 8192 }\n`,
`connections:\n fake:\n type: webui\n url: ${fake.url}\n models:\n fake/coder: { context: 32768, efforts: [low, high], effort: low }\n fake/other: { context: 8192 }\n`,
{ mode: 0o600 },
)
writeFileSync(join(paths.config, "config.yaml"), `model: fake/coder\n${extraConfig}`)
+4 -4
View File
@@ -31,16 +31,16 @@ test("no model yet: the sign-in is first and focused; ctrl+r opens the session o
return setup!.captureCharFrame()
}
let f = await frame()
expect(f).toContain("no model to talk to yet")
// 2.0: the server's models or none — the screen is the sign-in, and nothing else to fill in.
expect(f).toContain("not logged in to a LLeMbas server")
expect(f).toContain("Log in to a LLeMbas instance")
// Before the file instructions: the screen opens on the sign-in.
expect(f.indexOf("Log in to a LLeMbas instance")).toBeLessThan(f.indexOf("connections.yaml"))
expect(f).not.toContain("connections.yaml")
// Focused: what is typed is the address, without pressing l first.
await setup.mockInput.typeText("ai.example")
f = await frame()
expect(f).toContain("Address: ai.example")
// A model added meanwhile: ctrl+r, a letter-free key, opens the session.
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n fake:\n dialect: openai-chat\n base_url: ${fake.url}\n models: { m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n fake:\n type: webui\n url: ${fake.url}\n models: { fake/m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), "model: fake/m\n")
setup.mockInput.pressKey("r", { ctrl: true })
f = await frame()