@sean.holung/minicode 0.1.1 → 0.2.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.
Files changed (40) hide show
  1. package/README.md +47 -6
  2. package/dist/src/agent/config.js +1 -1
  3. package/dist/src/cli/args.js +65 -0
  4. package/dist/src/index.js +109 -26
  5. package/dist/src/session/session-store.js +82 -0
  6. package/dist/src/tools/find-references.js +1 -1
  7. package/dist/src/tools/get-dependencies.js +1 -1
  8. package/dist/src/tools/read-symbol.js +1 -2
  9. package/dist/src/tools/registry.js +26 -61
  10. package/dist/src/tools/search-code-map.js +1 -1
  11. package/dist/src/ui/cli-ink.js +91 -19
  12. package/dist/tests/agent.test.js +2 -3
  13. package/dist/tests/cli-args.test.js +73 -0
  14. package/dist/tests/cli-oneshot.integration.test.js +26 -0
  15. package/dist/tests/dependency-graph.test.js +12 -12
  16. package/dist/tests/file-tools.test.js +2 -3
  17. package/dist/tests/find-references.test.js +6 -6
  18. package/dist/tests/guardrails.test.js +1 -1
  19. package/dist/tests/indexer.test.js +9 -9
  20. package/dist/tests/model-client-openai.test.js +1 -1
  21. package/dist/tests/read-symbol.test.js +16 -17
  22. package/dist/tests/search-code-map.test.js +2 -2
  23. package/dist/tests/session-store.test.js +115 -0
  24. package/dist/tests/session.test.js +1 -1
  25. package/dist/tests/system-prompt.test.js +1 -1
  26. package/dist/tests/tool-registry.test.js +1 -1
  27. package/package.json +7 -2
  28. package/dist/src/agent/agent.js +0 -209
  29. package/dist/src/agent/types.js +0 -1
  30. package/dist/src/model/client.js +0 -374
  31. package/dist/src/prompt/system-prompt.js +0 -91
  32. package/dist/src/safety/guardrails.js +0 -55
  33. package/dist/src/session/session.js +0 -95
  34. package/dist/src/tools/edit-file.js +0 -73
  35. package/dist/src/tools/helpers.js +0 -42
  36. package/dist/src/tools/list-files.js +0 -63
  37. package/dist/src/tools/read-file.js +0 -79
  38. package/dist/src/tools/run-command.js +0 -92
  39. package/dist/src/tools/search.js +0 -153
  40. package/dist/src/tools/write-file.js +0 -44
