@thejeetsingh/kalcode 2.0.0 → 2.2.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.
Files changed (125) hide show
  1. package/README.md +0 -4
  2. package/dist/bin/kalcode.d.ts +2 -0
  3. package/dist/bin/kalcode.js +12 -0
  4. package/dist/bin/kalcode.js.map +1 -0
  5. package/dist/src/agent/context.d.ts +6 -0
  6. package/dist/src/agent/context.js +60 -0
  7. package/dist/src/agent/context.js.map +1 -0
  8. package/dist/src/agent/history.d.ts +8 -0
  9. package/dist/src/agent/history.js +59 -0
  10. package/dist/src/agent/history.js.map +1 -0
  11. package/dist/src/agent/loop.d.ts +6 -0
  12. package/dist/src/agent/loop.js +235 -0
  13. package/dist/src/agent/loop.js.map +1 -0
  14. package/dist/src/agent/memory.d.ts +2 -0
  15. package/dist/src/agent/memory.js +27 -0
  16. package/dist/src/agent/memory.js.map +1 -0
  17. package/dist/src/agent/permissions.d.ts +5 -0
  18. package/dist/src/agent/permissions.js +66 -0
  19. package/dist/src/agent/permissions.js.map +1 -0
  20. package/dist/src/agent/text-tool-parser.d.ts +2 -0
  21. package/dist/src/agent/text-tool-parser.js +68 -0
  22. package/dist/src/agent/text-tool-parser.js.map +1 -0
  23. package/dist/src/api/client.d.ts +2 -0
  24. package/dist/src/api/client.js +86 -0
  25. package/dist/src/api/client.js.map +1 -0
  26. package/dist/src/api/stream-parser.d.ts +2 -0
  27. package/dist/src/api/stream-parser.js +97 -0
  28. package/dist/src/api/stream-parser.js.map +1 -0
  29. package/dist/src/config.d.ts +7 -0
  30. package/dist/src/config.js +52 -0
  31. package/dist/src/config.js.map +1 -0
  32. package/dist/src/constants.d.ts +25 -0
  33. package/{src/constants.ts → dist/src/constants.js} +17 -19
  34. package/dist/src/constants.js.map +1 -0
  35. package/dist/src/git/git.d.ts +15 -0
  36. package/dist/src/git/git.js +73 -0
  37. package/dist/src/git/git.js.map +1 -0
  38. package/dist/src/index.d.ts +1 -0
  39. package/dist/src/index.js +415 -0
  40. package/dist/src/index.js.map +1 -0
  41. package/dist/src/proxy/server.d.ts +1 -0
  42. package/dist/src/proxy/server.js +92 -0
  43. package/dist/src/proxy/server.js.map +1 -0
  44. package/dist/src/tools/edit-file.d.ts +2 -0
  45. package/dist/src/tools/edit-file.js +88 -0
  46. package/dist/src/tools/edit-file.js.map +1 -0
  47. package/dist/src/tools/glob-tool.d.ts +2 -0
  48. package/dist/src/tools/glob-tool.js +52 -0
  49. package/dist/src/tools/glob-tool.js.map +1 -0
  50. package/dist/src/tools/grep.d.ts +2 -0
  51. package/dist/src/tools/grep.js +93 -0
  52. package/dist/src/tools/grep.js.map +1 -0
  53. package/dist/src/tools/list-directory.d.ts +2 -0
  54. package/dist/src/tools/list-directory.js +90 -0
  55. package/dist/src/tools/list-directory.js.map +1 -0
  56. package/dist/src/tools/read-file.d.ts +2 -0
  57. package/dist/src/tools/read-file.js +64 -0
  58. package/dist/src/tools/read-file.js.map +1 -0
  59. package/dist/src/tools/registry.d.ts +4 -0
  60. package/dist/src/tools/registry.js +32 -0
  61. package/dist/src/tools/registry.js.map +1 -0
  62. package/dist/src/tools/run-command.d.ts +2 -0
  63. package/dist/src/tools/run-command.js +98 -0
  64. package/dist/src/tools/run-command.js.map +1 -0
  65. package/dist/src/tools/write-file.d.ts +2 -0
  66. package/dist/src/tools/write-file.js +39 -0
  67. package/dist/src/tools/write-file.js.map +1 -0
  68. package/dist/src/types.d.ts +61 -0
  69. package/dist/src/types.js +2 -0
  70. package/dist/src/types.js.map +1 -0
  71. package/dist/src/ui/input.d.ts +5 -0
  72. package/dist/src/ui/input.js +52 -0
  73. package/dist/src/ui/input.js.map +1 -0
  74. package/dist/src/ui/model-picker.d.ts +7 -0
  75. package/dist/src/ui/model-picker.js +70 -0
  76. package/dist/src/ui/model-picker.js.map +1 -0
  77. package/dist/src/ui/skills-picker.d.ts +2 -0
  78. package/dist/src/ui/skills-picker.js +95 -0
  79. package/dist/src/ui/skills-picker.js.map +1 -0
  80. package/dist/src/ui/skills.d.ts +14 -0
  81. package/dist/src/ui/skills.js +137 -0
  82. package/dist/src/ui/skills.js.map +1 -0
  83. package/dist/src/ui/spinner.d.ts +5 -0
  84. package/dist/src/ui/spinner.js +49 -0
  85. package/dist/src/ui/spinner.js.map +1 -0
  86. package/dist/src/ui/stream-renderer.d.ts +2 -0
  87. package/dist/src/ui/stream-renderer.js +66 -0
  88. package/dist/src/ui/stream-renderer.js.map +1 -0
  89. package/dist/src/ui/terminal.d.ts +24 -0
  90. package/dist/src/ui/terminal.js +272 -0
  91. package/dist/src/ui/terminal.js.map +1 -0
  92. package/package.json +16 -16
  93. package/api/health.ts +0 -10
  94. package/api/v1/chat/completions.ts +0 -59
  95. package/bin/kalcode.ts +0 -14
  96. package/src/agent/context.ts +0 -62
  97. package/src/agent/history.ts +0 -70
  98. package/src/agent/loop.ts +0 -282
  99. package/src/agent/memory.ts +0 -26
  100. package/src/agent/permissions.ts +0 -84
  101. package/src/agent/text-tool-parser.ts +0 -71
  102. package/src/api/client.ts +0 -110
  103. package/src/api/stream-parser.ts +0 -109
  104. package/src/config.ts +0 -61
  105. package/src/git/git.ts +0 -86
  106. package/src/index.ts +0 -403
  107. package/src/proxy/server.ts +0 -128
  108. package/src/tools/edit-file.ts +0 -97
  109. package/src/tools/glob-tool.ts +0 -59
  110. package/src/tools/grep.ts +0 -96
  111. package/src/tools/list-directory.ts +0 -101
  112. package/src/tools/read-file.ts +0 -71
  113. package/src/tools/registry.ts +0 -41
  114. package/src/tools/run-command.ts +0 -99
  115. package/src/tools/write-file.ts +0 -42
  116. package/src/types.ts +0 -68
  117. package/src/ui/input.ts +0 -60
  118. package/src/ui/model-picker.ts +0 -92
  119. package/src/ui/skills-picker.ts +0 -113
  120. package/src/ui/skills.ts +0 -152
  121. package/src/ui/spinner.ts +0 -56
  122. package/src/ui/stream-renderer.ts +0 -69
  123. package/src/ui/terminal.ts +0 -337
  124. package/tsconfig.json +0 -15
  125. package/vercel.json +0 -12
