@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 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. On top it serves `POST /v1/messages` with Anthropic request/response shapes and SSE streaming, so anything that accepts an Anthropic `baseURL` + `apiKey` can use it as a drop-in, and pick a harness per request with `model: "<provider>:<model>"`.
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
- ## Install
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.4.1
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
- Then from any Anthropic client:
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
- const client = new Anthropic({
48
- baseURL: "http://127.0.0.1:8787",
49
- apiKey: process.env.YAGAMI_KEY, // your ygm_ key
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 `/v1/messages` 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.
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`. Claude and ACP agents stream tokens; Codex streams per message part.
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.4.1";
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 path7 = findExecutable(command, entry.path ? { explicit: entry.path } : {});
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: path7 !== void 0 && entry.enabled !== false,
1124
- ...path7 ? { path: path7 } : {},
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-ASS6MJ7C.js.map
2088
+ //# sourceMappingURL=chunk-2UG5CR5X.js.map