@justin06lee/yagami 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +114 -95
- package/dist/{chunk-ASS6MJ7C.js → chunk-2UG5CR5X.js} +273 -6
- package/dist/chunk-2UG5CR5X.js.map +1 -0
- package/dist/{chunk-M5UHR273.js → chunk-GPX47OIO.js} +80 -20
- package/dist/chunk-GPX47OIO.js.map +1 -0
- package/dist/cli.js +33 -8
- package/dist/cli.js.map +1 -1
- package/dist/{engine-pmCK3S7z.d.ts → hostConfig-Bzr9JB8R.d.ts} +20 -1
- package/dist/index.d.ts +199 -4
- package/dist/index.js +77 -3
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +3 -3
- package/dist/server.js +4 -3
- package/package.json +2 -2
- package/dist/chunk-ASS6MJ7C.js.map +0 -1
- package/dist/chunk-M5UHR273.js.map +0 -1
package/README.md
CHANGED
|
@@ -4,54 +4,62 @@
|
|
|
4
4
|
|
|
5
5
|
# yagami
|
|
6
6
|
|
|
7
|
-
**Your signed-in coding-agent CLIs as one self-hosted Anthropic-compatible API.**<br>
|
|
8
|
-
*Claude Code, Codex, OpenCode, Gemini CLI and any ACP agent — point any Anthropic client at your own subscriptions, or embed the engine as a library.*
|
|
7
|
+
**Your signed-in coding-agent CLIs as one self-hosted Anthropic- and OpenAI-compatible API.**<br>
|
|
8
|
+
*Claude Code, Codex, OpenCode, Gemini CLI and any ACP agent — point any Anthropic or OpenAI client at your own subscriptions, or embed the engine as a library with zero config.*
|
|
9
9
|
|
|
10
10
|
</div>
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
-
yagami does the T3-Code trick, generalized: it drives the coding-agent CLIs you already installed and logged into — Claude Code through the Agent SDK (`pathToClaudeCodeExecutable`), Codex through `codex exec`, and OpenCode, Gemini CLI, Copilot, Cursor, Qwen Code, Kimi, Goose and the rest of the [ACP registry](https://agentclientprotocol.com) through the Agent Client Protocol. No API keys from any vendor, no separate auth — each spawned engine uses the same login your terminal sessions do.
|
|
14
|
+
yagami does the T3-Code trick, generalized: it drives the coding-agent CLIs you already installed and logged into — Claude Code through the Agent SDK (`pathToClaudeCodeExecutable`), Codex through `codex exec`, and OpenCode, Gemini CLI, Copilot, Cursor, Qwen Code, Kimi, Goose and the rest of the [ACP registry](https://agentclientprotocol.com) through the Agent Client Protocol. No API keys from any vendor, no separate auth — each spawned engine uses the same login your terminal sessions do.
|
|
15
|
+
|
|
16
|
+
There are two doors in, and they're deliberately different:
|
|
17
|
+
|
|
18
|
+
| | Who it's for | What you get |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| **Server** — the `yagami` binary | A machine that should hand out an API key (a home server, a box your other apps talk to) | `yagami start` prints a **URL + API key** that works with any app that accepts an Anthropic *or* OpenAI base URL + key |
|
|
21
|
+
| **Library** — `@justin06lee/yagami` | An app running *on* the machine with the signed-in CLIs | `new Yagami()` — **no URL, no API key, nothing to configure**. It finds the CLIs itself and syncs with the binary's config |
|
|
15
22
|
|
|
16
23
|
> **Personal use only.** This exists so *you* can point *your own tools* at *your own subscriptions*. Offering subscription-backed access to other people is against every one of these vendors' terms. Keep the endpoint private and don't share keys.
|
|
17
24
|
|
|
18
|
-
##
|
|
25
|
+
## Server mode
|
|
19
26
|
|
|
20
27
|
```sh
|
|
21
28
|
make # bun install + build + install `yagami` onto your PATH (~/.local/bin)
|
|
22
29
|
yagami start # first run generates + saves an API key and prints it
|
|
23
30
|
```
|
|
24
31
|
|
|
25
|
-
`make update` stops any running yagami server, rebuilds, reinstalls, and restarts it.
|
|
26
|
-
|
|
27
|
-
Or install the published package from npm:
|
|
28
|
-
|
|
29
|
-
```sh
|
|
30
|
-
bun add -g @justin06lee/yagami # global `yagami` CLI
|
|
31
|
-
bun add @justin06lee/yagami # or as a library (see Library mode)
|
|
32
|
-
```
|
|
32
|
+
`make update` stops any running yagami server, rebuilds, reinstalls, and restarts it. Or install from npm: `bun add -g @justin06lee/yagami`.
|
|
33
33
|
|
|
34
34
|
```
|
|
35
|
-
yagami v0.
|
|
35
|
+
yagami v0.5.0
|
|
36
36
|
listening http://127.0.0.1:8787
|
|
37
37
|
provider claude — /Users/you/.local/bin/claude (2.1.238 (Claude Code))
|
|
38
38
|
also codex, opencode (use model "<provider>:<model>")
|
|
39
39
|
api key ygm_…
|
|
40
|
+
|
|
41
|
+
Connect apps — either dialect, same key (`yagami key` prints ready-to-paste env exports):
|
|
42
|
+
Anthropic apps baseURL http://127.0.0.1:8787 (ANTHROPIC_BASE_URL / ANTHROPIC_API_KEY)
|
|
43
|
+
OpenAI apps baseURL http://127.0.0.1:8787/v1 (OPENAI_BASE_URL / OPENAI_API_KEY)
|
|
40
44
|
```
|
|
41
45
|
|
|
42
|
-
|
|
46
|
+
Apps can't be reached by a key alone — they need somewhere to route — so the pair is always **URL + key**. Most apps take them as a "base URL" field or the standard env vars; `yagami key` prints both ready to paste:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
export ANTHROPIC_BASE_URL=http://127.0.0.1:8787 ANTHROPIC_API_KEY=ygm_...
|
|
50
|
+
export OPENAI_BASE_URL=http://127.0.0.1:8787/v1 OPENAI_API_KEY=ygm_...
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Anthropic-dialect apps speak to `POST /v1/messages`, OpenAI-dialect apps to `POST /v1/chat/completions` — same engine, same key, streaming included:
|
|
43
54
|
|
|
44
55
|
```ts
|
|
45
56
|
import Anthropic from "@anthropic-ai/sdk";
|
|
57
|
+
const anthropic = new Anthropic({ baseURL: "http://127.0.0.1:8787", apiKey: process.env.YAGAMI_KEY });
|
|
58
|
+
await anthropic.messages.create({ model: "sonnet", max_tokens: 1024, messages: [{ role: "user", content: "hello" }] });
|
|
46
59
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
await client.messages.create({ model: "sonnet", max_tokens: 1024, messages: [{ role: "user", content: "hello" }] });
|
|
53
|
-
await client.messages.create({ model: "codex:gpt-5.6-sol", max_tokens: 1024, messages: [{ role: "user", content: "hello" }] });
|
|
54
|
-
await client.messages.create({ model: "opencode:anthropic/claude-sonnet-4", max_tokens: 1024, messages: [{ role: "user", content: "hello" }] });
|
|
60
|
+
import OpenAI from "openai";
|
|
61
|
+
const openai = new OpenAI({ baseURL: "http://127.0.0.1:8787/v1", apiKey: process.env.YAGAMI_KEY });
|
|
62
|
+
await openai.chat.completions.create({ model: "codex:gpt-5.6-sol", messages: [{ role: "user", content: "hello" }] });
|
|
55
63
|
```
|
|
56
64
|
|
|
57
65
|
Or raw curl:
|
|
@@ -60,8 +68,84 @@ Or raw curl:
|
|
|
60
68
|
curl http://127.0.0.1:8787/v1/messages \
|
|
61
69
|
-H "x-api-key: ygm_..." -H "content-type: application/json" \
|
|
62
70
|
-d '{"model":"codex","max_tokens":64,"messages":[{"role":"user","content":"ping"}]}'
|
|
71
|
+
|
|
72
|
+
curl http://127.0.0.1:8787/v1/chat/completions \
|
|
73
|
+
-H "authorization: Bearer ygm_..." -H "content-type: application/json" \
|
|
74
|
+
-d '{"model":"codex","messages":[{"role":"user","content":"ping"}]}'
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
One caveat for OpenAI-dialect apps: the `model` field still routes through yagami's providers — set it to a model your CLIs actually serve (`sonnet`, `codex:gpt-5.6-sol`, `opencode:…`), not whatever `gpt-*` id the app defaults to.
|
|
78
|
+
|
|
79
|
+
## Library mode
|
|
80
|
+
|
|
81
|
+
For apps that run on the machine with the signed-in CLIs — a desktop app, a script, anything embedding the engine in-process. **No server, no URL, no API key**: `new Yagami()` auto-detects the CLIs and reads `~/.config/yagami/config.json` if the binary has one, so library and server stay in sync automatically.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import { Yagami } from "@justin06lee/yagami";
|
|
85
|
+
|
|
86
|
+
const yagami = new Yagami(); // that's it — hooks straight into the host's CLIs
|
|
87
|
+
|
|
88
|
+
// Anthropic SDK shape:
|
|
89
|
+
const msg = await yagami.messages.create({ messages: [{ role: "user", content: "hello" }] });
|
|
90
|
+
for await (const ev of yagami.messages.create({ messages: [...], stream: true })) { /* Anthropic stream events */ }
|
|
91
|
+
|
|
92
|
+
// OpenAI SDK shape, same engine:
|
|
93
|
+
const completion = await yagami.chat.completions.create({ messages: [{ role: "user", content: "hello" }] });
|
|
94
|
+
for await (const chunk of yagami.chat.completions.create({ messages: [...], stream: true })) { /* chat chunks */ }
|
|
95
|
+
|
|
96
|
+
// Models across every installed harness:
|
|
97
|
+
const { data } = await yagami.models.list();
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Options are for overrides only (`new Yagami({ defaultModel: "sonnet" })`, `{ defaultProvider: "codex" }`, `{ syncHostConfig: false }`, …); the zero-argument form is the intended use. For lower-level control the engine underneath is `yagami.engine` (a `YagamiEngine` — `complete()` returns cost/session/provider metadata, `stream()` returns raw SSE events), and you can hand-pick providers instead of auto-detecting:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { YagamiEngine, ClaudeProvider, AcpProvider } from "@justin06lee/yagami";
|
|
104
|
+
|
|
105
|
+
const engine = new YagamiEngine({
|
|
106
|
+
providers: [new ClaudeProvider(), new AcpProvider({ id: "gemini", label: "Gemini", command: "gemini", args: ["--acp"] })],
|
|
107
|
+
});
|
|
108
|
+
const { response, costUsd } = await engine.complete({ messages: [{ role: "user", content: "hello" }] });
|
|
63
109
|
```
|
|
64
110
|
|
|
111
|
+
Every provider implements one small `Provider` contract (`run(turn)` → normalized `session`/`text`/`thinking`/`done` events, plus `listModels()` and `version()`), so adding a harness that isn't ACP-capable is one file. Failures are typed: `AuthRequiredError` (carries the login command), `ProviderNotInstalledError` (carries the install hint), `ProviderError`.
|
|
112
|
+
|
|
113
|
+
### Building a UI on Claude Code
|
|
114
|
+
|
|
115
|
+
`Yagami`/`YagamiEngine` are completions-only by design. To build an actual coding UI — tools, permissions, plan mode, a warm session across turns — use `AgentSession`, which wraps the full Claude Code agent with the lifecycle the interactive terminal gives you for free:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import { AgentSession } from "@justin06lee/yagami";
|
|
119
|
+
|
|
120
|
+
const session = new AgentSession({
|
|
121
|
+
cwd: "/path/to/project",
|
|
122
|
+
parity: "terminal", // load your CLAUDE.md, skills, hooks, .mcp.json — like the CLI
|
|
123
|
+
appName: "my-app", // reported to Claude as the client
|
|
124
|
+
onPermission: async (req) => {
|
|
125
|
+
// Your approve/deny UI. Policy stays here; yagami owns the state machine.
|
|
126
|
+
const ok = await showDialog(req.toolName, req.input);
|
|
127
|
+
return ok ? { behavior: "allow" } : { behavior: "deny", message: "user declined" };
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
session.send("fix the failing test"); // process starts here and stays warm
|
|
132
|
+
for await (const msg of session) { // raw SDKMessages — render however you like
|
|
133
|
+
render(msg);
|
|
134
|
+
if (msg.type === "result") break;
|
|
135
|
+
}
|
|
136
|
+
session.send("now add a test for the edge case"); // next turn resumes the same session
|
|
137
|
+
await session.interrupt(); // the CLI's Esc
|
|
138
|
+
await session.setModel("opus"); // the CLI's /model
|
|
139
|
+
await session.setPermissionMode("plan"); // shift+tab
|
|
140
|
+
session.close();
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
This resolves the parts of embedding Claude Code that every host would otherwise reimplement identically — process lifecycle, session resume, interrupt, settings parity, and the permission state machine. What stays yours are the genuinely app-specific choices: rendering the `SDKMessage` stream, deciding what to auto-approve, and picking the working directory. `parity` is `"terminal"` (load user+project+local settings, matching your CLI), `"project"` (project+local only), or `"isolated"` (load nothing — reproducible, no personal config). The permission `fallback` defaults to `"deny"`, so a session is safe before the UI is wired up; `autoAllow`/`autoDeny` skip the handler for named tools.
|
|
144
|
+
|
|
145
|
+
For a lower-level handle, `claudeCodeSession(prompt, { options })` returns the raw Agent SDK `Query`.
|
|
146
|
+
|
|
147
|
+
The server is also embeddable: `import { startYagami } from "@justin06lee/yagami/server"`.
|
|
148
|
+
|
|
65
149
|
## Providers
|
|
66
150
|
|
|
67
151
|
A bare model id goes to the **default provider** (Claude Code unless you change it). `"<provider>:<model>"` routes to another harness; a bare provider id (`"codex"`) means that harness's own default model. `GET /v1/models` and `yagami models` list everything that's actually installed, with ids ready to paste.
|
|
@@ -97,6 +181,7 @@ Any other ACP agent works too — add it to config with its launch command:
|
|
|
97
181
|
| `yagami start` | Start the server (`-p` port, `-H` host, `--provider <id>` default provider, `--claude <path>`, `--cors`). Add `--daemon` to run it in the background (`--log <file>` overrides the default log at `~/.config/yagami/yagami.log`) |
|
|
98
182
|
| `yagami stop` | Stop the running server |
|
|
99
183
|
| `yagami status` | Show whether it's running, plus uptime, request count, and cumulative would-be API cost |
|
|
184
|
+
| `yagami key` | Print the URL + API key, plus ready-to-paste `ANTHROPIC_*`/`OPENAI_*` env exports for client apps |
|
|
100
185
|
| `yagami models` | List models across every installed provider (`--provider <id>` to filter) |
|
|
101
186
|
| `yagami keygen` | Generate another API key and save it to the config |
|
|
102
187
|
| `yagami doctor` | Check every harness CLI; `--live` sends one tiny real completion (`--provider <id>` to pick which) |
|
|
@@ -119,80 +204,14 @@ Every request is logged as one line (time, status, model, duration, cost, sessio
|
|
|
119
204
|
}
|
|
120
205
|
```
|
|
121
206
|
|
|
122
|
-
Env overrides: `YAGAMI_HOST`, `YAGAMI_PORT`, `YAGAMI_API_KEY`, `YAGAMI_PROVIDER`, `YAGAMI_DEFAULT_MODEL`, `YAGAMI_CLAUDE_PATH`, `YAGAMI_CODEX_PATH`. The older `claudePath` / `claudeConfigDir` keys still work as shorthands for `providers.claude`.
|
|
123
|
-
|
|
124
|
-
## Library mode
|
|
125
|
-
|
|
126
|
-
For apps that want the engine in-process with no HTTP hop (e.g. a desktop app):
|
|
127
|
-
|
|
128
|
-
```ts
|
|
129
|
-
import { YagamiEngine, claudeCodeSession, ClaudeProvider, CodexProvider, AcpProvider } from "@justin06lee/yagami";
|
|
130
|
-
|
|
131
|
-
// 1. Anthropic-shaped completions across every installed harness:
|
|
132
|
-
const engine = new YagamiEngine({ defaultModel: "sonnet" });
|
|
133
|
-
const { response } = await engine.complete({ messages: [{ role: "user", content: "hello" }] });
|
|
134
|
-
const codex = await engine.complete({ model: "codex", messages: [{ role: "user", content: "hello" }] });
|
|
135
|
-
|
|
136
|
-
// streaming (Anthropic SSE event objects):
|
|
137
|
-
const { events } = engine.stream({ model: "opencode", messages: [...], stream: true });
|
|
138
|
-
for await (const ev of events) { /* ev.event, ev.data */ }
|
|
139
|
-
|
|
140
|
-
// hand-pick providers instead of auto-detecting:
|
|
141
|
-
const custom = new YagamiEngine({
|
|
142
|
-
providers: [new ClaudeProvider(), new AcpProvider({ id: "gemini", label: "Gemini", command: "gemini", args: ["--acp"] })],
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
// 2. Full agentic Claude Code sessions — tools, permissions, the works.
|
|
146
|
-
for await (const msg of claudeCodeSession("fix the failing test", {
|
|
147
|
-
options: { cwd: "/path/to/project", permissionMode: "acceptEdits" },
|
|
148
|
-
})) {
|
|
149
|
-
// render SDK messages however you like
|
|
150
|
-
}
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
Every provider implements one small `Provider` contract (`run(turn)` → normalized `session`/`text`/`thinking`/`done` events, plus `listModels()` and `version()`), so adding a harness that isn't ACP-capable is one file. Failures are typed: `AuthRequiredError` (carries the login command), `ProviderNotInstalledError` (carries the install hint), `ProviderError`.
|
|
154
|
-
|
|
155
|
-
### Building a UI on Claude Code
|
|
156
|
-
|
|
157
|
-
`YagamiEngine` is completions-only by design. To build an actual coding UI — tools, permissions, plan mode, a warm session across turns — use `AgentSession`, which wraps the full Claude Code agent with the lifecycle the interactive terminal gives you for free:
|
|
158
|
-
|
|
159
|
-
```ts
|
|
160
|
-
import { AgentSession } from "@justin06lee/yagami";
|
|
161
|
-
|
|
162
|
-
const session = new AgentSession({
|
|
163
|
-
cwd: "/path/to/project",
|
|
164
|
-
parity: "terminal", // load your CLAUDE.md, skills, hooks, .mcp.json — like the CLI
|
|
165
|
-
appName: "my-app", // reported to Claude as the client
|
|
166
|
-
onPermission: async (req) => {
|
|
167
|
-
// Your approve/deny UI. Policy stays here; yagami owns the state machine.
|
|
168
|
-
const ok = await showDialog(req.toolName, req.input);
|
|
169
|
-
return ok ? { behavior: "allow" } : { behavior: "deny", message: "user declined" };
|
|
170
|
-
},
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
session.send("fix the failing test"); // process starts here and stays warm
|
|
174
|
-
for await (const msg of session) { // raw SDKMessages — render however you like
|
|
175
|
-
render(msg);
|
|
176
|
-
if (msg.type === "result") break;
|
|
177
|
-
}
|
|
178
|
-
session.send("now add a test for the edge case"); // next turn resumes the same session
|
|
179
|
-
await session.interrupt(); // the CLI's Esc
|
|
180
|
-
await session.setModel("opus"); // the CLI's /model
|
|
181
|
-
await session.setPermissionMode("plan"); // shift+tab
|
|
182
|
-
session.close();
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
This resolves the parts of embedding Claude Code that every host would otherwise reimplement identically — process lifecycle, session resume, interrupt, settings parity, and the permission state machine. What stays yours are the genuinely app-specific choices: rendering the `SDKMessage` stream, deciding what to auto-approve, and picking the working directory. `parity` is `"terminal"` (load user+project+local settings, matching your CLI), `"project"` (project+local only), or `"isolated"` (load nothing — reproducible, no personal config). The permission `fallback` defaults to `"deny"`, so a session is safe before the UI is wired up; `autoAllow`/`autoDeny` skip the handler for named tools.
|
|
186
|
-
|
|
187
|
-
For a lower-level handle, `claudeCodeSession(prompt, { options })` returns the raw Agent SDK `Query`.
|
|
188
|
-
|
|
189
|
-
The server is also embeddable: `import { startYagami } from "@justin06lee/yagami/server"`.
|
|
207
|
+
Env overrides: `YAGAMI_HOST`, `YAGAMI_PORT`, `YAGAMI_API_KEY`, `YAGAMI_PROVIDER`, `YAGAMI_DEFAULT_MODEL`, `YAGAMI_CLAUDE_PATH`, `YAGAMI_CODEX_PATH`. The older `claudePath` / `claudeConfigDir` keys still work as shorthands for `providers.claude`. Library mode reads the same file (minus the server-only fields — host, port, keys), which is what keeps an embedded `Yagami` and the binary in agreement.
|
|
190
208
|
|
|
191
209
|
## How it works
|
|
192
210
|
|
|
193
|
-
- **Engine**: each
|
|
211
|
+
- **Engine**: each request becomes one sandboxed turn on the chosen harness. Claude runs with `tools: []`, `settingSources: []` (your CLAUDE.md/skills never leak into API completions), `maxTurns: 1` and a deny-all permission callback; Codex runs in its read-only sandbox with no approvals; ACP agents are moved to a plan/read-only mode when they offer one and every permission request is refused. All of them work in a throwaway directory. The API is text-in/text-out; a leaked key can burn tokens but never edit anything on the host — though note that agents other than Claude keep their own read-only tools, so they can still *look* at that empty directory.
|
|
212
|
+
- **Dialects**: `POST /v1/messages` is native. `POST /v1/chat/completions` translates OpenAI shapes at the edge — system/developer messages fold into `system`, `image_url` parts become image blocks, streams are re-emitted as `chat.completion.chunk` events ending in `[DONE]`, and thinking output rides along as `reasoning_content`. Errors on that path come back OpenAI-shaped too. `GET /v1/models` serves one merged shape both SDKs parse.
|
|
194
213
|
- **Multi-turn**: the Messages API is stateless but harness sessions aren't. yagami hashes each conversation prefix (per provider) and remembers which session produced it; a follow-up request resumes that session and sends only the new user message. Unmatched histories fall back to replaying the transcript in a single prompt, and if a cached session turns out to be gone, the stale mapping is dropped and the request transparently retries via replay. The cache persists across restarts at `~/.config/yagami/sessions.json`.
|
|
195
|
-
- **Streaming**: every harness's output is normalized into deltas and re-emitted as a proper Anthropic SSE sequence — `message_start` → thinking/text content blocks → `message_delta` → `message_stop
|
|
214
|
+
- **Streaming**: every harness's output is normalized into deltas and re-emitted as a proper Anthropic SSE sequence — `message_start` → thinking/text content blocks → `message_delta` → `message_stop` (or the OpenAI chunk sequence on the chat-completions path). Claude and ACP agents stream tokens; Codex streams per message part.
|
|
196
215
|
- **Models**: `GET /v1/models` asks each installed CLI what it supports (Claude via the SDK, Codex via its app-server protocol, ACP agents via their session config) — probed once per process, then cached. Failed probes are skipped and retried next time; a static fallback list is served only if nothing answers (`x-yagami-models-source` says which).
|
|
197
216
|
- **Auth**: `x-api-key` or `Authorization: Bearer`, compared in constant time. Binds to `127.0.0.1` by default and warns loudly on anything else.
|
|
198
217
|
|
|
@@ -200,11 +219,11 @@ Extra response headers: `x-yagami-provider`, `x-yagami-cost-usd` (what the turn
|
|
|
200
219
|
|
|
201
220
|
## Limitations
|
|
202
221
|
|
|
203
|
-
- No `tools` / `tool_choice` (rejected with 400 — by design, see above). `tool_use`/`tool_result` content blocks are rejected too.
|
|
222
|
+
- No `tools` / `tool_choice` / function calling (rejected with 400 — by design, see above). `tool_use`/`tool_result` content blocks and OpenAI `tool`/`function` messages are rejected too.
|
|
204
223
|
- User messages may contain `text`, `image`, and `document` blocks (documents: Claude only; images: base64 sources only outside Claude); `system` and assistant messages are text-only. Thinking blocks echoed back in assistant history are dropped, not rejected. A conversation whose *history* contains images/documents can only be continued while the server that produced it still has that session cached.
|
|
205
224
|
- Assistant prefill (a trailing `assistant` message) is emulated: the engine is instructed to continue from the prefill text, and the response carries only the continuation, like the real API. An accidentally repeated prefill is stripped from the reply, including mid-stream.
|
|
206
|
-
- `max_tokens`, `temperature`, `top_p`, `top_k`, `stop_sequences` are accepted but ignored (reported via `x-yagami-ignored`) — none of the CLI engines expose them.
|
|
207
|
-
- `thinking` and a yagami-extension `effort` ("low"…"max") are passed through where the harness supports them (see the provider table) and reported as ignored elsewhere.
|
|
225
|
+
- `max_tokens`, `temperature`, `top_p`, `top_k`, `stop_sequences` (and their OpenAI counterparts, plus `presence_penalty`, `seed`, `response_format`, …) are accepted but ignored (reported via `x-yagami-ignored`) — none of the CLI engines expose them. OpenAI `n` must be 1.
|
|
226
|
+
- `thinking` and a yagami-extension `effort` ("low"…"max") are passed through where the harness supports them (see the provider table) and reported as ignored elsewhere. OpenAI `reasoning_effort` maps onto `effort`.
|
|
208
227
|
- Cost is reported only by harnesses that price their own turns (Claude, OpenCode); Codex reports token usage without cost.
|
|
209
228
|
|
|
210
229
|
## Development
|
|
@@ -180,7 +180,7 @@ function resolveClaudeExecutable(explicit) {
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
// src/version.ts
|
|
183
|
-
var VERSION = "0.
|
|
183
|
+
var VERSION = "0.5.0";
|
|
184
184
|
|
|
185
185
|
// src/core/providers/acp.ts
|
|
186
186
|
import { spawn, spawnSync } from "child_process";
|
|
@@ -1115,13 +1115,13 @@ function detectProviders(config = {}) {
|
|
|
1115
1115
|
const preset = presetFor(id);
|
|
1116
1116
|
const entry = config[id] ?? {};
|
|
1117
1117
|
const command = entry.command ?? preset?.command ?? id;
|
|
1118
|
-
const
|
|
1118
|
+
const path8 = findExecutable(command, entry.path ? { explicit: entry.path } : {});
|
|
1119
1119
|
return {
|
|
1120
1120
|
id,
|
|
1121
1121
|
label: entry.label ?? preset?.label ?? id,
|
|
1122
1122
|
kind: preset?.kind ?? "acp",
|
|
1123
|
-
installed:
|
|
1124
|
-
...
|
|
1123
|
+
installed: path8 !== void 0 && entry.enabled !== false,
|
|
1124
|
+
...path8 ? { path: path8 } : {},
|
|
1125
1125
|
loginCommand: entry.loginCommand ?? preset?.loginCommand ?? `${command} (sign in per its docs)`,
|
|
1126
1126
|
installHint: preset?.installHint ?? `Install \`${command}\`.`
|
|
1127
1127
|
};
|
|
@@ -1792,6 +1792,266 @@ ${promptText}`;
|
|
|
1792
1792
|
}
|
|
1793
1793
|
};
|
|
1794
1794
|
|
|
1795
|
+
// src/core/hostConfig.ts
|
|
1796
|
+
import * as fs7 from "fs";
|
|
1797
|
+
import * as os6 from "os";
|
|
1798
|
+
import * as path7 from "path";
|
|
1799
|
+
function yagamiConfigDir() {
|
|
1800
|
+
return process.env["YAGAMI_CONFIG_DIR"] ?? path7.join(os6.homedir(), ".config", "yagami");
|
|
1801
|
+
}
|
|
1802
|
+
function loadHostEngineConfig() {
|
|
1803
|
+
let file = {};
|
|
1804
|
+
try {
|
|
1805
|
+
file = JSON.parse(fs7.readFileSync(path7.join(yagamiConfigDir(), "config.json"), "utf8"));
|
|
1806
|
+
} catch {
|
|
1807
|
+
}
|
|
1808
|
+
const env = process.env;
|
|
1809
|
+
const defaultProvider = env["YAGAMI_PROVIDER"] ?? (typeof file["defaultProvider"] === "string" ? file["defaultProvider"] : void 0);
|
|
1810
|
+
const defaultModel = env["YAGAMI_DEFAULT_MODEL"] ?? (typeof file["defaultModel"] === "string" ? file["defaultModel"] : void 0);
|
|
1811
|
+
const claudePath = env["YAGAMI_CLAUDE_PATH"] ?? (typeof file["claudePath"] === "string" ? file["claudePath"] : void 0);
|
|
1812
|
+
const claudeConfigDir = typeof file["claudeConfigDir"] === "string" ? file["claudeConfigDir"] : void 0;
|
|
1813
|
+
const providers = file["providers"];
|
|
1814
|
+
return {
|
|
1815
|
+
...defaultProvider ? { defaultProvider } : {},
|
|
1816
|
+
...defaultModel ? { defaultModel } : {},
|
|
1817
|
+
...providers != null && typeof providers === "object" && !Array.isArray(providers) ? { providerConfig: providers } : {},
|
|
1818
|
+
...claudePath ? { claudePath } : {},
|
|
1819
|
+
...claudeConfigDir ? { claudeConfigDir } : {}
|
|
1820
|
+
};
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
// src/core/openai.ts
|
|
1824
|
+
var IGNORED_OPENAI_PARAMS = [
|
|
1825
|
+
"presence_penalty",
|
|
1826
|
+
"frequency_penalty",
|
|
1827
|
+
"logit_bias",
|
|
1828
|
+
"logprobs",
|
|
1829
|
+
"top_logprobs",
|
|
1830
|
+
"seed",
|
|
1831
|
+
"user",
|
|
1832
|
+
"response_format",
|
|
1833
|
+
"prediction",
|
|
1834
|
+
"modalities",
|
|
1835
|
+
"audio",
|
|
1836
|
+
"store",
|
|
1837
|
+
"parallel_tool_calls",
|
|
1838
|
+
"web_search_options"
|
|
1839
|
+
];
|
|
1840
|
+
var EFFORT_MAP = {
|
|
1841
|
+
minimal: "low",
|
|
1842
|
+
low: "low",
|
|
1843
|
+
medium: "medium",
|
|
1844
|
+
high: "high",
|
|
1845
|
+
xhigh: "xhigh",
|
|
1846
|
+
max: "max"
|
|
1847
|
+
};
|
|
1848
|
+
function imagePartToBlock(part) {
|
|
1849
|
+
const url = part["image_url"]?.["url"];
|
|
1850
|
+
if (typeof url !== "string" || url.length === 0) {
|
|
1851
|
+
throw new ApiError(400, "invalid_request_error", "`image_url` parts must carry an `image_url.url` string");
|
|
1852
|
+
}
|
|
1853
|
+
const dataUrl = /^data:([^;,]+);base64,(.+)$/s.exec(url);
|
|
1854
|
+
if (dataUrl) {
|
|
1855
|
+
return { type: "image", source: { type: "base64", media_type: dataUrl[1], data: dataUrl[2] } };
|
|
1856
|
+
}
|
|
1857
|
+
if (/^https?:\/\//.test(url)) {
|
|
1858
|
+
return { type: "image", source: { type: "url", url } };
|
|
1859
|
+
}
|
|
1860
|
+
throw new ApiError(400, "invalid_request_error", "`image_url.url` must be a data: URL or an http(s) URL");
|
|
1861
|
+
}
|
|
1862
|
+
function partsToContent(parts, role) {
|
|
1863
|
+
const blocks = [];
|
|
1864
|
+
for (const part of parts) {
|
|
1865
|
+
if (part?.type === "text" && typeof part["text"] === "string") {
|
|
1866
|
+
blocks.push({ type: "text", text: part["text"] });
|
|
1867
|
+
} else if (part?.type === "image_url" && role === "user") {
|
|
1868
|
+
blocks.push(imagePartToBlock(part));
|
|
1869
|
+
} else {
|
|
1870
|
+
throw new ApiError(
|
|
1871
|
+
400,
|
|
1872
|
+
"invalid_request_error",
|
|
1873
|
+
`unsupported content part type "${String(part?.type)}" for role "${role}" (yagami supports "text", plus "image_url" in user messages)`
|
|
1874
|
+
);
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
return blocks.every((b) => b.type === "text") ? blocks.map((b) => b["text"]).join("\n") : blocks;
|
|
1878
|
+
}
|
|
1879
|
+
function messageText(content, role) {
|
|
1880
|
+
if (content == null) return "";
|
|
1881
|
+
if (typeof content === "string") return content;
|
|
1882
|
+
const flattened = partsToContent(content, role);
|
|
1883
|
+
if (typeof flattened !== "string") {
|
|
1884
|
+
throw new ApiError(400, "invalid_request_error", `"${role}" messages may only contain text parts`);
|
|
1885
|
+
}
|
|
1886
|
+
return flattened;
|
|
1887
|
+
}
|
|
1888
|
+
function chatToMessagesRequest(body) {
|
|
1889
|
+
if (body == null || typeof body !== "object") {
|
|
1890
|
+
throw new ApiError(400, "invalid_request_error", "request body must be a JSON object");
|
|
1891
|
+
}
|
|
1892
|
+
if (body.tools != null || body.tool_choice != null || body.functions != null || body.function_call != null) {
|
|
1893
|
+
throw new ApiError(
|
|
1894
|
+
400,
|
|
1895
|
+
"invalid_request_error",
|
|
1896
|
+
"yagami does not support `tools`/function calling: the backing engine runs as a pure completions endpoint and never executes or emits tool calls."
|
|
1897
|
+
);
|
|
1898
|
+
}
|
|
1899
|
+
if (body.n != null && body.n !== 1) {
|
|
1900
|
+
throw new ApiError(400, "invalid_request_error", "`n` must be 1 (yagami produces a single completion)");
|
|
1901
|
+
}
|
|
1902
|
+
if (!Array.isArray(body.messages) || body.messages.length === 0) {
|
|
1903
|
+
throw new ApiError(400, "invalid_request_error", "`messages` must be a non-empty array");
|
|
1904
|
+
}
|
|
1905
|
+
const systemParts = [];
|
|
1906
|
+
const messages = [];
|
|
1907
|
+
for (const [i, m] of body.messages.entries()) {
|
|
1908
|
+
const role = m?.role;
|
|
1909
|
+
if (role === "system" || role === "developer") {
|
|
1910
|
+
systemParts.push(messageText(m.content, role));
|
|
1911
|
+
} else if (role === "user") {
|
|
1912
|
+
messages.push({ role: "user", content: Array.isArray(m.content) ? partsToContent(m.content, "user") : m.content ?? "" });
|
|
1913
|
+
} else if (role === "assistant") {
|
|
1914
|
+
messages.push({ role: "assistant", content: messageText(m.content, "assistant") });
|
|
1915
|
+
} else if (role === "tool" || role === "function") {
|
|
1916
|
+
throw new ApiError(400, "invalid_request_error", "yagami does not support tool/function messages (tool calling is disabled by design)");
|
|
1917
|
+
} else {
|
|
1918
|
+
throw new ApiError(400, "invalid_request_error", `messages[${i}].role must be "system", "developer", "user", or "assistant"`);
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
const extraIgnored = IGNORED_OPENAI_PARAMS.filter((p) => body[p] != null).map(String);
|
|
1922
|
+
let effort;
|
|
1923
|
+
if (body.reasoning_effort != null) {
|
|
1924
|
+
effort = EFFORT_MAP[String(body.reasoning_effort)];
|
|
1925
|
+
if (!effort) extraIgnored.push("reasoning_effort");
|
|
1926
|
+
}
|
|
1927
|
+
const maxTokens = body.max_completion_tokens ?? body.max_tokens;
|
|
1928
|
+
const system = systemParts.filter((s) => s.length > 0).join("\n\n");
|
|
1929
|
+
const req = {
|
|
1930
|
+
...body.model !== void 0 ? { model: body.model } : {},
|
|
1931
|
+
messages,
|
|
1932
|
+
...system.length > 0 ? { system } : {},
|
|
1933
|
+
...maxTokens !== void 0 ? { max_tokens: maxTokens } : {},
|
|
1934
|
+
...body.temperature !== void 0 ? { temperature: body.temperature } : {},
|
|
1935
|
+
...body.top_p !== void 0 ? { top_p: body.top_p } : {},
|
|
1936
|
+
...body.stop != null ? { stop_sequences: Array.isArray(body.stop) ? body.stop : [body.stop] } : {},
|
|
1937
|
+
...body.metadata != null ? { metadata: body.metadata } : {},
|
|
1938
|
+
...body.service_tier != null ? { service_tier: String(body.service_tier) } : {},
|
|
1939
|
+
...effort ? { effort } : {},
|
|
1940
|
+
...body.stream === true ? { stream: true } : {}
|
|
1941
|
+
};
|
|
1942
|
+
return { req, extraIgnored, includeUsage: body.stream_options?.include_usage === true };
|
|
1943
|
+
}
|
|
1944
|
+
function finishReason(stopReason) {
|
|
1945
|
+
return stopReason === "max_tokens" ? "length" : "stop";
|
|
1946
|
+
}
|
|
1947
|
+
function toOpenAiUsage(usage) {
|
|
1948
|
+
return {
|
|
1949
|
+
prompt_tokens: usage.input_tokens,
|
|
1950
|
+
completion_tokens: usage.output_tokens,
|
|
1951
|
+
total_tokens: usage.input_tokens + usage.output_tokens
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
function toChatCompletion(resp) {
|
|
1955
|
+
const text = resp.content.filter((b) => b.type === "text").map((b) => String(b["text"] ?? "")).join("");
|
|
1956
|
+
const thinking = resp.content.filter((b) => b.type === "thinking").map((b) => String(b["thinking"] ?? "")).join("");
|
|
1957
|
+
return {
|
|
1958
|
+
id: resp.id.replace(/^msg_/, "chatcmpl_"),
|
|
1959
|
+
object: "chat.completion",
|
|
1960
|
+
created: Math.floor(Date.now() / 1e3),
|
|
1961
|
+
model: resp.model,
|
|
1962
|
+
choices: [
|
|
1963
|
+
{
|
|
1964
|
+
index: 0,
|
|
1965
|
+
message: { role: "assistant", content: text, ...thinking ? { reasoning_content: thinking } : {}, refusal: null },
|
|
1966
|
+
finish_reason: finishReason(resp.stop_reason),
|
|
1967
|
+
logprobs: null
|
|
1968
|
+
}
|
|
1969
|
+
],
|
|
1970
|
+
usage: toOpenAiUsage(resp.usage)
|
|
1971
|
+
};
|
|
1972
|
+
}
|
|
1973
|
+
function openAiErrorBody(err) {
|
|
1974
|
+
return { error: { message: err.message, type: err.type, param: null, code: null } };
|
|
1975
|
+
}
|
|
1976
|
+
var ChatChunkTranslator = class {
|
|
1977
|
+
constructor(includeUsage) {
|
|
1978
|
+
this.includeUsage = includeUsage;
|
|
1979
|
+
}
|
|
1980
|
+
includeUsage;
|
|
1981
|
+
id = "chatcmpl_stream";
|
|
1982
|
+
model = "";
|
|
1983
|
+
created = Math.floor(Date.now() / 1e3);
|
|
1984
|
+
stopReason = null;
|
|
1985
|
+
usage = { input_tokens: 0, output_tokens: 0 };
|
|
1986
|
+
/** Set when the engine reported an error mid-stream (no [DONE] after). */
|
|
1987
|
+
errored = false;
|
|
1988
|
+
chunk(delta, finish = null) {
|
|
1989
|
+
return {
|
|
1990
|
+
id: this.id,
|
|
1991
|
+
object: "chat.completion.chunk",
|
|
1992
|
+
created: this.created,
|
|
1993
|
+
model: this.model,
|
|
1994
|
+
choices: [{ index: 0, delta, finish_reason: finish }]
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1997
|
+
/** Translate one engine SSE event into zero or more OpenAI chunk payloads. */
|
|
1998
|
+
push(ev) {
|
|
1999
|
+
const data = ev.data;
|
|
2000
|
+
switch (ev.event) {
|
|
2001
|
+
case "message_start": {
|
|
2002
|
+
const message = data["message"];
|
|
2003
|
+
if (message?.id) this.id = message.id.replace(/^msg_/, "chatcmpl_");
|
|
2004
|
+
if (message?.model) this.model = message.model;
|
|
2005
|
+
return [this.chunk({ role: "assistant", content: "" })];
|
|
2006
|
+
}
|
|
2007
|
+
case "content_block_delta": {
|
|
2008
|
+
const delta = data["delta"];
|
|
2009
|
+
if (delta?.type === "text_delta" && delta.text) return [this.chunk({ content: delta.text })];
|
|
2010
|
+
if (delta?.type === "thinking_delta" && delta.thinking) return [this.chunk({ reasoning_content: delta.thinking })];
|
|
2011
|
+
return [];
|
|
2012
|
+
}
|
|
2013
|
+
case "message_delta": {
|
|
2014
|
+
const delta = data["delta"];
|
|
2015
|
+
if (delta?.stop_reason) this.stopReason = delta.stop_reason;
|
|
2016
|
+
const usage = data["usage"];
|
|
2017
|
+
if (usage) this.usage = usage;
|
|
2018
|
+
return [];
|
|
2019
|
+
}
|
|
2020
|
+
case "message_stop": {
|
|
2021
|
+
const out = [this.chunk({}, finishReason(this.stopReason))];
|
|
2022
|
+
if (this.includeUsage) {
|
|
2023
|
+
out.push({
|
|
2024
|
+
id: this.id,
|
|
2025
|
+
object: "chat.completion.chunk",
|
|
2026
|
+
created: this.created,
|
|
2027
|
+
model: this.model,
|
|
2028
|
+
choices: [],
|
|
2029
|
+
usage: toOpenAiUsage(this.usage)
|
|
2030
|
+
});
|
|
2031
|
+
}
|
|
2032
|
+
return out;
|
|
2033
|
+
}
|
|
2034
|
+
case "error": {
|
|
2035
|
+
this.errored = true;
|
|
2036
|
+
const error = data["error"];
|
|
2037
|
+
return [{ error: { message: error?.message ?? "stream error", type: error?.type ?? "api_error", param: null, code: null } }];
|
|
2038
|
+
}
|
|
2039
|
+
default:
|
|
2040
|
+
return [];
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
};
|
|
2044
|
+
function modelListBody(models) {
|
|
2045
|
+
const created = Math.floor(Date.now() / 1e3);
|
|
2046
|
+
return {
|
|
2047
|
+
object: "list",
|
|
2048
|
+
data: models.map((m) => ({ type: "model", object: "model", created, owned_by: "yagami", ...m })),
|
|
2049
|
+
has_more: false,
|
|
2050
|
+
...models[0] ? { first_id: models[0].id } : {},
|
|
2051
|
+
...models.length > 0 ? { last_id: models[models.length - 1].id } : {}
|
|
2052
|
+
};
|
|
2053
|
+
}
|
|
2054
|
+
|
|
1795
2055
|
export {
|
|
1796
2056
|
ApiError,
|
|
1797
2057
|
YagamiError,
|
|
@@ -1816,6 +2076,13 @@ export {
|
|
|
1816
2076
|
loadProviders,
|
|
1817
2077
|
detectProviders,
|
|
1818
2078
|
SessionCache,
|
|
1819
|
-
YagamiEngine
|
|
2079
|
+
YagamiEngine,
|
|
2080
|
+
yagamiConfigDir,
|
|
2081
|
+
loadHostEngineConfig,
|
|
2082
|
+
chatToMessagesRequest,
|
|
2083
|
+
toChatCompletion,
|
|
2084
|
+
openAiErrorBody,
|
|
2085
|
+
ChatChunkTranslator,
|
|
2086
|
+
modelListBody
|
|
1820
2087
|
};
|
|
1821
|
-
//# sourceMappingURL=chunk-
|
|
2088
|
+
//# sourceMappingURL=chunk-2UG5CR5X.js.map
|