reasonix 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Reasonix Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,114 @@
1
+ # Reasonix
2
+
3
+ **The DeepSeek-native agent framework.** TypeScript. Ink TUI. No LangChain.
4
+
5
+ Reasonix is not another generic agent framework. It does one thing: take DeepSeek's
6
+ unusual economic and behavioral profile — dirt-cheap tokens, R1 reasoning traces,
7
+ automatic prefix caching — and turn them into agent-loop superpowers that generic
8
+ frameworks leave on the table.
9
+
10
+ ```bash
11
+ npm install reasonix # or: npm i -g reasonix for the CLI
12
+ export DEEPSEEK_API_KEY=sk-...
13
+ npx reasonix chat # live TUI with real-time cache-hit and cost panel
14
+ ```
15
+
16
+ ## Why Reasonix?
17
+
18
+ Every other framework treats DeepSeek as an OpenAI-compatible endpoint with a
19
+ different base URL. That works, but it leaves most of DeepSeek's advantages
20
+ unused. Reasonix is opinionated about three things:
21
+
22
+ ### 1. Cache-First Loop
23
+ DeepSeek bills cached input tokens at **~10% of the miss rate**. Reasonix
24
+ structures the agent loop as `[Immutable Prefix] + [Append-Only Log] +
25
+ [Volatile Scratch]` so every turn reuses the exact byte prefix.
26
+
27
+ **Validated on real DeepSeek API (`deepseek-chat`):**
28
+
29
+ | scenario | turns | cache hit | cost | cost on Claude Sonnet 4.6 | savings |
30
+ |---|---|---|---|---|---|
31
+ | Chinese multi-turn chat | 5 | **85.2%** | $0.000923 | $0.015174 | **93.9%** |
32
+ | Tool-use (calculator) | 2 | **94.9%** | $0.000142 | $0.003351 | **95.8%** |
33
+
34
+ ### 2. R1 Thought Harvesting
35
+ R1's `reasoning_content` contains a *plan*, not just trivia to display. Reasonix
36
+ parses it into typed plan state (subgoals, hypotheses, uncertainties, rejected
37
+ paths) and feeds that state to the orchestrator — branching decisions are made
38
+ on structured signals, not regex-brittle prompt hacks. *(v0.2)*
39
+
40
+ ### 3. Tool-Call Repair
41
+ R1/V3 have known quirks — tool calls leaking into `<think>`, dropped arguments
42
+ on deep schemas, truncated JSON, call-storm loops. Reasonix ships a full repair
43
+ pipeline: **scavenge + flatten + truncation recovery + storm breaker**.
44
+
45
+ ## Usage
46
+
47
+ ### Library
48
+
49
+ ```ts
50
+ import { CacheFirstLoop, DeepSeekClient, ImmutablePrefix, ToolRegistry } from "reasonix";
51
+
52
+ const client = new DeepSeekClient();
53
+ const tools = new ToolRegistry();
54
+
55
+ tools.register({
56
+ name: "add",
57
+ description: "Add two integers",
58
+ parameters: {
59
+ type: "object",
60
+ properties: { a: { type: "integer" }, b: { type: "integer" } },
61
+ required: ["a", "b"],
62
+ },
63
+ fn: ({ a, b }) => a + b,
64
+ });
65
+
66
+ const loop = new CacheFirstLoop({
67
+ client,
68
+ prefix: new ImmutablePrefix({
69
+ system: "You are a math helper.",
70
+ toolSpecs: tools.specs(),
71
+ }),
72
+ tools,
73
+ });
74
+
75
+ for await (const ev of loop.step("What is 17 + 25?")) {
76
+ console.log(ev);
77
+ }
78
+ console.log(loop.stats.summary());
79
+ ```
80
+
81
+ ### CLI / TUI
82
+
83
+ ```bash
84
+ reasonix chat # full-screen Ink TUI, live cache/cost panel
85
+ reasonix run "task" # one-shot, streaming output
86
+ reasonix stats <file> # summarize transcript JSONL
87
+ reasonix version
88
+ ```
89
+
90
+ ## Status
91
+
92
+ Pre-alpha. v0.0.1 ships Pillar 1 and Pillar 3 working end-to-end; Pillar 2 is a
93
+ stub with a stable surface. See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
94
+
95
+ ## Non-goals
96
+
97
+ - Multi-agent orchestration (use LangGraph if you need it).
98
+ - RAG / vector stores.
99
+ - Multi-provider abstraction. **Reasonix does DeepSeek, deeply.**
100
+ - Web UI / SaaS.
101
+
102
+ ## Development
103
+
104
+ ```bash
105
+ npm install
106
+ npm run dev chat # run CLI directly from TS (tsx)
107
+ npm run build # bundle to dist/
108
+ npm test # vitest
109
+ npm run lint # biome
110
+ ```
111
+
112
+ ## License
113
+
114
+ MIT
@@ -0,0 +1,261 @@
1
+ // src/client.ts
2
+ import { createParser } from "eventsource-parser";
3
+
4
+ // src/retry.ts
5
+ var DEFAULT_RETRYABLE_STATUSES = [408, 429, 500, 502, 503, 504];
6
+ async function fetchWithRetry(fetchFn, url, init, opts = {}) {
7
+ const maxAttempts = opts.maxAttempts ?? 4;
8
+ const initial = opts.initialBackoffMs ?? 500;
9
+ const cap = opts.maxBackoffMs ?? 1e4;
10
+ const retryable = new Set(opts.retryableStatuses ?? DEFAULT_RETRYABLE_STATUSES);
11
+ let lastError;
12
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
13
+ if (opts.signal?.aborted) throw new Error("aborted");
14
+ try {
15
+ const resp = await fetchFn(url, init);
16
+ if (resp.ok || !retryable.has(resp.status)) return resp;
17
+ if (attempt === maxAttempts - 1) return resp;
18
+ await resp.text().catch(() => void 0);
19
+ const waitMs = computeWait(attempt, initial, cap, resp.headers.get("Retry-After"));
20
+ opts.onRetry?.({ attempt: attempt + 1, reason: `http ${resp.status}`, waitMs });
21
+ await sleep(waitMs, opts.signal);
22
+ } catch (err) {
23
+ lastError = err;
24
+ if (isAbortError(err) || opts.signal?.aborted) throw err;
25
+ if (attempt === maxAttempts - 1) throw err;
26
+ const waitMs = computeWait(attempt, initial, cap, null);
27
+ opts.onRetry?.({
28
+ attempt: attempt + 1,
29
+ reason: `network: ${messageOf(err)}`,
30
+ waitMs
31
+ });
32
+ await sleep(waitMs, opts.signal);
33
+ }
34
+ }
35
+ throw lastError ?? new Error("fetchWithRetry: loop exited unexpectedly");
36
+ }
37
+ function computeWait(attempt, initial, cap, retryAfter) {
38
+ if (retryAfter) {
39
+ const seconds = Number.parseFloat(retryAfter);
40
+ if (Number.isFinite(seconds) && seconds > 0) {
41
+ return Math.min(seconds * 1e3, cap);
42
+ }
43
+ }
44
+ const exp = initial * 2 ** attempt;
45
+ const jitter = exp * (0.75 + Math.random() * 0.5);
46
+ return Math.min(Math.max(jitter, 0), cap);
47
+ }
48
+ function sleep(ms, signal) {
49
+ if (ms <= 0) return Promise.resolve();
50
+ return new Promise((resolve, reject) => {
51
+ const timer = setTimeout(resolve, ms);
52
+ if (signal) {
53
+ const onAbort = () => {
54
+ clearTimeout(timer);
55
+ reject(new Error("aborted"));
56
+ };
57
+ if (signal.aborted) onAbort();
58
+ else signal.addEventListener("abort", onAbort, { once: true });
59
+ }
60
+ });
61
+ }
62
+ function isAbortError(err) {
63
+ if (!err || typeof err !== "object") return false;
64
+ const name = err.name;
65
+ return name === "AbortError";
66
+ }
67
+ function messageOf(err) {
68
+ if (err instanceof Error) return err.message;
69
+ try {
70
+ return String(err);
71
+ } catch {
72
+ return "unknown error";
73
+ }
74
+ }
75
+
76
+ // src/client.ts
77
+ var Usage = class _Usage {
78
+ constructor(promptTokens = 0, completionTokens = 0, totalTokens = 0, promptCacheHitTokens = 0, promptCacheMissTokens = 0) {
79
+ this.promptTokens = promptTokens;
80
+ this.completionTokens = completionTokens;
81
+ this.totalTokens = totalTokens;
82
+ this.promptCacheHitTokens = promptCacheHitTokens;
83
+ this.promptCacheMissTokens = promptCacheMissTokens;
84
+ }
85
+ promptTokens;
86
+ completionTokens;
87
+ totalTokens;
88
+ promptCacheHitTokens;
89
+ promptCacheMissTokens;
90
+ get cacheHitRatio() {
91
+ const denom = this.promptCacheHitTokens + this.promptCacheMissTokens;
92
+ return denom > 0 ? this.promptCacheHitTokens / denom : 0;
93
+ }
94
+ static fromApi(raw) {
95
+ const u = raw ?? {};
96
+ return new _Usage(
97
+ u.prompt_tokens ?? 0,
98
+ u.completion_tokens ?? 0,
99
+ u.total_tokens ?? 0,
100
+ u.prompt_cache_hit_tokens ?? 0,
101
+ u.prompt_cache_miss_tokens ?? 0
102
+ );
103
+ }
104
+ };
105
+ var DeepSeekClient = class {
106
+ apiKey;
107
+ baseUrl;
108
+ timeoutMs;
109
+ retry;
110
+ _fetch;
111
+ constructor(opts = {}) {
112
+ const apiKey = opts.apiKey ?? process.env.DEEPSEEK_API_KEY;
113
+ if (!apiKey) {
114
+ throw new Error(
115
+ "DEEPSEEK_API_KEY is not set. Put it in .env or pass apiKey to DeepSeekClient."
116
+ );
117
+ }
118
+ this.apiKey = apiKey;
119
+ this.baseUrl = (opts.baseUrl ?? process.env.DEEPSEEK_BASE_URL ?? "https://api.deepseek.com").replace(/\/+$/, "");
120
+ this.timeoutMs = opts.timeoutMs ?? 12e4;
121
+ this._fetch = opts.fetch ?? globalThis.fetch.bind(globalThis);
122
+ this.retry = opts.retry ?? {};
123
+ }
124
+ buildPayload(opts, stream) {
125
+ const payload = {
126
+ model: opts.model,
127
+ messages: opts.messages,
128
+ stream
129
+ };
130
+ if (opts.tools?.length) payload.tools = opts.tools;
131
+ if (opts.temperature !== void 0) payload.temperature = opts.temperature;
132
+ if (opts.maxTokens !== void 0) payload.max_tokens = opts.maxTokens;
133
+ return payload;
134
+ }
135
+ async chat(opts) {
136
+ const ctrl = new AbortController();
137
+ const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
138
+ const signal = opts.signal ?? ctrl.signal;
139
+ try {
140
+ const resp = await fetchWithRetry(
141
+ this._fetch,
142
+ `${this.baseUrl}/chat/completions`,
143
+ {
144
+ method: "POST",
145
+ headers: {
146
+ Authorization: `Bearer ${this.apiKey}`,
147
+ "Content-Type": "application/json"
148
+ },
149
+ body: JSON.stringify(this.buildPayload(opts, false)),
150
+ signal
151
+ },
152
+ { ...this.retry, signal }
153
+ );
154
+ if (!resp.ok) {
155
+ throw new Error(`DeepSeek ${resp.status}: ${await resp.text()}`);
156
+ }
157
+ const data = await resp.json();
158
+ const choice = data.choices?.[0]?.message ?? {};
159
+ return {
160
+ content: choice.content ?? "",
161
+ reasoningContent: choice.reasoning_content ?? null,
162
+ toolCalls: choice.tool_calls ?? [],
163
+ usage: Usage.fromApi(data.usage),
164
+ raw: data
165
+ };
166
+ } finally {
167
+ clearTimeout(timer);
168
+ }
169
+ }
170
+ async *stream(opts) {
171
+ const ctrl = new AbortController();
172
+ const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
173
+ const signal = opts.signal ?? ctrl.signal;
174
+ let resp;
175
+ try {
176
+ resp = await fetchWithRetry(
177
+ this._fetch,
178
+ `${this.baseUrl}/chat/completions`,
179
+ {
180
+ method: "POST",
181
+ headers: {
182
+ Authorization: `Bearer ${this.apiKey}`,
183
+ "Content-Type": "application/json",
184
+ Accept: "text/event-stream"
185
+ },
186
+ body: JSON.stringify(this.buildPayload(opts, true)),
187
+ signal
188
+ },
189
+ { ...this.retry, signal }
190
+ );
191
+ } catch (err) {
192
+ clearTimeout(timer);
193
+ throw err;
194
+ }
195
+ if (!resp.ok || !resp.body) {
196
+ clearTimeout(timer);
197
+ throw new Error(`DeepSeek ${resp.status}: ${await resp.text().catch(() => "")}`);
198
+ }
199
+ const queue = [];
200
+ let done = false;
201
+ const parser = createParser({
202
+ onEvent: (ev) => {
203
+ if (!ev.data || ev.data === "[DONE]") {
204
+ done = true;
205
+ return;
206
+ }
207
+ try {
208
+ const json = JSON.parse(ev.data);
209
+ const delta = json.choices?.[0]?.delta ?? {};
210
+ const finishReason = json.choices?.[0]?.finish_reason ?? void 0;
211
+ const chunk = { raw: json, finishReason };
212
+ if (typeof delta.content === "string" && delta.content.length > 0) {
213
+ chunk.contentDelta = delta.content;
214
+ }
215
+ if (typeof delta.reasoning_content === "string" && delta.reasoning_content.length > 0) {
216
+ chunk.reasoningDelta = delta.reasoning_content;
217
+ }
218
+ if (Array.isArray(delta.tool_calls) && delta.tool_calls.length > 0) {
219
+ const tc = delta.tool_calls[0];
220
+ chunk.toolCallDelta = {
221
+ index: tc.index ?? 0,
222
+ id: tc.id,
223
+ name: tc.function?.name,
224
+ argumentsDelta: tc.function?.arguments
225
+ };
226
+ }
227
+ if (json.usage) {
228
+ chunk.usage = Usage.fromApi(json.usage);
229
+ }
230
+ queue.push(chunk);
231
+ } catch {
232
+ }
233
+ }
234
+ });
235
+ const reader = resp.body.getReader();
236
+ const decoder = new TextDecoder();
237
+ try {
238
+ while (true) {
239
+ if (queue.length > 0) {
240
+ yield queue.shift();
241
+ continue;
242
+ }
243
+ if (done) break;
244
+ const { value, done: streamDone } = await reader.read();
245
+ if (streamDone) break;
246
+ parser.feed(decoder.decode(value, { stream: true }));
247
+ }
248
+ while (queue.length > 0) yield queue.shift();
249
+ } finally {
250
+ clearTimeout(timer);
251
+ reader.releaseLock();
252
+ }
253
+ }
254
+ };
255
+
256
+ export {
257
+ fetchWithRetry,
258
+ Usage,
259
+ DeepSeekClient
260
+ };
261
+ //# sourceMappingURL=chunk-XILYSYPT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client.ts","../src/retry.ts"],"sourcesContent":["import { type EventSourceMessage, createParser } from \"eventsource-parser\";\nimport { type RetryOptions, fetchWithRetry } from \"./retry.js\";\nimport type { ChatMessage, ChatRequestOptions, RawUsage, ToolCall, ToolSpec } from \"./types.js\";\n\nexport class Usage {\n constructor(\n public promptTokens = 0,\n public completionTokens = 0,\n public totalTokens = 0,\n public promptCacheHitTokens = 0,\n public promptCacheMissTokens = 0,\n ) {}\n\n get cacheHitRatio(): number {\n const denom = this.promptCacheHitTokens + this.promptCacheMissTokens;\n return denom > 0 ? this.promptCacheHitTokens / denom : 0;\n }\n\n static fromApi(raw: RawUsage | undefined | null): Usage {\n const u = raw ?? {};\n return new Usage(\n u.prompt_tokens ?? 0,\n u.completion_tokens ?? 0,\n u.total_tokens ?? 0,\n u.prompt_cache_hit_tokens ?? 0,\n u.prompt_cache_miss_tokens ?? 0,\n );\n }\n}\n\nexport interface ChatResponse {\n content: string;\n reasoningContent: string | null;\n toolCalls: ToolCall[];\n usage: Usage;\n raw: unknown;\n}\n\nexport interface StreamChunk {\n contentDelta?: string;\n reasoningDelta?: string;\n toolCallDelta?: { index: number; id?: string; name?: string; argumentsDelta?: string };\n usage?: Usage;\n finishReason?: string;\n raw: any;\n}\n\nexport interface DeepSeekClientOptions {\n apiKey?: string;\n baseUrl?: string;\n timeoutMs?: number;\n fetch?: typeof fetch;\n /** Retry configuration. Pass `{ maxAttempts: 1 }` to disable retries. */\n retry?: RetryOptions;\n}\n\nexport class DeepSeekClient {\n readonly apiKey: string;\n readonly baseUrl: string;\n readonly timeoutMs: number;\n readonly retry: RetryOptions;\n private readonly _fetch: typeof fetch;\n\n constructor(opts: DeepSeekClientOptions = {}) {\n const apiKey = opts.apiKey ?? process.env.DEEPSEEK_API_KEY;\n if (!apiKey) {\n throw new Error(\n \"DEEPSEEK_API_KEY is not set. Put it in .env or pass apiKey to DeepSeekClient.\",\n );\n }\n this.apiKey = apiKey;\n this.baseUrl = (\n opts.baseUrl ??\n process.env.DEEPSEEK_BASE_URL ??\n \"https://api.deepseek.com\"\n ).replace(/\\/+$/, \"\");\n this.timeoutMs = opts.timeoutMs ?? 120_000;\n this._fetch = opts.fetch ?? globalThis.fetch.bind(globalThis);\n this.retry = opts.retry ?? {};\n }\n\n private buildPayload(opts: ChatRequestOptions, stream: boolean) {\n const payload: Record<string, unknown> = {\n model: opts.model,\n messages: opts.messages,\n stream,\n };\n if (opts.tools?.length) payload.tools = opts.tools;\n if (opts.temperature !== undefined) payload.temperature = opts.temperature;\n if (opts.maxTokens !== undefined) payload.max_tokens = opts.maxTokens;\n return payload;\n }\n\n async chat(opts: ChatRequestOptions): Promise<ChatResponse> {\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);\n const signal = opts.signal ?? ctrl.signal;\n\n try {\n const resp = await fetchWithRetry(\n this._fetch,\n `${this.baseUrl}/chat/completions`,\n {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(this.buildPayload(opts, false)),\n signal,\n },\n { ...this.retry, signal },\n );\n if (!resp.ok) {\n throw new Error(`DeepSeek ${resp.status}: ${await resp.text()}`);\n }\n const data: any = await resp.json();\n const choice = data.choices?.[0]?.message ?? {};\n return {\n content: choice.content ?? \"\",\n reasoningContent: choice.reasoning_content ?? null,\n toolCalls: choice.tool_calls ?? [],\n usage: Usage.fromApi(data.usage),\n raw: data,\n };\n } finally {\n clearTimeout(timer);\n }\n }\n\n async *stream(opts: ChatRequestOptions): AsyncGenerator<StreamChunk> {\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);\n const signal = opts.signal ?? ctrl.signal;\n\n let resp: Response;\n try {\n // Only the initial fetch is retried. Once the server has started sending\n // the stream body we do NOT retry — a mid-stream retry would re-bill and\n // desync the session context.\n resp = await fetchWithRetry(\n this._fetch,\n `${this.baseUrl}/chat/completions`,\n {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n \"Content-Type\": \"application/json\",\n Accept: \"text/event-stream\",\n },\n body: JSON.stringify(this.buildPayload(opts, true)),\n signal,\n },\n { ...this.retry, signal },\n );\n } catch (err) {\n clearTimeout(timer);\n throw err;\n }\n if (!resp.ok || !resp.body) {\n clearTimeout(timer);\n throw new Error(`DeepSeek ${resp.status}: ${await resp.text().catch(() => \"\")}`);\n }\n\n const queue: StreamChunk[] = [];\n let done = false;\n const parser = createParser({\n onEvent: (ev: EventSourceMessage) => {\n if (!ev.data || ev.data === \"[DONE]\") {\n done = true;\n return;\n }\n try {\n const json = JSON.parse(ev.data);\n const delta = json.choices?.[0]?.delta ?? {};\n const finishReason = json.choices?.[0]?.finish_reason ?? undefined;\n const chunk: StreamChunk = { raw: json, finishReason };\n if (typeof delta.content === \"string\" && delta.content.length > 0) {\n chunk.contentDelta = delta.content;\n }\n if (typeof delta.reasoning_content === \"string\" && delta.reasoning_content.length > 0) {\n chunk.reasoningDelta = delta.reasoning_content;\n }\n if (Array.isArray(delta.tool_calls) && delta.tool_calls.length > 0) {\n const tc = delta.tool_calls[0];\n chunk.toolCallDelta = {\n index: tc.index ?? 0,\n id: tc.id,\n name: tc.function?.name,\n argumentsDelta: tc.function?.arguments,\n };\n }\n if (json.usage) {\n chunk.usage = Usage.fromApi(json.usage);\n }\n queue.push(chunk);\n } catch {\n /* skip malformed sse frame */\n }\n },\n });\n\n const reader = resp.body.getReader();\n const decoder = new TextDecoder();\n try {\n while (true) {\n if (queue.length > 0) {\n yield queue.shift()!;\n continue;\n }\n if (done) break;\n const { value, done: streamDone } = await reader.read();\n if (streamDone) break;\n parser.feed(decoder.decode(value, { stream: true }));\n }\n while (queue.length > 0) yield queue.shift()!;\n } finally {\n clearTimeout(timer);\n reader.releaseLock();\n }\n }\n}\n\nexport type { ChatMessage, ToolCall, ToolSpec };\n","/**\n * Retry layer for DeepSeek API calls.\n *\n * Wraps a `fetch` function so that transient failures (rate limiting, server\n * overload, network blips) don't kill an agent session. We explicitly DO NOT\n * retry:\n * - 4xx client errors other than 408 / 429 (bad key, bad request, ...)\n * - aborted requests (user cancelled)\n * - mid-stream body read errors (retrying costs money AND would desync)\n *\n * Retrying is controlled by attempt count + exponential backoff with jitter.\n * If the server sends a `Retry-After` header we honor it (capped by\n * `maxBackoffMs` so a misconfigured upstream can't park us forever).\n */\n\nexport interface RetryOptions {\n /** Maximum total attempts (including the first). Default 4. */\n maxAttempts?: number;\n /** Initial backoff in ms. Doubles each retry, with jitter. Default 500. */\n initialBackoffMs?: number;\n /** Upper bound on any single backoff delay. Default 10000 (10s). */\n maxBackoffMs?: number;\n /** HTTP statuses to treat as retryable. Default [408, 429, 500, 502, 503, 504]. */\n retryableStatuses?: readonly number[];\n /** Abort signal; we do NOT retry once aborted. */\n signal?: AbortSignal;\n /** Telemetry hook — called before each wait. */\n onRetry?: (info: RetryInfo) => void;\n}\n\nexport interface RetryInfo {\n attempt: number;\n reason: string;\n waitMs: number;\n}\n\nconst DEFAULT_RETRYABLE_STATUSES = [408, 429, 500, 502, 503, 504] as const;\n\nexport async function fetchWithRetry(\n fetchFn: typeof fetch,\n url: string,\n init: RequestInit,\n opts: RetryOptions = {},\n): Promise<Response> {\n const maxAttempts = opts.maxAttempts ?? 4;\n const initial = opts.initialBackoffMs ?? 500;\n const cap = opts.maxBackoffMs ?? 10_000;\n const retryable = new Set(opts.retryableStatuses ?? DEFAULT_RETRYABLE_STATUSES);\n\n let lastError: unknown;\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n if (opts.signal?.aborted) throw new Error(\"aborted\");\n\n try {\n const resp = await fetchFn(url, init);\n\n // Success or non-retryable failure: return as-is.\n if (resp.ok || !retryable.has(resp.status)) return resp;\n\n // Retryable but out of attempts: return the last response so the caller\n // can surface the status to the user.\n if (attempt === maxAttempts - 1) return resp;\n\n // Drain the body so the connection can be reused on the next attempt.\n await resp.text().catch(() => undefined);\n\n const waitMs = computeWait(attempt, initial, cap, resp.headers.get(\"Retry-After\"));\n opts.onRetry?.({ attempt: attempt + 1, reason: `http ${resp.status}`, waitMs });\n await sleep(waitMs, opts.signal);\n } catch (err) {\n lastError = err;\n // Respect explicit aborts — do not retry.\n if (isAbortError(err) || opts.signal?.aborted) throw err;\n if (attempt === maxAttempts - 1) throw err;\n\n const waitMs = computeWait(attempt, initial, cap, null);\n opts.onRetry?.({\n attempt: attempt + 1,\n reason: `network: ${messageOf(err)}`,\n waitMs,\n });\n await sleep(waitMs, opts.signal);\n }\n }\n\n throw lastError ?? new Error(\"fetchWithRetry: loop exited unexpectedly\");\n}\n\nfunction computeWait(\n attempt: number,\n initial: number,\n cap: number,\n retryAfter: string | null,\n): number {\n if (retryAfter) {\n const seconds = Number.parseFloat(retryAfter);\n if (Number.isFinite(seconds) && seconds > 0) {\n return Math.min(seconds * 1000, cap);\n }\n }\n const exp = initial * 2 ** attempt;\n // Jitter range [75%, 125%] to spread retries out when many clients hit 429 together.\n const jitter = exp * (0.75 + Math.random() * 0.5);\n return Math.min(Math.max(jitter, 0), cap);\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n if (ms <= 0) return Promise.resolve();\n return new Promise((resolve, reject) => {\n const timer = setTimeout(resolve, ms);\n if (signal) {\n const onAbort = () => {\n clearTimeout(timer);\n reject(new Error(\"aborted\"));\n };\n if (signal.aborted) onAbort();\n else signal.addEventListener(\"abort\", onAbort, { once: true });\n }\n });\n}\n\nfunction isAbortError(err: unknown): boolean {\n if (!err || typeof err !== \"object\") return false;\n const name = (err as { name?: unknown }).name;\n return name === \"AbortError\";\n}\n\nfunction messageOf(err: unknown): string {\n if (err instanceof Error) return err.message;\n try {\n return String(err);\n } catch {\n return \"unknown error\";\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/client.ts
4
+ import { createParser } from "eventsource-parser";
5
+
6
+ // src/retry.ts
7
+ var DEFAULT_RETRYABLE_STATUSES = [408, 429, 500, 502, 503, 504];
8
+ async function fetchWithRetry(fetchFn, url, init, opts = {}) {
9
+ const maxAttempts = opts.maxAttempts ?? 4;
10
+ const initial = opts.initialBackoffMs ?? 500;
11
+ const cap = opts.maxBackoffMs ?? 1e4;
12
+ const retryable = new Set(opts.retryableStatuses ?? DEFAULT_RETRYABLE_STATUSES);
13
+ let lastError;
14
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
15
+ if (opts.signal?.aborted) throw new Error("aborted");
16
+ try {
17
+ const resp = await fetchFn(url, init);
18
+ if (resp.ok || !retryable.has(resp.status)) return resp;
19
+ if (attempt === maxAttempts - 1) return resp;
20
+ await resp.text().catch(() => void 0);
21
+ const waitMs = computeWait(attempt, initial, cap, resp.headers.get("Retry-After"));
22
+ opts.onRetry?.({ attempt: attempt + 1, reason: `http ${resp.status}`, waitMs });
23
+ await sleep(waitMs, opts.signal);
24
+ } catch (err) {
25
+ lastError = err;
26
+ if (isAbortError(err) || opts.signal?.aborted) throw err;
27
+ if (attempt === maxAttempts - 1) throw err;
28
+ const waitMs = computeWait(attempt, initial, cap, null);
29
+ opts.onRetry?.({
30
+ attempt: attempt + 1,
31
+ reason: `network: ${messageOf(err)}`,
32
+ waitMs
33
+ });
34
+ await sleep(waitMs, opts.signal);
35
+ }
36
+ }
37
+ throw lastError ?? new Error("fetchWithRetry: loop exited unexpectedly");
38
+ }
39
+ function computeWait(attempt, initial, cap, retryAfter) {
40
+ if (retryAfter) {
41
+ const seconds = Number.parseFloat(retryAfter);
42
+ if (Number.isFinite(seconds) && seconds > 0) {
43
+ return Math.min(seconds * 1e3, cap);
44
+ }
45
+ }
46
+ const exp = initial * 2 ** attempt;
47
+ const jitter = exp * (0.75 + Math.random() * 0.5);
48
+ return Math.min(Math.max(jitter, 0), cap);
49
+ }
50
+ function sleep(ms, signal) {
51
+ if (ms <= 0) return Promise.resolve();
52
+ return new Promise((resolve, reject) => {
53
+ const timer = setTimeout(resolve, ms);
54
+ if (signal) {
55
+ const onAbort = () => {
56
+ clearTimeout(timer);
57
+ reject(new Error("aborted"));
58
+ };
59
+ if (signal.aborted) onAbort();
60
+ else signal.addEventListener("abort", onAbort, { once: true });
61
+ }
62
+ });
63
+ }
64
+ function isAbortError(err) {
65
+ if (!err || typeof err !== "object") return false;
66
+ const name = err.name;
67
+ return name === "AbortError";
68
+ }
69
+ function messageOf(err) {
70
+ if (err instanceof Error) return err.message;
71
+ try {
72
+ return String(err);
73
+ } catch {
74
+ return "unknown error";
75
+ }
76
+ }
77
+
78
+ // src/client.ts
79
+ var Usage = class _Usage {
80
+ constructor(promptTokens = 0, completionTokens = 0, totalTokens = 0, promptCacheHitTokens = 0, promptCacheMissTokens = 0) {
81
+ this.promptTokens = promptTokens;
82
+ this.completionTokens = completionTokens;
83
+ this.totalTokens = totalTokens;
84
+ this.promptCacheHitTokens = promptCacheHitTokens;
85
+ this.promptCacheMissTokens = promptCacheMissTokens;
86
+ }
87
+ promptTokens;
88
+ completionTokens;
89
+ totalTokens;
90
+ promptCacheHitTokens;
91
+ promptCacheMissTokens;
92
+ get cacheHitRatio() {
93
+ const denom = this.promptCacheHitTokens + this.promptCacheMissTokens;
94
+ return denom > 0 ? this.promptCacheHitTokens / denom : 0;
95
+ }
96
+ static fromApi(raw) {
97
+ const u = raw ?? {};
98
+ return new _Usage(
99
+ u.prompt_tokens ?? 0,
100
+ u.completion_tokens ?? 0,
101
+ u.total_tokens ?? 0,
102
+ u.prompt_cache_hit_tokens ?? 0,
103
+ u.prompt_cache_miss_tokens ?? 0
104
+ );
105
+ }
106
+ };
107
+ var DeepSeekClient = class {
108
+ apiKey;
109
+ baseUrl;
110
+ timeoutMs;
111
+ retry;
112
+ _fetch;
113
+ constructor(opts = {}) {
114
+ const apiKey = opts.apiKey ?? process.env.DEEPSEEK_API_KEY;
115
+ if (!apiKey) {
116
+ throw new Error(
117
+ "DEEPSEEK_API_KEY is not set. Put it in .env or pass apiKey to DeepSeekClient."
118
+ );
119
+ }
120
+ this.apiKey = apiKey;
121
+ this.baseUrl = (opts.baseUrl ?? process.env.DEEPSEEK_BASE_URL ?? "https://api.deepseek.com").replace(/\/+$/, "");
122
+ this.timeoutMs = opts.timeoutMs ?? 12e4;
123
+ this._fetch = opts.fetch ?? globalThis.fetch.bind(globalThis);
124
+ this.retry = opts.retry ?? {};
125
+ }
126
+ buildPayload(opts, stream) {
127
+ const payload = {
128
+ model: opts.model,
129
+ messages: opts.messages,
130
+ stream
131
+ };
132
+ if (opts.tools?.length) payload.tools = opts.tools;
133
+ if (opts.temperature !== void 0) payload.temperature = opts.temperature;
134
+ if (opts.maxTokens !== void 0) payload.max_tokens = opts.maxTokens;
135
+ return payload;
136
+ }
137
+ async chat(opts) {
138
+ const ctrl = new AbortController();
139
+ const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
140
+ const signal = opts.signal ?? ctrl.signal;
141
+ try {
142
+ const resp = await fetchWithRetry(
143
+ this._fetch,
144
+ `${this.baseUrl}/chat/completions`,
145
+ {
146
+ method: "POST",
147
+ headers: {
148
+ Authorization: `Bearer ${this.apiKey}`,
149
+ "Content-Type": "application/json"
150
+ },
151
+ body: JSON.stringify(this.buildPayload(opts, false)),
152
+ signal
153
+ },
154
+ { ...this.retry, signal }
155
+ );
156
+ if (!resp.ok) {
157
+ throw new Error(`DeepSeek ${resp.status}: ${await resp.text()}`);
158
+ }
159
+ const data = await resp.json();
160
+ const choice = data.choices?.[0]?.message ?? {};
161
+ return {
162
+ content: choice.content ?? "",
163
+ reasoningContent: choice.reasoning_content ?? null,
164
+ toolCalls: choice.tool_calls ?? [],
165
+ usage: Usage.fromApi(data.usage),
166
+ raw: data
167
+ };
168
+ } finally {
169
+ clearTimeout(timer);
170
+ }
171
+ }
172
+ async *stream(opts) {
173
+ const ctrl = new AbortController();
174
+ const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
175
+ const signal = opts.signal ?? ctrl.signal;
176
+ let resp;
177
+ try {
178
+ resp = await fetchWithRetry(
179
+ this._fetch,
180
+ `${this.baseUrl}/chat/completions`,
181
+ {
182
+ method: "POST",
183
+ headers: {
184
+ Authorization: `Bearer ${this.apiKey}`,
185
+ "Content-Type": "application/json",
186
+ Accept: "text/event-stream"
187
+ },
188
+ body: JSON.stringify(this.buildPayload(opts, true)),
189
+ signal
190
+ },
191
+ { ...this.retry, signal }
192
+ );
193
+ } catch (err) {
194
+ clearTimeout(timer);
195
+ throw err;
196
+ }
197
+ if (!resp.ok || !resp.body) {
198
+ clearTimeout(timer);
199
+ throw new Error(`DeepSeek ${resp.status}: ${await resp.text().catch(() => "")}`);
200
+ }
201
+ const queue = [];
202
+ let done = false;
203
+ const parser = createParser({
204
+ onEvent: (ev) => {
205
+ if (!ev.data || ev.data === "[DONE]") {
206
+ done = true;
207
+ return;
208
+ }
209
+ try {
210
+ const json = JSON.parse(ev.data);
211
+ const delta = json.choices?.[0]?.delta ?? {};
212
+ const finishReason = json.choices?.[0]?.finish_reason ?? void 0;
213
+ const chunk = { raw: json, finishReason };
214
+ if (typeof delta.content === "string" && delta.content.length > 0) {
215
+ chunk.contentDelta = delta.content;
216
+ }
217
+ if (typeof delta.reasoning_content === "string" && delta.reasoning_content.length > 0) {
218
+ chunk.reasoningDelta = delta.reasoning_content;
219
+ }
220
+ if (Array.isArray(delta.tool_calls) && delta.tool_calls.length > 0) {
221
+ const tc = delta.tool_calls[0];
222
+ chunk.toolCallDelta = {
223
+ index: tc.index ?? 0,
224
+ id: tc.id,
225
+ name: tc.function?.name,
226
+ argumentsDelta: tc.function?.arguments
227
+ };
228
+ }
229
+ if (json.usage) {
230
+ chunk.usage = Usage.fromApi(json.usage);
231
+ }
232
+ queue.push(chunk);
233
+ } catch {
234
+ }
235
+ }
236
+ });
237
+ const reader = resp.body.getReader();
238
+ const decoder = new TextDecoder();
239
+ try {
240
+ while (true) {
241
+ if (queue.length > 0) {
242
+ yield queue.shift();
243
+ continue;
244
+ }
245
+ if (done) break;
246
+ const { value, done: streamDone } = await reader.read();
247
+ if (streamDone) break;
248
+ parser.feed(decoder.decode(value, { stream: true }));
249
+ }
250
+ while (queue.length > 0) yield queue.shift();
251
+ } finally {
252
+ clearTimeout(timer);
253
+ reader.releaseLock();
254
+ }
255
+ }
256
+ };
257
+
258
+ export {
259
+ Usage,
260
+ DeepSeekClient
261
+ };
262
+ //# sourceMappingURL=chunk-OSNTDDD6.js.map