package/src/agent/loop.ts DELETED
@@ -1,282 +0,0 @@
1
- import { streamChat } from "../api/client.js";
2
- import { getAllDefinitions, executeToolCall } from "../tools/registry.js";
3
- import { addMessage, getMessages } from "./history.js";
4
- import { MAX_LOOP_ITERATIONS } from "../constants.js";
5
- import type { Message, ToolCallAccumulator, TokenUsage } from "../types.js";
6
- import {
7
- startSpinner,
8
- stopSpinner,
9
- updateSpinner,
10
- succeedSpinner,
11
- failSpinner,
12
- } from "../ui/spinner.js";
13
- import {
14
- renderToolCall,
15
- renderToolResult,
16
- renderError,
17
- renderUsage,
18
- renderRetryWait,
19
- } from "../ui/terminal.js";
20
- import { writeStreamToken, finishStream } from "../ui/stream-renderer.js";
21
- import { parseTextToolCalls } from "./text-tool-parser.js";
22
- import { needsPermission, requestPermission } from "./permissions.js";
23
-
24
- let compactMode = false;
25
- let interrupted = false;
26
- let askMode = false;
27
-
28
- export function setCompact(val: boolean): void { compactMode = val; }
29
- export function getCompact(): boolean { return compactMode; }
30
- export function setAskMode(val: boolean): void { askMode = val; }
31
- export function getAskMode(): boolean { return askMode; }
32
- export function interruptAgent(): void { interrupted = true; }
33
-
34
- export async function runAgentLoop(
35
- apiKey: string,
36
- model: string,
37
- userMessage: string
38
- ): Promise<void> {
39
- addMessage({ role: "user", content: userMessage });
40
- interrupted = false;
41
-
42
- // In ask mode, only use read-only tools
43
- const allDefs = getAllDefinitions();
44
- const toolDefs = askMode
45
- ? allDefs.filter(t => !["writeFile", "editFile", "runCommand"].includes(t.function.name))
46
- : allDefs;
47
-
48
- let totalUsage: TokenUsage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
49
-
50
- for (let iteration = 0; iteration < MAX_LOOP_ITERATIONS; iteration++) {
51
- if (interrupted) break;
52
-
53
- startSpinner("thinking");
54
-
55
- let textContent = "";
56
- const toolCallAccumulators: Map<number, ToolCallAccumulator> = new Map();
57
- let hasError = false;
58
- let turnUsage: TokenUsage | null = null;
59
-
60
- try {
61
- let firstToken = true;
62
- const stream = streamChat(
63
- apiKey,
64
- model,
65
- getMessages(),
66
- toolDefs,
67
- (attempt, waitSec) => {
68
- renderRetryWait(attempt, waitSec);
69
- }
70
- );
71
-
72
- for await (const delta of stream) {
73
- if (interrupted) break;
74
-
75
- if (delta.type === "content" && delta.content) {
76
- if (firstToken) {
77
- stopSpinner();
78
- firstToken = false;
79
- }
80
- textContent += delta.content;
81
- // Only stream text if we haven't seen tool calls yet
82
- if (toolCallAccumulators.size === 0) {
83
- writeStreamToken(delta.content);
84
- }
85
- } else if (delta.type === "tool_call" && delta.toolCall) {
86
- // If we were streaming text and now got tool calls, clear the text
87
- if (toolCallAccumulators.size === 0 && textContent.trim()) {
88
- if (!firstToken) {
89
- clearStreamedText(textContent);
90
- finishStream();
91
- }
92
- }
93
- if (firstToken) {
94
- firstToken = false;
95
- }
96
- const tc = delta.toolCall;
97
- if (!toolCallAccumulators.has(tc.index)) {
98
- toolCallAccumulators.set(tc.index, {
99
- id: tc.id || "",
100
- function: { name: "", arguments: "" },
101
- });
102
- }
103
- const acc = toolCallAccumulators.get(tc.index)!;
104
- if (tc.id) acc.id = tc.id;
105
- if (tc.function?.name) acc.function.name += tc.function.name;
106
- if (tc.function?.arguments) acc.function.arguments += tc.function.arguments;
107
- // Show what tool is being prepared
108
- const toolName = acc.function.name || "tool call";
109
- updateSpinner(`preparing ${toolName}`);
110
- } else if (delta.type === "error") {
111
- stopSpinner();
112
- renderError(delta.error || "Unknown API error");
113
- hasError = true;
114
- break;
115
- }
116
-
117
- if (delta.usage) turnUsage = delta.usage;
118
- }
119
- } catch (err: unknown) {
120
- stopSpinner();
121
- renderError(`Stream error: ${err instanceof Error ? err.message : String(err)}`);
122
- return;
123
- }
124
-
125
- if (interrupted) {
126
- stopSpinner();
127
- finishStream();
128
- break;
129
- }
130
-
131
- // Parse text-based tool calls
132
- if (toolCallAccumulators.size === 0 && textContent.trim()) {
133
- const parsed = parseTextToolCalls(textContent);
134
- if (parsed && parsed.length > 0) {
135
- clearStreamedText(textContent);
136
- for (const tc of parsed) {
137
- toolCallAccumulators.set(toolCallAccumulators.size, tc);
138
- }
139
- textContent = "";
140
- }
141
- }
142
-
143
- // Stop spinner after stream ends (was still running during tool_call accumulation)
144
- stopSpinner();
145
-
146
- if (toolCallAccumulators.size > 0 && textContent.trim()) {
147
- clearStreamedText(textContent);
148
- }
149
-
150
- finishStream();
151
-
152
- if (hasError) return;
153
-
154
- if (turnUsage) {
155
- totalUsage.prompt_tokens += turnUsage.prompt_tokens;
156
- totalUsage.completion_tokens += turnUsage.completion_tokens;
157
- totalUsage.total_tokens += turnUsage.total_tokens;
158
- }
159
-
160
- const assistantMsg: Message = {
161
- role: "assistant",
162
- content: toolCallAccumulators.size > 0 ? null : (textContent || null),
163
- };
164
-
165
- if (toolCallAccumulators.size > 0) {
166
- assistantMsg.tool_calls = Array.from(toolCallAccumulators.values()).map(
167
- (acc) => ({
168
- id: acc.id,
169
- type: "function" as const,
170
- function: {
171
- name: acc.function.name,
172
- arguments: acc.function.arguments,
173
- },
174
- })
175
- );
176
- }
177
-
178
- addMessage(assistantMsg);
179
-
180
- if (!assistantMsg.tool_calls || assistantMsg.tool_calls.length === 0) {
181
- renderUsage(totalUsage.total_tokens > 0 ? totalUsage : null);
182
- return;
183
- }
184
-
185
- // Execute tool calls
186
- for (const tc of assistantMsg.tool_calls) {
187
- if (interrupted) break;
188
-
189
- let parsedArgs: Record<string, unknown>;
190
- try {
191
- parsedArgs = JSON.parse(tc.function.arguments || "{}");
192
- } catch {
193
- parsedArgs = {};
194
- }
195
-
196
- const toolName = tc.function.name;
197
- const summary = formatPermissionSummary(toolName, parsedArgs);
198
-
199
- // Permission check for write operations
200
- if (needsPermission(toolName)) {
201
- const allowed = await requestPermission(toolName, summary);
202
- if (!allowed) {
203
- renderToolCall(toolName, parsedArgs);
204
- console.log(` ${chalk.dim("⎿")} ${chalk.yellow("Denied by user")}`);
205
- addMessage({
206
- role: "tool",
207
- tool_call_id: tc.id,
208
- content: "Permission denied by user. Do not retry this operation.",
209
- });
210
- continue;
211
- }
212
- }
213
-
214
- renderToolCall(toolName, parsedArgs);
215
- startSpinner(toolName);
216
-
217
- try {
218
- const result = await executeToolCall(toolName, parsedArgs);
219
- succeedSpinner(toolName);
220
- renderToolResult(toolName, result, compactMode);
221
-
222
- addMessage({
223
- role: "tool",
224
- tool_call_id: tc.id,
225
- content: result,
226
- });
227
- } catch (err: unknown) {
228
- failSpinner(`${toolName} failed`);
229
- const errorMsg = err instanceof Error ? err.message : String(err);
230
- renderError(errorMsg);
231
-
232
- addMessage({
233
- role: "tool",
234
- tool_call_id: tc.id,
235
- content: `Error: ${errorMsg}`,
236
- });
237
- }
238
- }
239
- }
240
-
241
- if (interrupted) {
242
- console.log("");
243
- } else if (!interrupted) {
244
- renderError("Agent loop reached maximum iterations");
245
- }
246
- }
247
-
248
- function clearStreamedText(text: string): void {
249
- const cols = Math.max(20, process.stdout.columns || 80);
250
- const lines = text.split("\n");
251
- let rowsToClear = 0;
252
-
253
- for (const line of lines) {
254
- // Account for soft-wrapped rows to avoid leaving visual artifacts.
255
- const len = Math.max(1, line.length);
256
- rowsToClear += Math.ceil(len / cols);
257
- }
258
-
259
- for (let i = 0; i < rowsToClear; i++) {
260
- process.stdout.write("\x1b[2K");
261
- if (i < rowsToClear - 1) {
262
- process.stdout.write("\x1b[A");
263
- }
264
- }
265
- process.stdout.write("\r");
266
- }
267
-
268
- function formatPermissionSummary(name: string, args: Record<string, unknown>): string {
269
- switch (name) {
270
- case "writeFile":
271
- return String(args.filePath || "");
272
- case "editFile":
273
- return String(args.filePath || "");
274
- case "runCommand":
275
- return `$ ${String(args.command || "").slice(0, 60)}`;
276
- default:
277
- return "";
278
- }
279
- }
280
-
281
- // Need chalk for the denied message
282
- import chalk from "chalk";
@@ -1,26 +0,0 @@
1
- import { existsSync, readFileSync } from "fs";
2
- import { join } from "path";
3
-
4
- const MEMORY_FILES = ["KALCODE.md", "CONVENTIONS.md", ".kalcode.md"];
5
-
6
- export function loadProjectMemory(cwd: string): string | null {
7
- for (const name of MEMORY_FILES) {
8
- const path = join(cwd, name);
9
- if (existsSync(path)) {
10
- try {
11
- const content = readFileSync(path, "utf-8").trim();
12
- if (content) return content;
13
- } catch {
14
- // ignore
15
- }
16
- }
17
- }
18
- return null;
19
- }
20
-
21
- export function getMemoryFileName(cwd: string): string | null {
22
- for (const name of MEMORY_FILES) {
23
- if (existsSync(join(cwd, name))) return name;
24
- }
25
- return null;
26
- }
@@ -1,84 +0,0 @@
1
- import chalk from "chalk";
2
-
3
- export type PermissionLevel = "ask" | "auto" | "deny-writes";
4
-
5
- let permissionLevel: PermissionLevel = "ask";
6
- const sessionApprovals = new Set<string>();
7
-
8
- export function setPermissionLevel(level: PermissionLevel): void {
9
- permissionLevel = level;
10
- }
11
-
12
- export function getPermissionLevel(): PermissionLevel {
13
- return permissionLevel;
14
- }
15
-
16
- // Tools that modify state
17
- const WRITE_TOOLS = new Set(["writeFile", "editFile", "runCommand"]);
18
-
19
- export function needsPermission(toolName: string): boolean {
20
- if (permissionLevel === "auto") return false;
21
- if (permissionLevel === "deny-writes" && WRITE_TOOLS.has(toolName))
22
- return false;
23
- return WRITE_TOOLS.has(toolName);
24
- }
25
-
26
- export async function requestPermission(
27
- toolName: string,
28
- summary: string,
29
- ): Promise<boolean> {
30
- const key = `${toolName}:${summary}`;
31
- if (sessionApprovals.has(key)) return true;
32
-
33
- // Claude Code style: ? marker with clean options
34
- const promptText =
35
- ` ${chalk.yellow("?")} Allow ${chalk.bold.white(toolName)}` +
36
- (summary ? ` ${chalk.dim(summary)}` : "") +
37
- ` ${chalk.dim("(y)es / (n)o / (a)lways")} `;
38
-
39
- process.stdout.write(promptText);
40
-
41
- const answer = await readSingleKey();
42
- const writeDecision = (decision: string, color: (text: string) => string): void => {
43
- // Redraw the line to eliminate any echoed keypress artifacts (e.g. "yyes").
44
- process.stdout.write(`\r\x1b[2K${promptText}${color(decision)}\n`);
45
- };
46
-
47
- if (answer === "a") {
48
- writeDecision("always", chalk.green);
49
- setPermissionLevel("auto");
50
- return true;
51
- }
52
-
53
- if (answer === "y" || answer === "\r" || answer === "\n") {
54
- writeDecision("yes", chalk.green);
55
- sessionApprovals.add(key);
56
- return true;
57
- }
58
-
59
- writeDecision("no", chalk.red);
60
- return false;
61
- }
62
-
63
- function readSingleKey(): Promise<string> {
64
- return new Promise((resolve) => {
65
- const stdin = process.stdin;
66
- const wasRaw = stdin.isRaw;
67
- stdin.setRawMode(true);
68
- stdin.resume();
69
-
70
- function onData(data: Buffer) {
71
- stdin.removeListener("data", onData);
72
- stdin.setRawMode(wasRaw ?? false);
73
- const key = data.toString();
74
- // Ctrl+C
75
- if (key === "\x03") {
76
- resolve("n");
77
- return;
78
- }
79
- resolve(key.toLowerCase());
80
- }
81
-
82
- stdin.on("data", onData);
83
- });
84
- }
@@ -1,71 +0,0 @@
1
- import type { ToolCallAccumulator } from "../types.js";
2
-
3
- // Parse tool calls from model text output when the model doesn't use
4
- // the proper tool_call API. Handles formats like:
5
- // ```tool_code
6
- // [toolName(arg1='val1', arg2='val2')]
7
- // ```
8
- // or JSON-like: {"name": "toolName", "arguments": {"arg": "val"}}
9
-
10
- export function parseTextToolCalls(text: string): ToolCallAccumulator[] | null {
11
- const results: ToolCallAccumulator[] = [];
12
-
13
- // Pattern 1: ```tool_code blocks (gemma style)
14
- const toolCodeRegex = /```tool_code\s*\n([\s\S]*?)```/g;
15
- let match;
16
- while ((match = toolCodeRegex.exec(text)) !== null) {
17
- const block = match[1]!.trim();
18
- const calls = parseToolCodeBlock(block);
19
- results.push(...calls);
20
- }
21
-
22
- if (results.length > 0) return results;
23
-
24
- // Pattern 2: [toolName(args)] without code blocks
25
- const bracketRegex = /\[(\w+)\(([^)]*)\)\]/g;
26
- while ((match = bracketRegex.exec(text)) !== null) {
27
- const name = match[1]!;
28
- const argsStr = match[2]!;
29
- const args = parseKeyValueArgs(argsStr);
30
- results.push({
31
- id: `text-${Date.now()}-${results.length}`,
32
- function: { name, arguments: JSON.stringify(args) },
33
- });
34
- }
35
-
36
- return results.length > 0 ? results : null;
37
- }
38
-
39
- function parseToolCodeBlock(block: string): ToolCallAccumulator[] {
40
- const results: ToolCallAccumulator[] = [];
41
- // Match [toolName(key='value', key2='value2')] or [toolName(key="value")]
42
- const callRegex = /\[?(\w+)\(([^)]*)\)\]?/g;
43
- let match;
44
- while ((match = callRegex.exec(block)) !== null) {
45
- const name = match[1]!;
46
- const argsStr = match[2]!;
47
- const args = parseKeyValueArgs(argsStr);
48
- results.push({
49
- id: `text-${Date.now()}-${results.length}`,
50
- function: { name, arguments: JSON.stringify(args) },
51
- });
52
- }
53
- return results;
54
- }
55
-
56
- function parseKeyValueArgs(argsStr: string): Record<string, unknown> {
57
- const args: Record<string, unknown> = {};
58
- // Match key='value' or key="value" or key=value or key=true/false/number
59
- const argRegex = /(\w+)\s*=\s*(?:'([^']*)'|"([^"]*)"|(\S+))/g;
60
- let match;
61
- while ((match = argRegex.exec(argsStr)) !== null) {
62
- const key = match[1]!;
63
- const value = match[2] ?? match[3] ?? match[4]!;
64
- // Try to parse as boolean/number
65
- if (value === "true") args[key] = true;
66
- else if (value === "false") args[key] = false;
67
- else if (!isNaN(Number(value)) && value !== "") args[key] = Number(value);
68
- else args[key] = value;
69
- }
70
- return args;
71
- }
package/src/api/client.ts DELETED
@@ -1,110 +0,0 @@
1
- import { API_URL, RATE_LIMIT_MAX, RATE_LIMIT_WINDOW, MAX_RETRIES, RETRY_BASE_DELAY } from "../constants.js";
2
- import type { Message, StreamDelta, ToolDefinition } from "../types.js";
3
- import { parseSSEStream } from "./stream-parser.js";
4
-
5
- const requestTimestamps: number[] = [];
6
-
7
- async function waitForRateLimit(): Promise<void> {
8
- const now = Date.now();
9
- while (requestTimestamps.length > 0 && requestTimestamps[0]! < now - RATE_LIMIT_WINDOW) {
10
- requestTimestamps.shift();
11
- }
12
-
13
- if (requestTimestamps.length >= RATE_LIMIT_MAX) {
14
- const oldestInWindow = requestTimestamps[0]!;
15
- const waitMs = oldestInWindow + RATE_LIMIT_WINDOW - now + 100;
16
- if (waitMs > 0) {
17
- await new Promise((resolve) => setTimeout(resolve, waitMs));
18
- }
19
- }
20
-
21
- requestTimestamps.push(Date.now());
22
- }
23
-
24
- export async function* streamChat(
25
- apiKey: string,
26
- model: string,
27
- messages: Message[],
28
- tools?: ToolDefinition[],
29
- onRetry?: (attempt: number, waitSec: number) => void
30
- ): AsyncGenerator<StreamDelta> {
31
- const proxyUrl = (process.env.KALCODE_PROXY_URL || "").trim();
32
- const proxyToken = (process.env.KALCODE_PROXY_TOKEN || "").trim();
33
- const targetUrl = proxyUrl
34
- ? `${proxyUrl.replace(/\/+$/, "")}/v1/chat/completions`
35
- : API_URL;
36
- const authToken = proxyUrl ? proxyToken : apiKey;
37
-
38
- if (proxyUrl && !proxyToken) {
39
- yield {
40
- type: "error",
41
- error: "KALCODE_PROXY_URL is set but KALCODE_PROXY_TOKEN is missing.",
42
- };
43
- return;
44
- }
45
-
46
- if (!authToken) {
47
- yield {
48
- type: "error",
49
- error: proxyUrl
50
- ? "Missing proxy auth token."
51
- : "Missing NVIDIA API key.",
52
- };
53
- return;
54
- }
55
-
56
- for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
57
- await waitForRateLimit();
58
-
59
- const body: Record<string, unknown> = {
60
- model,
61
- messages,
62
- stream: true,
63
- max_tokens: 16384,
64
- temperature: 0,
65
- };
66
-
67
- if (tools && tools.length > 0) {
68
- body.tools = tools;
69
- body.tool_choice = "auto";
70
- }
71
-
72
- const response = await fetch(targetUrl, {
73
- method: "POST",
74
- headers: {
75
- "Content-Type": "application/json",
76
- Authorization: `Bearer ${authToken}`,
77
- },
78
- body: JSON.stringify(body),
79
- });
80
-
81
- if (response.status === 429 && attempt < MAX_RETRIES) {
82
- const retryAfter = response.headers.get("retry-after");
83
- const waitMs = retryAfter
84
- ? parseInt(retryAfter) * 1000
85
- : RETRY_BASE_DELAY * (attempt + 1);
86
- const waitSec = Math.ceil(waitMs / 1000);
87
- onRetry?.(attempt + 1, waitSec);
88
- await new Promise((resolve) => setTimeout(resolve, waitMs));
89
- continue;
90
- }
91
-
92
- if (!response.ok) {
93
- const text = await response.text();
94
- let errorMsg: string;
95
- try {
96
- const parsed = JSON.parse(text);
97
- errorMsg = parsed.error?.message || text;
98
- } catch {
99
- errorMsg = text;
100
- }
101
- yield { type: "error", error: `API error ${response.status}: ${errorMsg}` };
102
- return;
103
- }
104
-
105
- yield* parseSSEStream(response);
106
- return;
107
- }
108
-
109
- yield { type: "error", error: "Max retries exceeded (rate limited)" };
110
- }
@@ -1,109 +0,0 @@
1
- import type { StreamDelta } from "../types.js";
2
-
3
- export async function* parseSSEStream(
4
- response: Response
5
- ): AsyncGenerator<StreamDelta> {
6
- const reader = response.body?.getReader();
7
- if (!reader) {
8
- yield { type: "error", error: "No response body" };
9
- return;
10
- }
11
-
12
- const decoder = new TextDecoder();
13
- let buffer = "";
14
-
15
- try {
16
- while (true) {
17
- const { done, value } = await reader.read();
18
- if (done) break;
19
-
20
- buffer += decoder.decode(value, { stream: true });
21
- const lines = buffer.split("\n");
22
- buffer = lines.pop() || "";
23
-
24
- for (const line of lines) {
25
- const trimmed = line.trim();
26
- if (!trimmed || trimmed.startsWith(":")) continue;
27
-
28
- if (trimmed.startsWith("data: ")) {
29
- const data = trimmed.slice(6);
30
- if (data === "[DONE]") {
31
- yield { type: "done" };
32
- return;
33
- }
34
-
35
- try {
36
- const parsed = JSON.parse(data);
37
-
38
- if (parsed.error) {
39
- yield {
40
- type: "error",
41
- error:
42
- typeof parsed.error === "string"
43
- ? parsed.error
44
- : parsed.error.message || JSON.stringify(parsed.error),
45
- };
46
- continue;
47
- }
48
-
49
- const choice = parsed.choices?.[0];
50
- if (!choice) {
51
- // Check for usage in non-choice chunks
52
- if (parsed.usage) {
53
- yield {
54
- type: "done",
55
- usage: {
56
- prompt_tokens: parsed.usage.prompt_tokens || 0,
57
- completion_tokens: parsed.usage.completion_tokens || 0,
58
- total_tokens: parsed.usage.total_tokens || 0,
59
- },
60
- };
61
- }
62
- continue;
63
- }
64
-
65
- const delta = choice.delta;
66
- if (!delta) continue;
67
-
68
- // Build the base delta
69
- if (delta.content) {
70
- yield { type: "content", content: delta.content };
71
- }
72
-
73
- if (delta.tool_calls) {
74
- for (const tc of delta.tool_calls) {
75
- yield {
76
- type: "tool_call",
77
- toolCall: {
78
- index: tc.index,
79
- id: tc.id,
80
- function: tc.function,
81
- },
82
- };
83
- }
84
- }
85
-
86
- // Extract usage if present
87
- if (parsed.usage) {
88
- yield {
89
- type: "content",
90
- content: "",
91
- usage: {
92
- prompt_tokens: parsed.usage.prompt_tokens || 0,
93
- completion_tokens: parsed.usage.completion_tokens || 0,
94
- total_tokens: parsed.usage.total_tokens || 0,
95
- },
96
- };
97
- }
98
- } catch {
99
- // skip malformed JSON chunks
100
- }
101
- }
102
- }
103
- }
104
- } finally {
105
- reader.releaseLock();
106
- }
107
-
108
- yield { type: "done" };
109
- }