@justin06lee/yagami 0.4.1 → 0.6.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 +141 -95
- package/dist/{chunk-M5UHR273.js → chunk-D2PNH6GV.js} +80 -20
- package/dist/chunk-D2PNH6GV.js.map +1 -0
- package/dist/{chunk-ASS6MJ7C.js → chunk-ZYHC7PXX.js} +887 -13
- package/dist/chunk-ZYHC7PXX.js.map +1 -0
- package/dist/cli.js +33 -8
- package/dist/cli.js.map +1 -1
- package/dist/{engine-pmCK3S7z.d.ts → hostConfig-HmZ3z297.d.ts} +114 -2
- package/dist/index.d.ts +207 -5
- package/dist/index.js +79 -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)
|
|
44
|
+
```
|
|
45
|
+
|
|
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_...
|
|
40
51
|
```
|
|
41
52
|
|
|
42
|
-
|
|
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,111 @@ 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
|
+
### Building a UI on any other harness
|
|
148
|
+
|
|
149
|
+
The same idea works for the non-Claude harnesses — verbatim. Codex and every ACP agent implement `SessionProvider.openSession()`: a live, warm session on the harness's own engine (`codex app-server` — what the Codex TUI runs on; a persistent ACP connection for OpenCode, Gemini, and friends), with the harness's own config, sandbox, and approval flow. Nothing is overridden unless you pass `native` overrides; approval requests are forwarded to your handler exactly as the harness's own UI would prompt.
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
import { createProvider, isSessionProvider } from "@justin06lee/yagami";
|
|
153
|
+
|
|
154
|
+
const codex = createProvider("codex", {}, { appName: "my-app" });
|
|
155
|
+
if (isSessionProvider(codex)) {
|
|
156
|
+
const session = codex.openSession({
|
|
157
|
+
cwd: "/path/to/project",
|
|
158
|
+
permissions: {
|
|
159
|
+
decide: async (req) => (await showDialog(req.tool, req.input)) ? "allow" : "deny",
|
|
160
|
+
}, // "allow_always" answers like the TUI's "don't ask again"
|
|
161
|
+
});
|
|
162
|
+
for await (const ev of session.send("fix the failing test")) {
|
|
163
|
+
// normalized AgentEvents: text / thinking / tool_call (started→completed,
|
|
164
|
+
// with inputs and outputs) / permission / done (usage, stop reason)
|
|
165
|
+
}
|
|
166
|
+
session.send("now add a test"); // same warm thread, context carries
|
|
167
|
+
await session.interrupt();
|
|
168
|
+
await session.close(); // session.id resumes it later via { resume }
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`ProviderSessionOptions` takes `cwd`, `model`, `resume`, `effort`, `systemPrompt` (extra developer instructions where the harness supports them), and a `native` escape hatch (Codex: `{ sandbox, approvalPolicy, config }`; ACP: `{ mode }`). The completion-turn `run()` path stays for API-style callers; sessions are for hosts that want the real interactive agent.
|
|
173
|
+
|
|
174
|
+
The server is also embeddable: `import { startYagami } from "@justin06lee/yagami/server"`.
|
|
175
|
+
|
|
65
176
|
## Providers
|
|
66
177
|
|
|
67
178
|
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 +208,7 @@ Any other ACP agent works too — add it to config with its launch command:
|
|
|
97
208
|
| `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
209
|
| `yagami stop` | Stop the running server |
|
|
99
210
|
| `yagami status` | Show whether it's running, plus uptime, request count, and cumulative would-be API cost |
|
|
211
|
+
| `yagami key` | Print the URL + API key, plus ready-to-paste `ANTHROPIC_*`/`OPENAI_*` env exports for client apps |
|
|
100
212
|
| `yagami models` | List models across every installed provider (`--provider <id>` to filter) |
|
|
101
213
|
| `yagami keygen` | Generate another API key and save it to the config |
|
|
102
214
|
| `yagami doctor` | Check every harness CLI; `--live` sends one tiny real completion (`--provider <id>` to pick which) |
|
|
@@ -119,80 +231,14 @@ Every request is logged as one line (time, status, model, duration, cost, sessio
|
|
|
119
231
|
}
|
|
120
232
|
```
|
|
121
233
|
|
|
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"`.
|
|
234
|
+
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
235
|
|
|
191
236
|
## How it works
|
|
192
237
|
|
|
193
|
-
- **Engine**: each
|
|
238
|
+
- **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.
|
|
239
|
+
- **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
240
|
- **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
|
|
241
|
+
- **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
242
|
- **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
243
|
- **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
244
|
|
|
@@ -200,11 +246,11 @@ Extra response headers: `x-yagami-provider`, `x-yagami-cost-usd` (what the turn
|
|
|
200
246
|
|
|
201
247
|
## Limitations
|
|
202
248
|
|
|
203
|
-
- No `tools` / `tool_choice` (rejected with 400 — by design, see above). `tool_use`/`tool_result` content blocks are rejected too.
|
|
249
|
+
- 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
250
|
- 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
251
|
- 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.
|
|
252
|
+
- `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.
|
|
253
|
+
- `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
254
|
- Cost is reported only by harnesses that price their own turns (Claude, OpenCode); Codex reports token usage without cost.
|
|
209
255
|
|
|
210
256
|
## Development
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
|
+
ApiError,
|
|
3
|
+
ChatChunkTranslator,
|
|
2
4
|
SessionCache,
|
|
3
5
|
VERSION,
|
|
4
6
|
YagamiEngine,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
+
chatToMessagesRequest,
|
|
8
|
+
modelListBody,
|
|
9
|
+
openAiErrorBody,
|
|
10
|
+
toApiError,
|
|
11
|
+
toChatCompletion,
|
|
12
|
+
yagamiConfigDir
|
|
13
|
+
} from "./chunk-ZYHC7PXX.js";
|
|
7
14
|
|
|
8
15
|
// src/server.ts
|
|
9
16
|
import { serve } from "@hono/node-server";
|
|
@@ -27,10 +34,10 @@ function safeEqual(a, b) {
|
|
|
27
34
|
function errorBody(type, message) {
|
|
28
35
|
return { type: "error", error: { type, message } };
|
|
29
36
|
}
|
|
30
|
-
function requestLine(status, model, startedAt, extra = {}) {
|
|
37
|
+
function requestLine(path2, status, model, startedAt, extra = {}) {
|
|
31
38
|
const parts = [
|
|
32
39
|
(/* @__PURE__ */ new Date()).toISOString(),
|
|
33
|
-
`POST
|
|
40
|
+
`POST ${path2} ${status}`,
|
|
34
41
|
`model=${model}`,
|
|
35
42
|
`${((Date.now() - startedAt) / 1e3).toFixed(1)}s`
|
|
36
43
|
];
|
|
@@ -67,7 +74,8 @@ function createApp(options) {
|
|
|
67
74
|
const header = c.req.header("x-api-key") ?? c.req.header("authorization")?.replace(/^Bearer\s+/i, "");
|
|
68
75
|
const ok = header != null && apiKeys.some((key) => safeEqual(key, header));
|
|
69
76
|
if (!ok) {
|
|
70
|
-
|
|
77
|
+
const err = new ApiError(401, "authentication_error", "invalid API key (x-api-key or Authorization: Bearer)");
|
|
78
|
+
return c.req.path.startsWith("/v1/chat") ? c.json(openAiErrorBody(err), 401) : c.json(err.toBody(), 401);
|
|
71
79
|
}
|
|
72
80
|
await next();
|
|
73
81
|
});
|
|
@@ -83,12 +91,7 @@ function createApp(options) {
|
|
|
83
91
|
} catch {
|
|
84
92
|
}
|
|
85
93
|
c.header("x-yagami-models-source", source);
|
|
86
|
-
return c.json(
|
|
87
|
-
data: models.map((m) => ({ type: "model", ...m })),
|
|
88
|
-
has_more: false,
|
|
89
|
-
first_id: models[0]?.id,
|
|
90
|
-
last_id: models[models.length - 1]?.id
|
|
91
|
-
});
|
|
94
|
+
return c.json(modelListBody(models));
|
|
92
95
|
});
|
|
93
96
|
app.post("/v1/messages", async (c) => {
|
|
94
97
|
let body;
|
|
@@ -109,7 +112,7 @@ function createApp(options) {
|
|
|
109
112
|
onResult: (info) => {
|
|
110
113
|
if (info.costUsd !== void 0) stats.totalCostUsd += info.costUsd;
|
|
111
114
|
log?.(
|
|
112
|
-
requestLine(200, model, startedAt, {
|
|
115
|
+
requestLine("/v1/messages", 200, model, startedAt, {
|
|
113
116
|
stream: true,
|
|
114
117
|
...info.costUsd !== void 0 ? { cost: info.costUsd } : {},
|
|
115
118
|
...info.sessionId ? { session: info.sessionId } : {}
|
|
@@ -129,7 +132,7 @@ function createApp(options) {
|
|
|
129
132
|
const result = await engine.complete(req);
|
|
130
133
|
if (result.costUsd !== void 0) stats.totalCostUsd += result.costUsd;
|
|
131
134
|
log?.(
|
|
132
|
-
requestLine(200, model, startedAt, {
|
|
135
|
+
requestLine("/v1/messages", 200, model, startedAt, {
|
|
133
136
|
...result.costUsd !== void 0 ? { cost: result.costUsd } : {},
|
|
134
137
|
...result.sessionId ? { session: result.sessionId } : {}
|
|
135
138
|
})
|
|
@@ -141,10 +144,72 @@ function createApp(options) {
|
|
|
141
144
|
return c.json(result.response);
|
|
142
145
|
} catch (err) {
|
|
143
146
|
const apiErr = toApiError(err);
|
|
144
|
-
log?.(requestLine(apiErr.status, model, startedAt, { error: apiErr.type }));
|
|
147
|
+
log?.(requestLine("/v1/messages", apiErr.status, model, startedAt, { error: apiErr.type }));
|
|
145
148
|
return c.json(apiErr.toBody(), apiErr.status);
|
|
146
149
|
}
|
|
147
150
|
});
|
|
151
|
+
app.post("/v1/chat/completions", async (c) => {
|
|
152
|
+
let body;
|
|
153
|
+
try {
|
|
154
|
+
body = await c.req.json();
|
|
155
|
+
} catch {
|
|
156
|
+
return c.json(openAiErrorBody(new ApiError(400, "invalid_request_error", "request body must be valid JSON")), 400);
|
|
157
|
+
}
|
|
158
|
+
const startedAt = Date.now();
|
|
159
|
+
const chatReq = body;
|
|
160
|
+
const model = typeof chatReq?.model === "string" ? chatReq.model : "(default)";
|
|
161
|
+
stats.requests += 1;
|
|
162
|
+
try {
|
|
163
|
+
const { req, extraIgnored, includeUsage } = chatToMessagesRequest(chatReq);
|
|
164
|
+
if (req.stream === true) {
|
|
165
|
+
const abortController = new AbortController();
|
|
166
|
+
const { ignored, provider, events } = engine.stream(req, {
|
|
167
|
+
signal: abortController.signal,
|
|
168
|
+
onResult: (info) => {
|
|
169
|
+
if (info.costUsd !== void 0) stats.totalCostUsd += info.costUsd;
|
|
170
|
+
log?.(
|
|
171
|
+
requestLine("/v1/chat/completions", 200, model, startedAt, {
|
|
172
|
+
stream: true,
|
|
173
|
+
...info.costUsd !== void 0 ? { cost: info.costUsd } : {},
|
|
174
|
+
...info.sessionId ? { session: info.sessionId } : {}
|
|
175
|
+
})
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
c.header("x-yagami-provider", provider);
|
|
180
|
+
const allIgnored2 = [...ignored, ...extraIgnored];
|
|
181
|
+
if (allIgnored2.length > 0) c.header("x-yagami-ignored", allIgnored2.join(","));
|
|
182
|
+
const translator = new ChatChunkTranslator(includeUsage);
|
|
183
|
+
return streamSSE(c, async (stream) => {
|
|
184
|
+
stream.onAbort(() => abortController.abort());
|
|
185
|
+
for await (const ev of events) {
|
|
186
|
+
for (const chunk of translator.push(ev)) {
|
|
187
|
+
await stream.writeSSE({ data: JSON.stringify(chunk) });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (!translator.errored) await stream.writeSSE({ data: "[DONE]" });
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
const result = await engine.complete(req);
|
|
194
|
+
if (result.costUsd !== void 0) stats.totalCostUsd += result.costUsd;
|
|
195
|
+
log?.(
|
|
196
|
+
requestLine("/v1/chat/completions", 200, model, startedAt, {
|
|
197
|
+
...result.costUsd !== void 0 ? { cost: result.costUsd } : {},
|
|
198
|
+
...result.sessionId ? { session: result.sessionId } : {}
|
|
199
|
+
})
|
|
200
|
+
);
|
|
201
|
+
c.header("x-yagami-provider", result.provider);
|
|
202
|
+
const allIgnored = [...result.ignored, ...extraIgnored];
|
|
203
|
+
if (allIgnored.length > 0) c.header("x-yagami-ignored", allIgnored.join(","));
|
|
204
|
+
if (result.costUsd !== void 0) c.header("x-yagami-cost-usd", result.costUsd.toFixed(6));
|
|
205
|
+
if (result.sessionId) c.header("x-yagami-session", result.sessionId);
|
|
206
|
+
return c.json(toChatCompletion(result.response));
|
|
207
|
+
} catch (err) {
|
|
208
|
+
const apiErr = toApiError(err);
|
|
209
|
+
log?.(requestLine("/v1/chat/completions", apiErr.status, model, startedAt, { error: apiErr.type }));
|
|
210
|
+
return c.json(openAiErrorBody(apiErr), apiErr.status);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
148
213
|
app.notFound(
|
|
149
214
|
(c) => c.json(errorBody("not_found_error", `no route for ${c.req.method} ${c.req.path}`), 404)
|
|
150
215
|
);
|
|
@@ -158,16 +223,12 @@ function createApp(options) {
|
|
|
158
223
|
// src/server/config.ts
|
|
159
224
|
import { randomBytes } from "crypto";
|
|
160
225
|
import * as fs from "fs";
|
|
161
|
-
import * as os from "os";
|
|
162
226
|
import * as path from "path";
|
|
163
227
|
var DEFAULT_CONFIG = {
|
|
164
228
|
host: "127.0.0.1",
|
|
165
229
|
port: 8787,
|
|
166
230
|
apiKeys: []
|
|
167
231
|
};
|
|
168
|
-
function yagamiConfigDir() {
|
|
169
|
-
return process.env["YAGAMI_CONFIG_DIR"] ?? path.join(os.homedir(), ".config", "yagami");
|
|
170
|
-
}
|
|
171
232
|
function configFilePath() {
|
|
172
233
|
return path.join(yagamiConfigDir(), "config.json");
|
|
173
234
|
}
|
|
@@ -298,7 +359,6 @@ function definedProps(obj) {
|
|
|
298
359
|
|
|
299
360
|
export {
|
|
300
361
|
createApp,
|
|
301
|
-
yagamiConfigDir,
|
|
302
362
|
configFilePath,
|
|
303
363
|
sessionCachePath,
|
|
304
364
|
serverStatePath,
|
|
@@ -314,4 +374,4 @@ export {
|
|
|
314
374
|
maskKey,
|
|
315
375
|
startYagami
|
|
316
376
|
};
|
|
317
|
-
//# sourceMappingURL=chunk-
|
|
377
|
+
//# sourceMappingURL=chunk-D2PNH6GV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server.ts","../src/server/app.ts","../src/server/config.ts"],"sourcesContent":["import { serve, type ServerType } from \"@hono/node-server\";\nimport { YagamiEngine } from \"./core/engine.js\";\nimport { SessionCache } from \"./core/sessionCache.js\";\nimport { createApp } from \"./server/app.js\";\nimport {\n loadConfig,\n sessionCachePath,\n type YagamiConfig,\n} from \"./server/config.js\";\nimport { VERSION } from \"./version.js\";\n\nexport interface RunningServer {\n server: ServerType;\n engine: YagamiEngine;\n sessionCache: SessionCache;\n config: YagamiConfig;\n url: string;\n close(): Promise<void>;\n}\n\nexport interface StartOptions extends Partial<YagamiConfig> {\n /** Sink for one-line request logs (default: console.log). Pass null to disable. */\n log?: ((line: string) => void) | null;\n}\n\n/**\n * Start a yagami server. Overrides are merged over the loaded config\n * (~/.config/yagami/config.json plus YAGAMI_* env vars).\n */\nexport async function startYagami(overrides: StartOptions = {}): Promise<RunningServer> {\n const { log, ...configOverrides } = overrides;\n const config: YagamiConfig = { ...loadConfig(), ...definedProps(configOverrides) };\n if (config.apiKeys.length === 0) {\n throw new Error(\n \"no API keys configured — run `yagami keygen` (or set YAGAMI_API_KEY) so the endpoint isn't unauthenticated\",\n );\n }\n\n const sessionCache = new SessionCache({ persistPath: sessionCachePath() });\n const engine = new YagamiEngine({\n ...(config.providers ? { providerConfig: config.providers } : {}),\n ...(config.defaultProvider ? { defaultProvider: config.defaultProvider } : {}),\n ...(config.claudePath ? { claudePath: config.claudePath } : {}),\n ...(config.claudeConfigDir ? { claudeConfigDir: config.claudeConfigDir } : {}),\n ...(config.defaultModel ? { defaultModel: config.defaultModel } : {}),\n sessionCache,\n appName: \"yagami\",\n });\n\n const app = createApp({\n engine,\n apiKeys: config.apiKeys,\n cors: config.cors,\n version: VERSION,\n ...(log === null ? {} : { log: log ?? ((line: string) => console.log(line)) }),\n });\n\n const server = await new Promise<ServerType>((resolve) => {\n const s = serve({ fetch: app.fetch, hostname: config.host, port: config.port }, () => resolve(s));\n });\n\n const address = server.address();\n const port = typeof address === \"object\" && address ? address.port : config.port;\n\n return {\n server,\n engine,\n sessionCache,\n config: { ...config, port },\n url: `http://${config.host}:${port}`,\n close: () =>\n new Promise<void>((resolve, reject) => {\n server.close((err) => (err ? reject(err) : resolve()));\n }),\n };\n}\n\nfunction definedProps<T extends object>(obj: T): Partial<T> {\n return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined)) as Partial<T>;\n}\n\nexport { createApp } from \"./server/app.js\";\nexport type { AppOptions, EngineLike } from \"./server/app.js\";\nexport {\n loadConfig,\n loadFileConfig,\n saveConfig,\n generateApiKey,\n configFilePath,\n sessionCachePath,\n serverStatePath,\n logFilePath,\n readServerState,\n writeServerState,\n clearServerState,\n isProcessAlive,\n yagamiConfigDir,\n type YagamiConfig,\n type ServerState,\n} from \"./server/config.js\";\n","import { createHash, randomUUID, timingSafeEqual } from \"node:crypto\";\nimport { Hono } from \"hono\";\nimport { cors } from \"hono/cors\";\nimport { streamSSE } from \"hono/streaming\";\nimport type { ContentfulStatusCode } from \"hono/utils/http-status\";\nimport { ApiError, type MessagesRequest } from \"../core/types.js\";\nimport { toApiError } from \"../core/errors.js\";\nimport type { CompleteResult, EngineModel, StreamOptions, StreamStart } from \"../core/engine.js\";\nimport {\n ChatChunkTranslator,\n chatToMessagesRequest,\n modelListBody,\n openAiErrorBody,\n toChatCompletion,\n type ChatCompletionsRequest,\n} from \"../core/openai.js\";\n\n/** What the app needs from an engine — lets tests inject a fake. */\nexport interface EngineLike {\n /** Executable of the default provider. */\n executable: string;\n defaultProviderId: string;\n providerIds: string[];\n complete(req: MessagesRequest): Promise<CompleteResult>;\n stream(req: MessagesRequest, opts?: StreamOptions): StreamStart;\n listModels(): Promise<EngineModel[]>;\n}\n\nexport interface AppOptions {\n engine: EngineLike;\n apiKeys: string[];\n cors?: boolean;\n version?: string;\n /** Sink for one-line request logs; omit to disable request logging. */\n log?: (line: string) => void;\n}\n\n/** Served by GET /v1/models only when probing the CLI fails. */\nconst FALLBACK_MODELS: EngineModel[] = [\n \"claude-fable-5\",\n \"claude-opus-5\",\n \"claude-sonnet-5\",\n \"claude-haiku-4-5-20251001\",\n].map((id) => ({ id, display_name: id }));\n\nfunction safeEqual(a: string, b: string): boolean {\n const ha = createHash(\"sha256\").update(a).digest();\n const hb = createHash(\"sha256\").update(b).digest();\n return timingSafeEqual(ha, hb);\n}\n\nfunction errorBody(type: ApiError[\"type\"], message: string) {\n return { type: \"error\" as const, error: { type, message } };\n}\n\nfunction requestLine(\n path: string,\n status: number,\n model: string,\n startedAt: number,\n extra: { cost?: number; session?: string; stream?: boolean; error?: string } = {},\n): string {\n const parts = [\n new Date().toISOString(),\n `POST ${path} ${status}`,\n `model=${model}`,\n `${((Date.now() - startedAt) / 1000).toFixed(1)}s`,\n ];\n if (extra.stream) parts.push(\"stream\");\n if (extra.cost !== undefined) parts.push(`cost=$${extra.cost.toFixed(6)}`);\n if (extra.session) parts.push(`session=${extra.session}`);\n if (extra.error) parts.push(`error=${extra.error}`);\n return parts.join(\" \");\n}\n\nexport function createApp(options: AppOptions): Hono {\n const { engine, apiKeys, log } = options;\n const app = new Hono();\n const stats = { startedAt: Date.now(), requests: 0, totalCostUsd: 0 };\n\n if (options.cors) app.use(\"*\", cors());\n\n app.use(\"*\", async (c, next) => {\n c.header(\"request-id\", `req_${randomUUID().replace(/-/g, \"\")}`);\n await next();\n });\n\n app.get(\"/healthz\", (c) =>\n c.json({\n ok: true,\n service: \"yagami\",\n version: options.version,\n provider: engine.defaultProviderId,\n providers: engine.providerIds,\n executable: engine.executable,\n uptime_s: Math.round((Date.now() - stats.startedAt) / 1000),\n requests: stats.requests,\n total_cost_usd: stats.totalCostUsd,\n }),\n );\n\n app.use(\"/v1/*\", async (c, next) => {\n const header = c.req.header(\"x-api-key\") ?? c.req.header(\"authorization\")?.replace(/^Bearer\\s+/i, \"\");\n const ok = header != null && apiKeys.some((key) => safeEqual(key, header));\n if (!ok) {\n const err = new ApiError(401, \"authentication_error\", \"invalid API key (x-api-key or Authorization: Bearer)\");\n // The chat-completions path answers in the OpenAI error shape.\n return c.req.path.startsWith(\"/v1/chat\")\n ? c.json(openAiErrorBody(err), 401)\n : c.json(err.toBody(), 401);\n }\n await next();\n });\n\n // Served in a merged shape: Anthropic fields + OpenAI fields per model, so\n // both SDKs' models.list() parse it.\n app.get(\"/v1/models\", async (c) => {\n let models = FALLBACK_MODELS;\n let source = \"fallback\";\n try {\n const probed = await engine.listModels();\n if (probed.length > 0) {\n models = probed;\n source = \"engine\";\n }\n } catch {\n // engine unavailable or slow — the static list keeps clients working\n }\n c.header(\"x-yagami-models-source\", source);\n return c.json(modelListBody(models));\n });\n\n app.post(\"/v1/messages\", async (c) => {\n let body: unknown;\n try {\n body = await c.req.json();\n } catch {\n return c.json(errorBody(\"invalid_request_error\", \"request body must be valid JSON\"), 400);\n }\n const req = body as MessagesRequest;\n const startedAt = Date.now();\n const model = typeof req.model === \"string\" ? req.model : \"(default)\";\n stats.requests += 1;\n\n try {\n if (req.stream === true) {\n const abortController = new AbortController();\n const { ignored, provider, events } = engine.stream(req, {\n signal: abortController.signal,\n onResult: (info) => {\n if (info.costUsd !== undefined) stats.totalCostUsd += info.costUsd;\n log?.(\n requestLine(\"/v1/messages\", 200, model, startedAt, {\n stream: true,\n ...(info.costUsd !== undefined ? { cost: info.costUsd } : {}),\n ...(info.sessionId ? { session: info.sessionId } : {}),\n }),\n );\n },\n });\n c.header(\"x-yagami-provider\", provider);\n if (ignored.length > 0) c.header(\"x-yagami-ignored\", ignored.join(\",\"));\n return streamSSE(c, async (stream) => {\n stream.onAbort(() => abortController.abort());\n for await (const ev of events) {\n await stream.writeSSE({ event: ev.event, data: JSON.stringify(ev.data) });\n }\n });\n }\n\n const result = await engine.complete(req);\n if (result.costUsd !== undefined) stats.totalCostUsd += result.costUsd;\n log?.(\n requestLine(\"/v1/messages\", 200, model, startedAt, {\n ...(result.costUsd !== undefined ? { cost: result.costUsd } : {}),\n ...(result.sessionId ? { session: result.sessionId } : {}),\n }),\n );\n c.header(\"x-yagami-provider\", result.provider);\n if (result.ignored.length > 0) c.header(\"x-yagami-ignored\", result.ignored.join(\",\"));\n if (result.costUsd !== undefined) c.header(\"x-yagami-cost-usd\", result.costUsd.toFixed(6));\n if (result.sessionId) c.header(\"x-yagami-session\", result.sessionId);\n return c.json(result.response);\n } catch (err) {\n const apiErr = toApiError(err);\n log?.(requestLine(\"/v1/messages\", apiErr.status, model, startedAt, { error: apiErr.type }));\n return c.json(apiErr.toBody(), apiErr.status as ContentfulStatusCode);\n }\n });\n\n // OpenAI dialect: the same engine behind Chat Completions shapes, so apps\n // that expect an OpenAI base URL + API key work against the same key.\n app.post(\"/v1/chat/completions\", async (c) => {\n let body: unknown;\n try {\n body = await c.req.json();\n } catch {\n return c.json(openAiErrorBody(new ApiError(400, \"invalid_request_error\", \"request body must be valid JSON\")), 400);\n }\n const startedAt = Date.now();\n const chatReq = body as ChatCompletionsRequest;\n const model = typeof chatReq?.model === \"string\" ? chatReq.model : \"(default)\";\n stats.requests += 1;\n\n try {\n const { req, extraIgnored, includeUsage } = chatToMessagesRequest(chatReq);\n\n if (req.stream === true) {\n const abortController = new AbortController();\n const { ignored, provider, events } = engine.stream(req, {\n signal: abortController.signal,\n onResult: (info) => {\n if (info.costUsd !== undefined) stats.totalCostUsd += info.costUsd;\n log?.(\n requestLine(\"/v1/chat/completions\", 200, model, startedAt, {\n stream: true,\n ...(info.costUsd !== undefined ? { cost: info.costUsd } : {}),\n ...(info.sessionId ? { session: info.sessionId } : {}),\n }),\n );\n },\n });\n c.header(\"x-yagami-provider\", provider);\n const allIgnored = [...ignored, ...extraIgnored];\n if (allIgnored.length > 0) c.header(\"x-yagami-ignored\", allIgnored.join(\",\"));\n const translator = new ChatChunkTranslator(includeUsage);\n return streamSSE(c, async (stream) => {\n stream.onAbort(() => abortController.abort());\n for await (const ev of events) {\n for (const chunk of translator.push(ev)) {\n await stream.writeSSE({ data: JSON.stringify(chunk) });\n }\n }\n if (!translator.errored) await stream.writeSSE({ data: \"[DONE]\" });\n });\n }\n\n const result = await engine.complete(req);\n if (result.costUsd !== undefined) stats.totalCostUsd += result.costUsd;\n log?.(\n requestLine(\"/v1/chat/completions\", 200, model, startedAt, {\n ...(result.costUsd !== undefined ? { cost: result.costUsd } : {}),\n ...(result.sessionId ? { session: result.sessionId } : {}),\n }),\n );\n c.header(\"x-yagami-provider\", result.provider);\n const allIgnored = [...result.ignored, ...extraIgnored];\n if (allIgnored.length > 0) c.header(\"x-yagami-ignored\", allIgnored.join(\",\"));\n if (result.costUsd !== undefined) c.header(\"x-yagami-cost-usd\", result.costUsd.toFixed(6));\n if (result.sessionId) c.header(\"x-yagami-session\", result.sessionId);\n return c.json(toChatCompletion(result.response));\n } catch (err) {\n const apiErr = toApiError(err);\n log?.(requestLine(\"/v1/chat/completions\", apiErr.status, model, startedAt, { error: apiErr.type }));\n return c.json(openAiErrorBody(apiErr), apiErr.status as ContentfulStatusCode);\n }\n });\n\n app.notFound((c) =>\n c.json(errorBody(\"not_found_error\", `no route for ${c.req.method} ${c.req.path}`), 404),\n );\n\n app.onError((err, c) => {\n const apiErr = toApiError(err);\n return c.json(apiErr.toBody(), apiErr.status as ContentfulStatusCode);\n });\n\n return app;\n}\n","import { randomBytes } from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { yagamiConfigDir } from \"../core/hostConfig.js\";\nimport type { ProviderConfigEntry } from \"../core/providers/registry.js\";\n\nexport { yagamiConfigDir } from \"../core/hostConfig.js\";\n\nexport interface YagamiConfig {\n host: string;\n port: number;\n apiKeys: string[];\n /** @deprecated Use providers.claude.path. */\n claudePath?: string;\n /** @deprecated Use providers.claude.configDir. */\n claudeConfigDir?: string;\n defaultModel?: string;\n cors?: boolean;\n /** Provider used for bare model ids (default: claude). */\n defaultProvider?: string;\n /** Per-provider settings, keyed by provider id. */\n providers?: Record<string, ProviderConfigEntry>;\n}\n\nexport const DEFAULT_CONFIG: YagamiConfig = {\n host: \"127.0.0.1\",\n port: 8787,\n apiKeys: [],\n};\n\nexport function configFilePath(): string {\n return path.join(yagamiConfigDir(), \"config.json\");\n}\n\nexport function sessionCachePath(): string {\n return path.join(yagamiConfigDir(), \"sessions.json\");\n}\n\nexport function serverStatePath(): string {\n return path.join(yagamiConfigDir(), \"server.json\");\n}\n\nexport function logFilePath(): string {\n return path.join(yagamiConfigDir(), \"yagami.log\");\n}\n\n/** What a running server records about itself for `stop`/`status`. */\nexport interface ServerState {\n pid: number;\n host: string;\n port: number;\n url: string;\n startedAt: string;\n version: string;\n log?: string;\n}\n\nexport function readServerState(): ServerState | undefined {\n try {\n const state = JSON.parse(fs.readFileSync(serverStatePath(), \"utf8\")) as ServerState;\n return typeof state?.pid === \"number\" ? state : undefined;\n } catch {\n return undefined;\n }\n}\n\nexport function writeServerState(state: ServerState): void {\n fs.mkdirSync(yagamiConfigDir(), { recursive: true, mode: 0o700 });\n fs.writeFileSync(serverStatePath(), `${JSON.stringify(state, null, 2)}\\n`, { mode: 0o600 });\n}\n\n/** Remove the state file; with `pid`, only if it still belongs to that pid. */\nexport function clearServerState(pid?: number): void {\n try {\n if (pid !== undefined && readServerState()?.pid !== pid) return;\n fs.unlinkSync(serverStatePath());\n } catch {\n // already gone\n }\n}\n\nexport function isProcessAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch {\n return false;\n }\n}\n\n/** Config as stored on disk, without env overrides (safe to save back). */\nexport function loadFileConfig(): YagamiConfig {\n let fromFile: Partial<YagamiConfig> = {};\n try {\n fromFile = JSON.parse(fs.readFileSync(configFilePath(), \"utf8\")) as Partial<YagamiConfig>;\n } catch {\n // no config file yet\n }\n return {\n ...DEFAULT_CONFIG,\n ...fromFile,\n apiKeys: Array.isArray(fromFile.apiKeys) ? fromFile.apiKeys.filter((k) => typeof k === \"string\") : [],\n };\n}\n\n/** File config plus environment overrides. */\nexport function loadConfig(): YagamiConfig {\n const cfg = loadFileConfig();\n const env = process.env;\n if (env[\"YAGAMI_HOST\"]) cfg.host = env[\"YAGAMI_HOST\"];\n if (env[\"YAGAMI_PORT\"] && Number.isFinite(Number(env[\"YAGAMI_PORT\"]))) {\n cfg.port = Number(env[\"YAGAMI_PORT\"]);\n }\n if (env[\"YAGAMI_API_KEY\"] && !cfg.apiKeys.includes(env[\"YAGAMI_API_KEY\"])) {\n cfg.apiKeys.push(env[\"YAGAMI_API_KEY\"]);\n }\n if (env[\"YAGAMI_CLAUDE_PATH\"]) cfg.claudePath = env[\"YAGAMI_CLAUDE_PATH\"];\n if (env[\"YAGAMI_DEFAULT_MODEL\"]) cfg.defaultModel = env[\"YAGAMI_DEFAULT_MODEL\"];\n if (env[\"YAGAMI_PROVIDER\"]) cfg.defaultProvider = env[\"YAGAMI_PROVIDER\"];\n return cfg;\n}\n\nexport function saveConfig(cfg: YagamiConfig): string {\n const dir = yagamiConfigDir();\n fs.mkdirSync(dir, { recursive: true, mode: 0o700 });\n const file = configFilePath();\n fs.writeFileSync(file, `${JSON.stringify(cfg, null, 2)}\\n`, { mode: 0o600 });\n return file;\n}\n\nexport function generateApiKey(): string {\n return `ygm_${randomBytes(24).toString(\"hex\")}`;\n}\n\nexport function maskKey(key: string): string {\n return key.length <= 12 ? key : `${key.slice(0, 12)}…`;\n}\n"],"mappings":"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","names":["path","allIgnored"]}
|