@@ -1,6 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { test } from "node:test";
3
- import { ToolRegistry } from "../src/tools/registry.js";
3
+ import { ToolRegistry } from "@minicode/agent-sdk";
4
4
  const echoTool = {
5
5
  name: "echo",
6
6
  description: "Echo input value",
package/package.json CHANGED
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "name": "@sean.holung/minicode",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
+ "workspaces": [
5
+ "packages/*"
6
+ ],
4
7
  "description": "A coding agent for mini models",
5
8
  "type": "module",
6
9
  "engines": {
@@ -17,16 +20,18 @@
17
20
  "scripts": {
18
21
  "dev": "node --env-file=.env --import tsx src/index.ts",
19
22
  "dev:ink": "CLI_UI_MODE=ink node --env-file=.env --import tsx src/index.ts",
20
- "build": "tsc -p tsconfig.json",
23
+ "build": "npm run build --workspace=packages/agent-sdk && tsc -p tsconfig.json && chmod +x dist/src/index.js",
21
24
  "start": "node dist/src/index.js",
22
25
  "install:global": "npm run build && npm link",
23
26
  "lint": "eslint \"src/**/*.ts\" \"src/**/*.tsx\" \"tests/**/*.ts\" --max-warnings=0",
27
+ "pretest": "npm run build --workspace=packages/agent-sdk",
24
28
  "test": "node --test --import tsx \"tests/**/*.test.ts\"",
25
29
  "verify-index": "node --import tsx test-programs/verify-index/verify.ts"
26
30
  },
27
31
  "dependencies": {
28
32
  "@anthropic-ai/sdk": "^0.76.0",
29
33
  "@inkjs/ui": "^2.0.0",
34
+ "@minicode/agent-sdk": "*",
30
35
  "dotenv": "^17.3.1",
31
36
  "ink": "^6.8.0",
32
37
  "picocolors": "^1.1.1",
@@ -1,209 +0,0 @@
1
- import { buildSystemPrompt } from "../prompt/system-prompt.js";
2
- import { ensureStepWithinLimit } from "../safety/guardrails.js";
3
- import { Session } from "../session/session.js";
4
- function stableSerialize(value) {
5
- if (Array.isArray(value)) {
6
- return `[${value.map((item) => stableSerialize(item)).join(",")}]`;
7
- }
8
- if (value && typeof value === "object") {
9
- const entries = Object.entries(value)
10
- .sort(([a], [b]) => a.localeCompare(b))
11
- .map(([key, entryValue]) => `${JSON.stringify(key)}:${stableSerialize(entryValue)}`);
12
- return `{${entries.join(",")}}`;
13
- }
14
- return JSON.stringify(value);
15
- }
16
- function signatureForToolCall(toolCall) {
17
- return `${toolCall.name}:${stableSerialize(toolCall.input)}`;
18
- }
19
- function formatToolCallForProgress(toolCall, maxArgsLen = 100) {
20
- const argsStr = JSON.stringify(toolCall.input);
21
- const truncated = argsStr.length > maxArgsLen
22
- ? argsStr.slice(0, maxArgsLen) + "..."
23
- : argsStr;
24
- return `${toolCall.name}(${truncated})`;
25
- }
26
- const VERBOSE_SEP = "─".repeat(60);
27
- const PROGRESS_THINKING_MAX = 200;
28
- export class CodingAgent {
29
- session;
30
- config;
31
- modelClient;
32
- toolRegistry;
33
- projectIndex;
34
- verbose;
35
- onProgress;
36
- onUiUpdate;
37
- constructor(params) {
38
- this.config = params.config;
39
- this.modelClient = params.modelClient;
40
- this.toolRegistry = params.toolRegistry;
41
- this.session = params.session ?? new Session();
42
- this.projectIndex = params.projectIndex;
43
- this.verbose = params.verbose ?? false;
44
- this.onProgress = params.onProgress;
45
- this.onUiUpdate = params.onUiUpdate;
46
- }
47
- getSession() {
48
- return this.session;
49
- }
50
- async runTurn(userMessage, options) {
51
- this.session.addMessage({
52
- role: "user",
53
- content: userMessage,
54
- });
55
- const toolSchemas = this.toolRegistry.getToolSchemas();
56
- const codeMapResult = this.projectIndex?.getCodeMap();
57
- const systemPrompt = buildSystemPrompt(this.config, toolSchemas, codeMapResult);
58
- const recentToolCallFingerprints = [];
59
- let totalInputTokens = 0;
60
- let totalOutputTokens = 0;
61
- for (let step = 0; step < this.config.maxSteps; step += 1) {
62
- ensureStepWithinLimit(step, this.config.maxSteps);
63
- if (this.onUiUpdate) {
64
- this.onUiUpdate({ type: "step", step });
65
- }
66
- this.session.trim(this.config.maxContextTokens, this.config.keepRecentMessages);
67
- const messages = this.session.getMessages();
68
- if (this.verbose) {
69
- console.error(`\n${VERBOSE_SEP}`);
70
- console.error(`[verbose] Request (step ${step})`);
71
- console.error(`${VERBOSE_SEP}`);
72
- console.error("\n[System Prompt]\n", systemPrompt);
73
- console.error("\n[Messages]\n", JSON.stringify(messages, null, 2));
74
- console.error(VERBOSE_SEP);
75
- }
76
- const response = await this.modelClient.chat({
77
- model: this.config.model,
78
- system: systemPrompt,
79
- messages,
80
- tools: toolSchemas,
81
- maxTokens: this.config.maxTokens,
82
- ...(this.onUiUpdate
83
- ? {
84
- onStream: (chunk) => {
85
- this.onUiUpdate({ type: "streaming_chunk", content: chunk });
86
- },
87
- }
88
- : {}),
89
- ...(options?.signal && { signal: options.signal }),
90
- });
91
- totalInputTokens += response.usage.inputTokens;
92
- totalOutputTokens += response.usage.outputTokens;
93
- if (this.verbose) {
94
- console.error(`\n${VERBOSE_SEP}`);
95
- console.error("[verbose] Response");
96
- console.error(`${VERBOSE_SEP}`);
97
- console.error("Text:", response.text);
98
- console.error("Tool calls:", response.toolCalls.length);
99
- if (response.toolCalls.length > 0) {
100
- console.error("Tools:", response.toolCalls.map((t) => `${t.name}(${JSON.stringify(t.input)})`).join(", "));
101
- }
102
- console.error("Usage:", response.usage);
103
- console.error(VERBOSE_SEP);
104
- }
105
- if (response.toolCalls.length === 0) {
106
- const finalText = response.text.length > 0
107
- ? response.text
108
- : "The model returned no response or tool calls. If you asked for code changes or other work, try rephrasing your request or using a model with stronger tool-use support.";
109
- this.session.addMessage({
110
- role: "assistant",
111
- content: finalText,
112
- });
113
- const streamed = this.config.modelProvider === "openai-compatible" && !!this.onUiUpdate;
114
- return {
115
- text: finalText,
116
- usage: { inputTokens: totalInputTokens, outputTokens: totalOutputTokens },
117
- streamed,
118
- };
119
- }
120
- if (response.text.length > 0) {
121
- const truncated = response.text.length > PROGRESS_THINKING_MAX
122
- ? response.text.slice(0, PROGRESS_THINKING_MAX) + "..."
123
- : response.text;
124
- if (this.onProgress) {
125
- this.onProgress(`thinking: ${truncated}`);
126
- }
127
- if (this.onUiUpdate) {
128
- this.onUiUpdate({ type: "thinking", content: truncated });
129
- }
130
- }
131
- this.session.addMessage({
132
- role: "assistant",
133
- content: response.text,
134
- toolCalls: response.toolCalls,
135
- });
136
- for (const toolCall of response.toolCalls) {
137
- const fingerprint = signatureForToolCall(toolCall);
138
- recentToolCallFingerprints.push(fingerprint);
139
- if (recentToolCallFingerprints.length >
140
- this.config.loopDetectionWindow) {
141
- recentToolCallFingerprints.shift();
142
- }
143
- const repeatedCalls = recentToolCallFingerprints.filter((value) => value === fingerprint).length;
144
- if (repeatedCalls >= 3) {
145
- const loopMessage = "Stopped due to repeated identical tool calls. Please refine the prompt or provide additional constraints.";
146
- this.session.addMessage({
147
- role: "assistant",
148
- content: loopMessage,
149
- });
150
- return {
151
- text: loopMessage,
152
- usage: { inputTokens: totalInputTokens, outputTokens: totalOutputTokens },
153
- streamed: false,
154
- };
155
- }
156
- if (this.onProgress) {
157
- this.onProgress(`tool_call: ${formatToolCallForProgress(toolCall)}`);
158
- }
159
- if (this.onUiUpdate) {
160
- this.onUiUpdate({
161
- type: "tool_call_start",
162
- name: toolCall.name,
163
- input: toolCall.input,
164
- });
165
- }
166
- if (this.verbose) {
167
- console.error(`\n${VERBOSE_SEP}`);
168
- console.error(`[verbose] Tool: ${toolCall.name}`);
169
- console.error("Arguments:", JSON.stringify(toolCall.input, null, 2));
170
- }
171
- const toolStartMs = Date.now();
172
- let toolResult = await this.toolRegistry.execute(toolCall.name, toolCall.input);
173
- const maxChars = this.config.maxToolOutputChars;
174
- if (maxChars > 0 && toolResult.length > maxChars) {
175
- toolResult = `${toolResult.slice(0, maxChars)}\n\n[... truncated, ${toolResult.length - maxChars} more chars ...]`;
176
- }
177
- if (this.onUiUpdate) {
178
- this.onUiUpdate({
179
- type: "tool_call_end",
180
- name: toolCall.name,
181
- input: toolCall.input,
182
- result: toolResult,
183
- elapsedMs: Date.now() - toolStartMs,
184
- });
185
- }
186
- if (this.verbose) {
187
- console.error("Output:", toolResult);
188
- console.error(VERBOSE_SEP);
189
- }
190
- this.session.addMessage({
191
- role: "tool",
192
- toolCallId: toolCall.id,
193
- toolName: toolCall.name,
194
- content: toolResult,
195
- });
196
- }
197
- }
198
- const stepLimitMessage = "Reached the maximum number of steps for this turn. I stopped to avoid an infinite loop.";
199
- this.session.addMessage({
200
- role: "assistant",
201
- content: stepLimitMessage,
202
- });
203
- return {
204
- text: stepLimitMessage,
205
- usage: { inputTokens: totalInputTokens, outputTokens: totalOutputTokens },
206
- streamed: false,
207
- };
208
- }
209
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,374 +0,0 @@
1
- import process from "node:process";
2
- import { setTimeout as sleep } from "node:timers/promises";
3
- import Anthropic from "@anthropic-ai/sdk";
4
- function toAnthropicMessages(messages) {
5
- const converted = [];
6
- for (const message of messages) {
7
- if (message.role === "user") {
8
- converted.push({
9
- role: "user",
10
- content: message.content,
11
- });
12
- continue;
13
- }
14
- if (message.role === "assistant") {
15
- const content = [];
16
- if (message.content.trim().length > 0) {
17
- content.push({
18
- type: "text",
19
- text: message.content,
20
- });
21
- }
22
- for (const toolCall of message.toolCalls ?? []) {
23
- content.push({
24
- type: "tool_use",
25
- id: toolCall.id,
26
- name: toolCall.name,
27
- input: toolCall.input,
28
- });
29
- }
30
- converted.push({
31
- role: "assistant",
32
- content: content.length > 0 ? content : "",
33
- });
34
- continue;
35
- }
36
- converted.push({
37
- role: "user",
38
- content: [
39
- {
40
- type: "tool_result",
41
- tool_use_id: message.toolCallId,
42
- content: message.content,
43
- },
44
- ],
45
- });
46
- }
47
- return converted;
48
- }
49
- function parseResponse(response) {
50
- const textParts = [];
51
- const toolCalls = [];
52
- for (const block of response.content) {
53
- if (block.type === "text") {
54
- textParts.push(block.text);
55
- continue;
56
- }
57
- if (block.type === "tool_use") {
58
- toolCalls.push({
59
- id: block.id,
60
- name: block.name,
61
- input: block.input,
62
- });
63
- }
64
- }
65
- return {
66
- text: textParts.join("\n").trim(),
67
- toolCalls,
68
- stopReason: response.stop_reason === "tool_use"
69
- ? "tool_use"
70
- : response.stop_reason === "max_tokens"
71
- ? "max_tokens"
72
- : "end_turn",
73
- usage: {
74
- inputTokens: response.usage.input_tokens,
75
- outputTokens: response.usage.output_tokens,
76
- },
77
- };
78
- }
79
- function isAbortError(error) {
80
- return error instanceof Error && error.name === "AbortError";
81
- }
82
- async function withRetry(fn, attempts = 3) {
83
- let lastError;
84
- for (let attempt = 1; attempt <= attempts; attempt += 1) {
85
- try {
86
- return await fn();
87
- }
88
- catch (error) {
89
- lastError = error;
90
- if (isAbortError(error)) {
91
- throw error;
92
- }
93
- if (attempt === attempts) {
94
- break;
95
- }
96
- const delayMs = 500 * 2 ** (attempt - 1);
97
- await sleep(delayMs);
98
- }
99
- }
100
- throw lastError;
101
- }
102
- function toOpenAICompatibleMessages(system, messages) {
103
- const converted = [
104
- {
105
- role: "system",
106
- content: system,
107
- },
108
- ];
109
- for (const message of messages) {
110
- if (message.role === "user") {
111
- converted.push({
112
- role: "user",
113
- content: message.content,
114
- });
115
- continue;
116
- }
117
- if (message.role === "assistant") {
118
- const toolCalls = message.toolCalls?.map((toolCall) => ({
119
- id: toolCall.id,
120
- type: "function",
121
- function: {
122
- name: toolCall.name,
123
- arguments: JSON.stringify(toolCall.input),
124
- },
125
- }));
126
- converted.push({
127
- role: "assistant",
128
- content: message.content.length > 0 ? message.content : null,
129
- ...(toolCalls ? { tool_calls: toolCalls } : {}),
130
- });
131
- continue;
132
- }
133
- converted.push({
134
- role: "tool",
135
- tool_call_id: message.toolCallId,
136
- content: message.content,
137
- });
138
- }
139
- return converted;
140
- }
141
- function toOpenAICompatibleTools(tools) {
142
- return tools.map((tool) => ({
143
- type: "function",
144
- function: {
145
- name: tool.name,
146
- description: tool.description,
147
- parameters: tool.input_schema,
148
- },
149
- }));
150
- }
151
- function parseOpenAICompatibleToolArguments(rawArguments) {
152
- try {
153
- const parsed = JSON.parse(rawArguments);
154
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
155
- return parsed;
156
- }
157
- }
158
- catch {
159
- // Fall back to empty object when model emits malformed function arguments.
160
- }
161
- return {};
162
- }
163
- function parseOpenAICompatibleResponse(response) {
164
- const firstChoice = response.choices?.[0];
165
- const message = firstChoice?.message;
166
- if (!message) {
167
- throw new Error("OpenAI-compatible response missing choices[0].message.");
168
- }
169
- const toolCalls = message.tool_calls?.map((toolCall, index) => ({
170
- id: toolCall.id || `tool-call-${index + 1}`,
171
- name: toolCall.function.name,
172
- input: parseOpenAICompatibleToolArguments(toolCall.function.arguments),
173
- })) ?? [];
174
- const finishReason = firstChoice?.finish_reason ?? null;
175
- const stopReason = finishReason === "tool_calls" || finishReason === "function_call"
176
- ? "tool_use"
177
- : finishReason === "length"
178
- ? "max_tokens"
179
- : toolCalls.length > 0
180
- ? "tool_use"
181
- : "end_turn";
182
- return {
183
- text: (message.content ?? "").trim(),
184
- toolCalls,
185
- stopReason,
186
- usage: {
187
- inputTokens: response.usage?.prompt_tokens ?? 0,
188
- outputTokens: response.usage?.completion_tokens ?? 0,
189
- },
190
- };
191
- }
192
- function normalizeBaseUrl(baseUrl) {
193
- const trimmed = baseUrl.trim().replace(/\/+$/, "");
194
- if (trimmed.length === 0) {
195
- throw new Error("OPENAI_BASE_URL cannot be empty.");
196
- }
197
- return trimmed;
198
- }
199
- export class AnthropicModelClient {
200
- client;
201
- constructor(apiKey = process.env.ANTHROPIC_API_KEY) {
202
- if (!apiKey) {
203
- throw new Error("Missing ANTHROPIC_API_KEY. Copy .env.example to .env and set a key.");
204
- }
205
- this.client = new Anthropic({ apiKey });
206
- }
207
- async chat(params) {
208
- const response = await withRetry(() => this.client.messages.create({
209
- model: params.model,
210
- max_tokens: params.maxTokens,
211
- system: params.system,
212
- messages: toAnthropicMessages(params.messages),
213
- tools: params.tools,
214
- stream: false,
215
- }));
216
- return parseResponse(response);
217
- }
218
- }
219
- async function parseOpenAIStream(reader, onStream) {
220
- const decoder = new TextDecoder();
221
- let buffer = "";
222
- let content = "";
223
- const toolCallsAcc = [];
224
- const usage = { prompt_tokens: 0, completion_tokens: 0 };
225
- let finishReason = null;
226
- const processLines = (lines) => {
227
- for (const line of lines) {
228
- const trimmed = line.trim();
229
- if (!trimmed.startsWith("data: "))
230
- continue;
231
- const data = trimmed.slice(6).trim();
232
- if (data === "[DONE]")
233
- continue;
234
- try {
235
- const chunk = JSON.parse(data);
236
- const choice = chunk.choices?.[0];
237
- if (!choice)
238
- continue;
239
- const delta = choice.delta;
240
- if (delta?.content) {
241
- content += delta.content;
242
- onStream?.(delta.content);
243
- }
244
- if (delta?.tool_calls) {
245
- for (const tc of delta.tool_calls) {
246
- const idx = tc.index ?? 0;
247
- if (!toolCallsAcc[idx]) {
248
- toolCallsAcc[idx] = { id: tc.id ?? "", name: tc.function?.name ?? "", arguments: tc.function?.arguments ?? "" };
249
- }
250
- else {
251
- if (tc.id)
252
- toolCallsAcc[idx].id = tc.id;
253
- if (tc.function?.name)
254
- toolCallsAcc[idx].name = tc.function.name;
255
- if (tc.function?.arguments)
256
- toolCallsAcc[idx].arguments += tc.function.arguments;
257
- }
258
- }
259
- }
260
- if (choice.finish_reason)
261
- finishReason = choice.finish_reason;
262
- if (chunk.usage) {
263
- usage.prompt_tokens = chunk.usage.prompt_tokens ?? 0;
264
- usage.completion_tokens = chunk.usage.completion_tokens ?? 0;
265
- }
266
- }
267
- catch {
268
- // skip malformed chunks
269
- }
270
- }
271
- };
272
- while (true) {
273
- const { done, value } = await reader.read();
274
- if (value) {
275
- buffer += decoder.decode(value, { stream: true });
276
- }
277
- const lines = buffer.split(/\r?\n/);
278
- buffer = lines.pop() ?? "";
279
- processLines(lines);
280
- if (done) {
281
- if (buffer.trim())
282
- processLines([buffer]);
283
- break;
284
- }
285
- }
286
- const toolCalls = toolCallsAcc
287
- .filter((tc) => tc.id || tc.name)
288
- .map((tc, i) => ({
289
- id: tc.id || `tool-call-${i + 1}`,
290
- name: tc.name,
291
- input: parseOpenAICompatibleToolArguments(tc.arguments),
292
- }));
293
- const stopReason = finishReason === "tool_calls" || finishReason === "function_call"
294
- ? "tool_use"
295
- : finishReason === "length"
296
- ? "max_tokens"
297
- : toolCalls.length > 0
298
- ? "tool_use"
299
- : "end_turn";
300
- return {
301
- text: content.trim(),
302
- toolCalls,
303
- stopReason,
304
- usage: { inputTokens: usage.prompt_tokens, outputTokens: usage.completion_tokens },
305
- };
306
- }
307
- export class OpenAICompatibleModelClient {
308
- baseUrl;
309
- apiKey;
310
- fetchImpl;
311
- constructor(params) {
312
- this.baseUrl = normalizeBaseUrl(params?.baseUrl ?? process.env.OPENAI_BASE_URL ?? "http://localhost:1234/v1");
313
- const isOpenRouter = this.baseUrl.includes("openrouter");
314
- this.apiKey =
315
- params?.apiKey ??
316
- (isOpenRouter
317
- ? process.env.OPENROUTER_API_KEY ?? process.env.OPENAI_API_KEY
318
- : process.env.OPENAI_API_KEY);
319
- this.fetchImpl = params?.fetchImpl ?? fetch;
320
- }
321
- async chat(params) {
322
- const headers = {
323
- "Content-Type": "application/json",
324
- };
325
- const apiKey = this.apiKey?.trim();
326
- if (apiKey && apiKey.length > 0) {
327
- if (this.baseUrl.includes("openrouter") &&
328
- apiKey.startsWith("sk-proj-")) {
329
- throw new Error("OpenRouter requires an OpenRouter API key (sk-or-v1-...), not an OpenAI key (sk-proj-...). Get one at https://openrouter.ai/keys");
330
- }
331
- headers.Authorization = `Bearer ${apiKey}`;
332
- }
333
- else if (this.baseUrl.includes("openrouter")) {
334
- throw new Error("Missing OpenRouter API key. Set OPENAI_API_KEY or OPENROUTER_API_KEY in .env. Get one at https://openrouter.ai/keys");
335
- }
336
- const useStream = params.onStream !== undefined;
337
- const response = await withRetry(async () => {
338
- const httpResponse = await this.fetchImpl(`${this.baseUrl}/chat/completions`, {
339
- method: "POST",
340
- headers,
341
- body: JSON.stringify({
342
- model: params.model,
343
- messages: toOpenAICompatibleMessages(params.system, params.messages),
344
- tools: toOpenAICompatibleTools(params.tools),
345
- tool_choice: "auto",
346
- max_tokens: params.maxTokens,
347
- stream: useStream,
348
- }),
349
- ...(params.signal && { signal: params.signal }),
350
- });
351
- if (!httpResponse.ok) {
352
- const bodyText = await httpResponse.text();
353
- throw new Error(`OpenAI-compatible request failed (${httpResponse.status}): ${bodyText}`);
354
- }
355
- if (useStream && httpResponse.body) {
356
- return parseOpenAIStream(httpResponse.body.getReader(), params.onStream);
357
- }
358
- const payload = (await httpResponse.json());
359
- return parseOpenAICompatibleResponse(payload);
360
- });
361
- return response;
362
- }
363
- }
364
- export function createModelClient(config) {
365
- if (config.modelProvider === "openai-compatible") {
366
- return new OpenAICompatibleModelClient({
367
- baseUrl: config.openAiBaseUrl,
368
- ...(config.openAiApiKey !== undefined
369
- ? { apiKey: config.openAiApiKey }
370
- : {}),
371
- });
372
- }
373
- return new AnthropicModelClient();
374
- }