multiarena 0.1.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/dist/config/loader.d.ts +6 -0
- package/dist/config/loader.js +69 -0
- package/dist/config/types.d.ts +15 -0
- package/dist/config/types.js +6 -0
- package/dist/core/session.d.ts +40 -0
- package/dist/core/session.js +155 -0
- package/dist/core/turn.d.ts +31 -0
- package/dist/core/turn.js +112 -0
- package/dist/core/types.d.ts +25 -0
- package/dist/core/types.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +76 -0
- package/dist/isolation/worktree.d.ts +11 -0
- package/dist/isolation/worktree.js +117 -0
- package/dist/persistence/session.d.ts +17 -0
- package/dist/persistence/session.js +27 -0
- package/dist/provider/adapters/anthropic.d.ts +11 -0
- package/dist/provider/adapters/anthropic.js +146 -0
- package/dist/provider/adapters/google.d.ts +11 -0
- package/dist/provider/adapters/google.js +177 -0
- package/dist/provider/adapters/ollama.d.ts +11 -0
- package/dist/provider/adapters/ollama.js +147 -0
- package/dist/provider/adapters/openai.d.ts +11 -0
- package/dist/provider/adapters/openai.js +167 -0
- package/dist/provider/provider.d.ts +7 -0
- package/dist/provider/provider.js +21 -0
- package/dist/provider/types.d.ts +41 -0
- package/dist/provider/types.js +1 -0
- package/dist/tools/builtin/bash.d.ts +2 -0
- package/dist/tools/builtin/bash.js +34 -0
- package/dist/tools/builtin/editFile.d.ts +2 -0
- package/dist/tools/builtin/editFile.js +40 -0
- package/dist/tools/builtin/glob.d.ts +2 -0
- package/dist/tools/builtin/glob.js +77 -0
- package/dist/tools/builtin/grep.d.ts +2 -0
- package/dist/tools/builtin/grep.js +120 -0
- package/dist/tools/builtin/readFile.d.ts +2 -0
- package/dist/tools/builtin/readFile.js +27 -0
- package/dist/tools/builtin/writeFile.d.ts +2 -0
- package/dist/tools/builtin/writeFile.js +29 -0
- package/dist/tools/permission.d.ts +7 -0
- package/dist/tools/permission.js +31 -0
- package/dist/tools/registry.d.ts +9 -0
- package/dist/tools/registry.js +37 -0
- package/dist/tools/types.d.ts +11 -0
- package/dist/tools/types.js +1 -0
- package/dist/ui/app.d.ts +4 -0
- package/dist/ui/app.js +343 -0
- package/dist/ui/components/BroadcastSummary.d.ts +7 -0
- package/dist/ui/components/BroadcastSummary.js +18 -0
- package/dist/ui/components/InputBar.d.ts +9 -0
- package/dist/ui/components/InputBar.js +11 -0
- package/dist/ui/components/ModelDetail.d.ts +8 -0
- package/dist/ui/components/ModelDetail.js +13 -0
- package/dist/ui/components/OutputArea.d.ts +15 -0
- package/dist/ui/components/OutputArea.js +29 -0
- package/dist/ui/components/StatusBar.d.ts +9 -0
- package/dist/ui/components/StatusBar.js +51 -0
- package/package.json +60 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface SavedSession {
|
|
2
|
+
id: string;
|
|
3
|
+
timestamp: string;
|
|
4
|
+
models: Array<{
|
|
5
|
+
name: string;
|
|
6
|
+
messages: Array<{
|
|
7
|
+
role: string;
|
|
8
|
+
content: string;
|
|
9
|
+
tool_call_id?: string;
|
|
10
|
+
}>;
|
|
11
|
+
buffer: string;
|
|
12
|
+
}>;
|
|
13
|
+
lastTarget: "broadcast" | string;
|
|
14
|
+
}
|
|
15
|
+
export declare function saveSession(session: SavedSession): void;
|
|
16
|
+
export declare function loadSession(id: string): SavedSession | null;
|
|
17
|
+
export declare function listSessions(): SavedSession[];
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
const SESSIONS_DIR = path.join(os.homedir(), ".arena", "sessions");
|
|
5
|
+
export function saveSession(session) {
|
|
6
|
+
fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
7
|
+
const filePath = path.join(SESSIONS_DIR, `${session.id}.json`);
|
|
8
|
+
fs.writeFileSync(filePath, JSON.stringify(session, null, 2));
|
|
9
|
+
}
|
|
10
|
+
export function loadSession(id) {
|
|
11
|
+
const filePath = path.join(SESSIONS_DIR, `${id}.json`);
|
|
12
|
+
if (!fs.existsSync(filePath))
|
|
13
|
+
return null;
|
|
14
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
15
|
+
}
|
|
16
|
+
export function listSessions() {
|
|
17
|
+
if (!fs.existsSync(SESSIONS_DIR))
|
|
18
|
+
return [];
|
|
19
|
+
return fs
|
|
20
|
+
.readdirSync(SESSIONS_DIR)
|
|
21
|
+
.filter((f) => f.endsWith(".json"))
|
|
22
|
+
.map((f) => {
|
|
23
|
+
const data = JSON.parse(fs.readFileSync(path.join(SESSIONS_DIR, f), "utf-8"));
|
|
24
|
+
return data;
|
|
25
|
+
})
|
|
26
|
+
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
27
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Provider } from "../provider.js";
|
|
2
|
+
import { ChatRequest, StreamEvent } from "../types.js";
|
|
3
|
+
export declare class AnthropicProvider implements Provider {
|
|
4
|
+
private client;
|
|
5
|
+
private activeController;
|
|
6
|
+
constructor(apiKey: string);
|
|
7
|
+
chat(request: ChatRequest): AsyncGenerator<StreamEvent>;
|
|
8
|
+
abort(): void;
|
|
9
|
+
private convertMessages;
|
|
10
|
+
private convertTools;
|
|
11
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
2
|
+
const DEFAULT_MODEL = "claude-sonnet-4-20250514";
|
|
3
|
+
const DEFAULT_MAX_TOKENS = 4096;
|
|
4
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
5
|
+
export class AnthropicProvider {
|
|
6
|
+
client;
|
|
7
|
+
activeController = null;
|
|
8
|
+
constructor(apiKey) {
|
|
9
|
+
this.client = new Anthropic({ apiKey, timeout: DEFAULT_TIMEOUT_MS, maxRetries: 2 });
|
|
10
|
+
}
|
|
11
|
+
async *chat(request) {
|
|
12
|
+
const abortController = new AbortController();
|
|
13
|
+
this.activeController = abortController;
|
|
14
|
+
let inputTokens = 0;
|
|
15
|
+
let outputTokens = 0;
|
|
16
|
+
try {
|
|
17
|
+
const stream = this.client.messages.stream({
|
|
18
|
+
model: request.model || DEFAULT_MODEL,
|
|
19
|
+
max_tokens: DEFAULT_MAX_TOKENS,
|
|
20
|
+
system: request.system,
|
|
21
|
+
messages: this.convertMessages(request.messages),
|
|
22
|
+
tools: this.convertTools(request.tools),
|
|
23
|
+
}, { signal: abortController.signal });
|
|
24
|
+
for await (const event of stream) {
|
|
25
|
+
switch (event.type) {
|
|
26
|
+
case "message_start":
|
|
27
|
+
inputTokens = event.message.usage.input_tokens;
|
|
28
|
+
break;
|
|
29
|
+
case "content_block_delta":
|
|
30
|
+
if (event.delta.type === "text_delta") {
|
|
31
|
+
yield { type: "text", content: event.delta.text };
|
|
32
|
+
}
|
|
33
|
+
break;
|
|
34
|
+
case "content_block_stop": {
|
|
35
|
+
// The type definition for RawContentBlockStopEvent is incomplete in SDK v0.30.0;
|
|
36
|
+
// the actual API response includes a `content_block` field.
|
|
37
|
+
const block = event.content_block;
|
|
38
|
+
if (block?.type === "tool_use") {
|
|
39
|
+
yield {
|
|
40
|
+
type: "tool_call",
|
|
41
|
+
id: block.id,
|
|
42
|
+
name: block.name,
|
|
43
|
+
args: JSON.stringify(block.input),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
case "message_delta":
|
|
49
|
+
outputTokens = event.usage.output_tokens;
|
|
50
|
+
break;
|
|
51
|
+
case "message_stop":
|
|
52
|
+
yield {
|
|
53
|
+
type: "done",
|
|
54
|
+
usage: { input: inputTokens, output: outputTokens },
|
|
55
|
+
};
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (error instanceof Error &&
|
|
62
|
+
(error.name === "AbortError" || error.name === "APIUserAbortError")) {
|
|
63
|
+
return; // Aborted silently
|
|
64
|
+
}
|
|
65
|
+
yield {
|
|
66
|
+
type: "error",
|
|
67
|
+
message: error instanceof Error ? error.message : String(error),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
this.activeController = null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
abort() {
|
|
75
|
+
this.activeController?.abort();
|
|
76
|
+
}
|
|
77
|
+
// ── Message conversion ──────────────────────────────────────────────
|
|
78
|
+
convertMessages(messages) {
|
|
79
|
+
const result = [];
|
|
80
|
+
for (const msg of messages) {
|
|
81
|
+
switch (msg.role) {
|
|
82
|
+
case "user":
|
|
83
|
+
result.push({ role: "user", content: msg.content });
|
|
84
|
+
break;
|
|
85
|
+
case "assistant": {
|
|
86
|
+
const content = [];
|
|
87
|
+
if (msg.content) {
|
|
88
|
+
content.push({ type: "text", text: msg.content });
|
|
89
|
+
}
|
|
90
|
+
if (msg.tool_calls) {
|
|
91
|
+
for (const tc of msg.tool_calls) {
|
|
92
|
+
content.push({
|
|
93
|
+
type: "tool_use",
|
|
94
|
+
id: tc.id,
|
|
95
|
+
name: tc.name,
|
|
96
|
+
input: tc.arguments ? safeJsonParse(tc.arguments) : {},
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// If no tool_calls and content is plain text, use the string form directly
|
|
101
|
+
if (content.length === 1 && content[0].type === "text") {
|
|
102
|
+
result.push({ role: "assistant", content: msg.content });
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
result.push({ role: "assistant", content });
|
|
106
|
+
}
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
case "tool":
|
|
110
|
+
result.push({
|
|
111
|
+
role: "user",
|
|
112
|
+
content: [
|
|
113
|
+
{
|
|
114
|
+
type: "tool_result",
|
|
115
|
+
tool_use_id: msg.tool_call_id || "",
|
|
116
|
+
content: msg.content,
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
});
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
// ── Tool conversion ─────────────────────────────────────────────────
|
|
126
|
+
convertTools(tools) {
|
|
127
|
+
if (!tools || tools.length === 0)
|
|
128
|
+
return undefined;
|
|
129
|
+
return tools.map((t) => ({
|
|
130
|
+
name: t.name,
|
|
131
|
+
description: t.description,
|
|
132
|
+
input_schema: {
|
|
133
|
+
...t.parameters,
|
|
134
|
+
type: "object",
|
|
135
|
+
},
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function safeJsonParse(raw) {
|
|
140
|
+
try {
|
|
141
|
+
return JSON.parse(raw);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return {};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Provider } from "../provider.js";
|
|
2
|
+
import { ChatRequest, StreamEvent } from "../types.js";
|
|
3
|
+
export declare class GoogleProvider implements Provider {
|
|
4
|
+
private genAI;
|
|
5
|
+
private aborted;
|
|
6
|
+
constructor(apiKey: string);
|
|
7
|
+
chat(request: ChatRequest): AsyncGenerator<StreamEvent>;
|
|
8
|
+
abort(): void;
|
|
9
|
+
private convertMessages;
|
|
10
|
+
private convertTools;
|
|
11
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { GoogleGenerativeAI } from "@google/generative-ai";
|
|
2
|
+
const DEFAULT_MODEL = "gemini-2.5-flash";
|
|
3
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
4
|
+
export class GoogleProvider {
|
|
5
|
+
genAI;
|
|
6
|
+
aborted = false;
|
|
7
|
+
constructor(apiKey) {
|
|
8
|
+
this.genAI = new GoogleGenerativeAI(apiKey);
|
|
9
|
+
}
|
|
10
|
+
async *chat(request) {
|
|
11
|
+
this.aborted = false;
|
|
12
|
+
const maxAttempts = 2;
|
|
13
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
14
|
+
try {
|
|
15
|
+
const model = this.genAI.getGenerativeModel({
|
|
16
|
+
model: request.model || DEFAULT_MODEL,
|
|
17
|
+
systemInstruction: request.system,
|
|
18
|
+
tools: this.convertTools(request.tools),
|
|
19
|
+
});
|
|
20
|
+
const contents = this.convertMessages(request.messages);
|
|
21
|
+
const result = await model.generateContentStream({ contents }, { timeout: DEFAULT_TIMEOUT_MS });
|
|
22
|
+
let textOffset = 0;
|
|
23
|
+
let fnCallCount = 0;
|
|
24
|
+
for await (const chunk of result.stream) {
|
|
25
|
+
if (this.aborted)
|
|
26
|
+
return;
|
|
27
|
+
// Yield new text
|
|
28
|
+
const fullText = chunk.text();
|
|
29
|
+
if (fullText && fullText.length > textOffset) {
|
|
30
|
+
const delta = fullText.slice(textOffset);
|
|
31
|
+
textOffset = fullText.length;
|
|
32
|
+
if (delta) {
|
|
33
|
+
yield { type: "text", content: delta };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Yield new function calls
|
|
37
|
+
const fnCalls = chunk.functionCalls?.();
|
|
38
|
+
if (fnCalls) {
|
|
39
|
+
for (let i = fnCallCount; i < fnCalls.length; i++) {
|
|
40
|
+
const fc = fnCalls[i];
|
|
41
|
+
yield {
|
|
42
|
+
type: "tool_call",
|
|
43
|
+
id: `${fc.name}_${i}`,
|
|
44
|
+
name: fc.name,
|
|
45
|
+
args: JSON.stringify(fc.args),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
fnCallCount = fnCalls.length;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Try to get usage metadata from the completed response
|
|
52
|
+
let usage = { input: 0, output: 0 };
|
|
53
|
+
try {
|
|
54
|
+
const response = await result.response;
|
|
55
|
+
const metadata = response.usageMetadata;
|
|
56
|
+
if (metadata) {
|
|
57
|
+
usage = {
|
|
58
|
+
input: metadata.promptTokenCount ?? 0,
|
|
59
|
+
output: metadata.candidatesTokenCount ?? 0,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// usageMetadata may not be available in all SDK versions
|
|
65
|
+
}
|
|
66
|
+
yield { type: "done", usage };
|
|
67
|
+
return; // success — exit retry loop
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (this.aborted)
|
|
71
|
+
return;
|
|
72
|
+
if (attempt < maxAttempts - 1 && isRetryableError(error)) {
|
|
73
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
yield {
|
|
77
|
+
type: "error",
|
|
78
|
+
message: error instanceof Error ? error.message : String(error),
|
|
79
|
+
};
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
abort() {
|
|
85
|
+
this.aborted = true;
|
|
86
|
+
}
|
|
87
|
+
// ── Message conversion ──────────────────────────────────────────────
|
|
88
|
+
convertMessages(messages) {
|
|
89
|
+
const result = [];
|
|
90
|
+
for (const msg of messages) {
|
|
91
|
+
switch (msg.role) {
|
|
92
|
+
case "user":
|
|
93
|
+
result.push({
|
|
94
|
+
role: "user",
|
|
95
|
+
parts: [{ text: msg.content }],
|
|
96
|
+
});
|
|
97
|
+
break;
|
|
98
|
+
case "assistant": {
|
|
99
|
+
const parts = [];
|
|
100
|
+
if (msg.content) {
|
|
101
|
+
parts.push({ text: msg.content });
|
|
102
|
+
}
|
|
103
|
+
if (msg.tool_calls) {
|
|
104
|
+
for (const tc of msg.tool_calls) {
|
|
105
|
+
parts.push({
|
|
106
|
+
functionCall: {
|
|
107
|
+
name: tc.name,
|
|
108
|
+
args: safeJsonParse(tc.arguments),
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
result.push({ role: "model", parts });
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
case "tool": {
|
|
117
|
+
const toolCall = findToolCall(messages, msg.tool_call_id);
|
|
118
|
+
result.push({
|
|
119
|
+
role: "function",
|
|
120
|
+
parts: [
|
|
121
|
+
{
|
|
122
|
+
functionResponse: {
|
|
123
|
+
name: toolCall?.name ?? "",
|
|
124
|
+
response: { content: msg.content },
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
});
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
// ── Tool conversion ─────────────────────────────────────────────────
|
|
136
|
+
convertTools(tools) {
|
|
137
|
+
if (!tools || tools.length === 0)
|
|
138
|
+
return undefined;
|
|
139
|
+
return [
|
|
140
|
+
{
|
|
141
|
+
functionDeclarations: tools.map((t) => ({
|
|
142
|
+
name: t.name,
|
|
143
|
+
description: t.description,
|
|
144
|
+
parameters: t.parameters,
|
|
145
|
+
})),
|
|
146
|
+
},
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function safeJsonParse(raw) {
|
|
151
|
+
try {
|
|
152
|
+
return JSON.parse(raw);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return {};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function findToolCall(messages, toolCallId) {
|
|
159
|
+
if (!toolCallId)
|
|
160
|
+
return undefined;
|
|
161
|
+
for (const msg of messages) {
|
|
162
|
+
if (msg.tool_calls) {
|
|
163
|
+
const found = msg.tool_calls.find((tc) => tc.id === toolCallId);
|
|
164
|
+
if (found)
|
|
165
|
+
return found;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
function isRetryableError(error) {
|
|
171
|
+
if (!(error instanceof Error))
|
|
172
|
+
return false;
|
|
173
|
+
if (error.name === "TypeError" && error.message.includes("fetch"))
|
|
174
|
+
return true;
|
|
175
|
+
const msg = error.message;
|
|
176
|
+
return /(429|500|502|503|504)/.test(msg);
|
|
177
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Provider } from "../provider.js";
|
|
2
|
+
import { ChatRequest, StreamEvent } from "../types.js";
|
|
3
|
+
export declare class OllamaProvider implements Provider {
|
|
4
|
+
private baseURL;
|
|
5
|
+
private activeController;
|
|
6
|
+
constructor(baseURL: string);
|
|
7
|
+
chat(request: ChatRequest): AsyncGenerator<StreamEvent>;
|
|
8
|
+
abort(): void;
|
|
9
|
+
private convertMessages;
|
|
10
|
+
private convertTools;
|
|
11
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
const DEFAULT_MODEL = "llama3";
|
|
2
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
3
|
+
export class OllamaProvider {
|
|
4
|
+
baseURL;
|
|
5
|
+
activeController = null;
|
|
6
|
+
constructor(baseURL) {
|
|
7
|
+
this.baseURL = baseURL;
|
|
8
|
+
}
|
|
9
|
+
async *chat(request) {
|
|
10
|
+
const controller = new AbortController();
|
|
11
|
+
this.activeController = controller;
|
|
12
|
+
const timeoutId = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
|
|
13
|
+
try {
|
|
14
|
+
const body = {
|
|
15
|
+
model: request.model || DEFAULT_MODEL,
|
|
16
|
+
messages: this.convertMessages(request.messages),
|
|
17
|
+
stream: true,
|
|
18
|
+
};
|
|
19
|
+
if (request.tools && request.tools.length > 0) {
|
|
20
|
+
body.tools = this.convertTools(request.tools);
|
|
21
|
+
}
|
|
22
|
+
const response = await fetch(`${this.baseURL}/api/chat`, {
|
|
23
|
+
method: "POST",
|
|
24
|
+
headers: { "Content-Type": "application/json" },
|
|
25
|
+
body: JSON.stringify(body),
|
|
26
|
+
signal: controller.signal,
|
|
27
|
+
});
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
const text = await response.text();
|
|
30
|
+
yield { type: "error", message: `Ollama error (${response.status}): ${text}` };
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const reader = response.body?.getReader();
|
|
34
|
+
if (!reader) {
|
|
35
|
+
yield { type: "error", message: "No response body from Ollama" };
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const decoder = new TextDecoder();
|
|
39
|
+
let lineBuffer = "";
|
|
40
|
+
let inputTokens = 0;
|
|
41
|
+
let outputTokens = 0;
|
|
42
|
+
while (true) {
|
|
43
|
+
const { done, value } = await reader.read();
|
|
44
|
+
if (done)
|
|
45
|
+
break;
|
|
46
|
+
lineBuffer += decoder.decode(value, { stream: true });
|
|
47
|
+
const lines = lineBuffer.split("\n");
|
|
48
|
+
lineBuffer = lines.pop() ?? "";
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
if (!line.trim())
|
|
51
|
+
continue;
|
|
52
|
+
let chunk;
|
|
53
|
+
try {
|
|
54
|
+
chunk = JSON.parse(line);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (chunk.message) {
|
|
60
|
+
const msg = chunk.message;
|
|
61
|
+
if (msg.content) {
|
|
62
|
+
yield { type: "text", content: msg.content };
|
|
63
|
+
}
|
|
64
|
+
if (msg.tool_calls) {
|
|
65
|
+
for (const tc of msg.tool_calls) {
|
|
66
|
+
const id = `call_${tc.function.name}_${Math.random().toString(36).slice(2, 8)}`;
|
|
67
|
+
yield {
|
|
68
|
+
type: "tool_call",
|
|
69
|
+
id,
|
|
70
|
+
name: tc.function.name,
|
|
71
|
+
args: JSON.stringify(tc.function.arguments),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (chunk.done) {
|
|
77
|
+
inputTokens = chunk.prompt_eval_count ?? 0;
|
|
78
|
+
outputTokens = chunk.eval_count ?? 0;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
yield { type: "done", usage: { input: inputTokens, output: outputTokens } };
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
yield {
|
|
89
|
+
type: "error",
|
|
90
|
+
message: error instanceof Error ? error.message : String(error),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
clearTimeout(timeoutId);
|
|
95
|
+
this.activeController = null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
abort() {
|
|
99
|
+
this.activeController?.abort();
|
|
100
|
+
}
|
|
101
|
+
convertMessages(messages) {
|
|
102
|
+
return messages.map((msg) => {
|
|
103
|
+
switch (msg.role) {
|
|
104
|
+
case "user":
|
|
105
|
+
return { role: "user", content: msg.content };
|
|
106
|
+
case "assistant": {
|
|
107
|
+
const result = { role: "assistant", content: msg.content || "" };
|
|
108
|
+
if (msg.tool_calls && msg.tool_calls.length > 0) {
|
|
109
|
+
result.tool_calls = msg.tool_calls.map((tc) => ({
|
|
110
|
+
function: {
|
|
111
|
+
name: tc.name,
|
|
112
|
+
arguments: safeJsonParse(tc.arguments),
|
|
113
|
+
},
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
case "tool":
|
|
119
|
+
return {
|
|
120
|
+
role: "tool",
|
|
121
|
+
content: msg.content,
|
|
122
|
+
tool_call_id: msg.tool_call_id || "",
|
|
123
|
+
};
|
|
124
|
+
default:
|
|
125
|
+
return { role: "user", content: msg.content };
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
convertTools(tools) {
|
|
130
|
+
return tools.map((t) => ({
|
|
131
|
+
type: "function",
|
|
132
|
+
function: {
|
|
133
|
+
name: t.name,
|
|
134
|
+
description: t.description,
|
|
135
|
+
parameters: t.parameters,
|
|
136
|
+
},
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function safeJsonParse(raw) {
|
|
141
|
+
try {
|
|
142
|
+
return JSON.parse(raw);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return {};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Provider } from "../provider.js";
|
|
2
|
+
import { ChatRequest, StreamEvent } from "../types.js";
|
|
3
|
+
export declare class OpenAIProvider implements Provider {
|
|
4
|
+
private client;
|
|
5
|
+
private activeController;
|
|
6
|
+
constructor(apiKey: string, baseURL?: string);
|
|
7
|
+
chat(request: ChatRequest): AsyncGenerator<StreamEvent>;
|
|
8
|
+
abort(): void;
|
|
9
|
+
private convertMessages;
|
|
10
|
+
private convertTools;
|
|
11
|
+
}
|