ashlrcode 1.0.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/LICENSE +21 -0
- package/README.md +295 -0
- package/package.json +46 -0
- package/src/__tests__/branded-types.test.ts +47 -0
- package/src/__tests__/context.test.ts +163 -0
- package/src/__tests__/cost-tracker.test.ts +274 -0
- package/src/__tests__/cron.test.ts +197 -0
- package/src/__tests__/dream.test.ts +204 -0
- package/src/__tests__/error-handler.test.ts +192 -0
- package/src/__tests__/features.test.ts +69 -0
- package/src/__tests__/file-history.test.ts +177 -0
- package/src/__tests__/hooks.test.ts +145 -0
- package/src/__tests__/keybindings.test.ts +159 -0
- package/src/__tests__/model-patches.test.ts +82 -0
- package/src/__tests__/permissions-rules.test.ts +121 -0
- package/src/__tests__/permissions.test.ts +108 -0
- package/src/__tests__/project-config.test.ts +63 -0
- package/src/__tests__/retry.test.ts +321 -0
- package/src/__tests__/router.test.ts +158 -0
- package/src/__tests__/session-compact.test.ts +191 -0
- package/src/__tests__/session.test.ts +145 -0
- package/src/__tests__/skill-registry.test.ts +130 -0
- package/src/__tests__/speculation.test.ts +196 -0
- package/src/__tests__/tasks-v2.test.ts +267 -0
- package/src/__tests__/telemetry.test.ts +149 -0
- package/src/__tests__/tool-executor.test.ts +141 -0
- package/src/__tests__/tool-registry.test.ts +166 -0
- package/src/__tests__/undercover.test.ts +93 -0
- package/src/__tests__/workflow.test.ts +195 -0
- package/src/agent/async-context.ts +64 -0
- package/src/agent/context.ts +245 -0
- package/src/agent/cron.ts +189 -0
- package/src/agent/dream.ts +165 -0
- package/src/agent/error-handler.ts +108 -0
- package/src/agent/ipc.ts +256 -0
- package/src/agent/kairos.ts +207 -0
- package/src/agent/loop.ts +314 -0
- package/src/agent/model-patches.ts +68 -0
- package/src/agent/speculation.ts +219 -0
- package/src/agent/sub-agent.ts +125 -0
- package/src/agent/system-prompt.ts +231 -0
- package/src/agent/team.ts +220 -0
- package/src/agent/tool-executor.ts +162 -0
- package/src/agent/workflow.ts +189 -0
- package/src/agent/worktree-manager.ts +86 -0
- package/src/autopilot/queue.ts +186 -0
- package/src/autopilot/scanner.ts +245 -0
- package/src/autopilot/types.ts +58 -0
- package/src/bridge/bridge-client.ts +57 -0
- package/src/bridge/bridge-server.ts +81 -0
- package/src/cli.ts +1120 -0
- package/src/config/features.ts +51 -0
- package/src/config/git.ts +137 -0
- package/src/config/hooks.ts +201 -0
- package/src/config/permissions.ts +251 -0
- package/src/config/project-config.ts +63 -0
- package/src/config/remote-settings.ts +163 -0
- package/src/config/settings-sync.ts +170 -0
- package/src/config/settings.ts +113 -0
- package/src/config/undercover.ts +76 -0
- package/src/config/upgrade-notice.ts +65 -0
- package/src/mcp/client.ts +197 -0
- package/src/mcp/manager.ts +125 -0
- package/src/mcp/oauth.ts +252 -0
- package/src/mcp/types.ts +61 -0
- package/src/persistence/memory.ts +129 -0
- package/src/persistence/session.ts +289 -0
- package/src/planning/plan-mode.ts +128 -0
- package/src/planning/plan-tools.ts +138 -0
- package/src/providers/anthropic.ts +177 -0
- package/src/providers/cost-tracker.ts +184 -0
- package/src/providers/retry.ts +264 -0
- package/src/providers/router.ts +159 -0
- package/src/providers/types.ts +79 -0
- package/src/providers/xai.ts +217 -0
- package/src/repl.tsx +1384 -0
- package/src/setup.ts +119 -0
- package/src/skills/loader.ts +78 -0
- package/src/skills/registry.ts +78 -0
- package/src/skills/types.ts +11 -0
- package/src/state/file-history.ts +264 -0
- package/src/telemetry/event-log.ts +116 -0
- package/src/tools/agent.ts +133 -0
- package/src/tools/ask-user.ts +229 -0
- package/src/tools/bash.ts +146 -0
- package/src/tools/config.ts +147 -0
- package/src/tools/diff.ts +137 -0
- package/src/tools/file-edit.ts +123 -0
- package/src/tools/file-read.ts +82 -0
- package/src/tools/file-write.ts +82 -0
- package/src/tools/glob.ts +76 -0
- package/src/tools/grep.ts +187 -0
- package/src/tools/ls.ts +77 -0
- package/src/tools/lsp.ts +375 -0
- package/src/tools/mcp-resources.ts +83 -0
- package/src/tools/mcp-tool.ts +47 -0
- package/src/tools/memory.ts +148 -0
- package/src/tools/notebook-edit.ts +133 -0
- package/src/tools/peers.ts +113 -0
- package/src/tools/powershell.ts +83 -0
- package/src/tools/registry.ts +114 -0
- package/src/tools/send-message.ts +75 -0
- package/src/tools/sleep.ts +50 -0
- package/src/tools/snip.ts +143 -0
- package/src/tools/tasks.ts +349 -0
- package/src/tools/team.ts +309 -0
- package/src/tools/todo-write.ts +93 -0
- package/src/tools/tool-search.ts +83 -0
- package/src/tools/types.ts +52 -0
- package/src/tools/web-browser.ts +263 -0
- package/src/tools/web-fetch.ts +118 -0
- package/src/tools/web-search.ts +107 -0
- package/src/tools/workflow.ts +188 -0
- package/src/tools/worktree.ts +143 -0
- package/src/types/branded.ts +22 -0
- package/src/ui/App.tsx +184 -0
- package/src/ui/BuddyPanel.tsx +52 -0
- package/src/ui/PermissionPrompt.tsx +29 -0
- package/src/ui/banner.ts +217 -0
- package/src/ui/buddy-ai.ts +108 -0
- package/src/ui/buddy.ts +466 -0
- package/src/ui/context-bar.ts +60 -0
- package/src/ui/effort.ts +65 -0
- package/src/ui/keybindings.ts +143 -0
- package/src/ui/markdown.ts +271 -0
- package/src/ui/message-renderer.ts +73 -0
- package/src/ui/mode.ts +80 -0
- package/src/ui/notifications.ts +57 -0
- package/src/ui/speech-bubble.ts +95 -0
- package/src/ui/spinner.ts +116 -0
- package/src/ui/theme.ts +98 -0
- package/src/version.ts +5 -0
- package/src/voice/voice-mode.ts +169 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider router — selects provider, handles failover, tracks costs.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createOpenAICompatibleProvider, createXAIProvider } from "./xai.ts";
|
|
6
|
+
import { createAnthropicProvider } from "./anthropic.ts";
|
|
7
|
+
import { CostTracker } from "./cost-tracker.ts";
|
|
8
|
+
import { CircuitBreaker } from "./retry.ts";
|
|
9
|
+
import type {
|
|
10
|
+
Provider,
|
|
11
|
+
ProviderConfig,
|
|
12
|
+
ProviderRequest,
|
|
13
|
+
ProviderRouterConfig,
|
|
14
|
+
StreamEvent,
|
|
15
|
+
TokenUsage,
|
|
16
|
+
} from "./types.ts";
|
|
17
|
+
|
|
18
|
+
export { CostTracker };
|
|
19
|
+
|
|
20
|
+
export class ProviderRouter {
|
|
21
|
+
private providers: Provider[] = [];
|
|
22
|
+
private currentIndex = 0;
|
|
23
|
+
private circuitBreakers = new Map<string, CircuitBreaker>();
|
|
24
|
+
costTracker = new CostTracker();
|
|
25
|
+
|
|
26
|
+
/** @deprecated Use costTracker instead */
|
|
27
|
+
get costs() {
|
|
28
|
+
const tracker = this.costTracker;
|
|
29
|
+
return {
|
|
30
|
+
get totalInputTokens() { return tracker.totalInputTokens; },
|
|
31
|
+
get totalOutputTokens() { return tracker.totalOutputTokens; },
|
|
32
|
+
get totalReasoningTokens() { return tracker.totalReasoningTokens; },
|
|
33
|
+
get totalCostUSD() { return tracker.totalCostUSD; },
|
|
34
|
+
perProvider: new Map<string, { inputTokens: number; outputTokens: number; reasoningTokens: number; costUSD: number }>(
|
|
35
|
+
tracker.getBreakdown().map(e => [
|
|
36
|
+
`${e.provider}`,
|
|
37
|
+
{ inputTokens: e.usage.inputTokens, outputTokens: e.usage.outputTokens, reasoningTokens: e.usage.reasoningTokens, costUSD: e.costUSD },
|
|
38
|
+
])
|
|
39
|
+
),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
constructor(config: ProviderRouterConfig) {
|
|
44
|
+
this.providers.push(this.createProvider(config.primary));
|
|
45
|
+
for (const fallback of config.fallbacks ?? []) {
|
|
46
|
+
this.providers.push(this.createProvider(fallback));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private createProvider(
|
|
51
|
+
config: ProviderConfig & { provider: string }
|
|
52
|
+
): Provider {
|
|
53
|
+
switch (config.provider) {
|
|
54
|
+
case "xai":
|
|
55
|
+
return createXAIProvider(config);
|
|
56
|
+
case "anthropic":
|
|
57
|
+
return createAnthropicProvider(config);
|
|
58
|
+
case "openai":
|
|
59
|
+
return createOpenAICompatibleProvider("openai", {
|
|
60
|
+
...config,
|
|
61
|
+
baseURL: config.baseURL ?? "https://api.openai.com/v1",
|
|
62
|
+
}, [0, 0]);
|
|
63
|
+
case "ollama":
|
|
64
|
+
return createOpenAICompatibleProvider("ollama", {
|
|
65
|
+
...config,
|
|
66
|
+
apiKey: config.apiKey || "ollama",
|
|
67
|
+
baseURL: config.baseURL ?? "http://localhost:11434/v1",
|
|
68
|
+
}, [0, 0]); // Free — local model
|
|
69
|
+
case "groq":
|
|
70
|
+
return createOpenAICompatibleProvider("groq", {
|
|
71
|
+
...config,
|
|
72
|
+
baseURL: config.baseURL ?? "https://api.groq.com/openai/v1",
|
|
73
|
+
}, [0.05, 0.10]);
|
|
74
|
+
case "deepseek":
|
|
75
|
+
return createOpenAICompatibleProvider("deepseek", {
|
|
76
|
+
...config,
|
|
77
|
+
baseURL: config.baseURL ?? "https://api.deepseek.com/v1",
|
|
78
|
+
}, [0.14, 0.28]);
|
|
79
|
+
default:
|
|
80
|
+
throw new Error(`Unknown provider: ${config.provider}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
get currentProvider(): Provider {
|
|
85
|
+
return this.providers[this.currentIndex]!;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private getCircuitBreaker(providerName: string): CircuitBreaker {
|
|
89
|
+
let cb = this.circuitBreakers.get(providerName);
|
|
90
|
+
if (!cb) {
|
|
91
|
+
cb = new CircuitBreaker(5, 60_000);
|
|
92
|
+
this.circuitBreakers.set(providerName, cb);
|
|
93
|
+
}
|
|
94
|
+
return cb;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async *stream(request: ProviderRequest): AsyncGenerator<StreamEvent> {
|
|
98
|
+
let lastError: Error | null = null;
|
|
99
|
+
|
|
100
|
+
for (let i = this.currentIndex; i < this.providers.length; i++) {
|
|
101
|
+
const provider = this.providers[i]!;
|
|
102
|
+
const breaker = this.getCircuitBreaker(provider.name);
|
|
103
|
+
|
|
104
|
+
// If this provider's circuit is open, skip to the next one
|
|
105
|
+
if (!breaker.canRequest()) {
|
|
106
|
+
process.stderr.write(
|
|
107
|
+
`[router] ${provider.name} circuit breaker open (${breaker.getStatus()}), skipping...\n`,
|
|
108
|
+
);
|
|
109
|
+
if (i + 1 < this.providers.length) continue;
|
|
110
|
+
throw new Error(
|
|
111
|
+
`All providers unavailable — ${provider.name} circuit breaker is open. Wait and retry.`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
for await (const event of provider.stream(request)) {
|
|
117
|
+
// Track usage
|
|
118
|
+
if (event.type === "usage" && event.usage) {
|
|
119
|
+
this.trackUsage(provider, event.usage);
|
|
120
|
+
}
|
|
121
|
+
yield event;
|
|
122
|
+
}
|
|
123
|
+
breaker.recordSuccess();
|
|
124
|
+
return; // Success — exit
|
|
125
|
+
} catch (err) {
|
|
126
|
+
lastError = err as Error;
|
|
127
|
+
breaker.recordFailure();
|
|
128
|
+
|
|
129
|
+
const isRateLimit =
|
|
130
|
+
lastError.message.includes("429") ||
|
|
131
|
+
lastError.message.includes("rate_limit") ||
|
|
132
|
+
lastError.message.includes("quota");
|
|
133
|
+
|
|
134
|
+
if (isRateLimit && i + 1 < this.providers.length) {
|
|
135
|
+
console.error(
|
|
136
|
+
`\n⚠ ${provider.name} rate limited, falling back to ${this.providers[i + 1]!.name}...`
|
|
137
|
+
);
|
|
138
|
+
this.currentIndex = i + 1;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
throw lastError;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
throw lastError ?? new Error("No providers available");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private trackUsage(provider: Provider, usage: TokenUsage) {
|
|
149
|
+
this.costTracker.record(provider.name, provider.config.model, {
|
|
150
|
+
inputTokens: usage.inputTokens,
|
|
151
|
+
outputTokens: usage.outputTokens,
|
|
152
|
+
reasoningTokens: usage.reasoningTokens ?? 0,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
getCostSummary(): string {
|
|
157
|
+
return this.costTracker.formatSummary();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified provider interface for multi-model support.
|
|
3
|
+
* Normalizes Claude (Anthropic SDK) and xAI/OpenAI (OpenAI SDK) into a common streaming interface.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface ToolDefinition {
|
|
7
|
+
name: string;
|
|
8
|
+
description: string;
|
|
9
|
+
input_schema: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ToolCall {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
input: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ToolResult {
|
|
19
|
+
tool_use_id: string;
|
|
20
|
+
content: string;
|
|
21
|
+
is_error?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface Message {
|
|
25
|
+
role: "user" | "assistant" | "tool";
|
|
26
|
+
content: string | ContentBlock[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type ContentBlock =
|
|
30
|
+
| { type: "text"; text: string }
|
|
31
|
+
| { type: "image_url"; image_url: { url: string } }
|
|
32
|
+
| { type: "tool_use"; id: string; name: string; input: Record<string, unknown> }
|
|
33
|
+
| { type: "tool_result"; tool_use_id: string; content: string; is_error?: boolean };
|
|
34
|
+
|
|
35
|
+
export type StopReason = "end_turn" | "tool_use" | "max_tokens";
|
|
36
|
+
|
|
37
|
+
export interface StreamEvent {
|
|
38
|
+
type: "text_delta" | "tool_call_start" | "tool_call_delta" | "tool_call_end" | "message_end" | "usage";
|
|
39
|
+
text?: string;
|
|
40
|
+
toolCall?: Partial<ToolCall>;
|
|
41
|
+
stopReason?: StopReason;
|
|
42
|
+
usage?: TokenUsage;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface TokenUsage {
|
|
46
|
+
inputTokens: number;
|
|
47
|
+
outputTokens: number;
|
|
48
|
+
/** xAI-specific: cost in USD ticks (1 tick = $0.000001) */
|
|
49
|
+
costTicks?: number;
|
|
50
|
+
/** Reasoning tokens used by the model (separate from output) */
|
|
51
|
+
reasoningTokens?: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ProviderConfig {
|
|
55
|
+
apiKey: string;
|
|
56
|
+
model: string;
|
|
57
|
+
baseURL?: string;
|
|
58
|
+
maxTokens?: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ProviderRequest {
|
|
62
|
+
systemPrompt: string;
|
|
63
|
+
messages: Message[];
|
|
64
|
+
tools: ToolDefinition[];
|
|
65
|
+
maxTokens?: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface Provider {
|
|
69
|
+
name: string;
|
|
70
|
+
config: ProviderConfig;
|
|
71
|
+
stream(request: ProviderRequest): AsyncGenerator<StreamEvent>;
|
|
72
|
+
/** Cost per million tokens [input, output] */
|
|
73
|
+
pricing: [number, number];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface ProviderRouterConfig {
|
|
77
|
+
primary: ProviderConfig & { provider: "xai" | "anthropic" | "openai" };
|
|
78
|
+
fallbacks?: Array<ProviderConfig & { provider: "xai" | "anthropic" | "openai" }>;
|
|
79
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* xAI Grok provider — uses OpenAI SDK with xAI base URL.
|
|
3
|
+
* Pattern from ashlr-landing/lib/xai-client.ts.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import OpenAI from "openai";
|
|
7
|
+
import type {
|
|
8
|
+
Provider,
|
|
9
|
+
ProviderConfig,
|
|
10
|
+
ProviderRequest,
|
|
11
|
+
StreamEvent,
|
|
12
|
+
ToolCall,
|
|
13
|
+
} from "./types.ts";
|
|
14
|
+
import { withRetry } from "./retry.ts";
|
|
15
|
+
|
|
16
|
+
export function createXAIProvider(config: ProviderConfig): Provider {
|
|
17
|
+
return createOpenAICompatibleProvider("xai", config, [0.2, 0.5]);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function createOpenAICompatibleProvider(
|
|
21
|
+
name: string,
|
|
22
|
+
config: ProviderConfig,
|
|
23
|
+
pricing: [number, number]
|
|
24
|
+
): Provider {
|
|
25
|
+
const client = new OpenAI({
|
|
26
|
+
apiKey: config.apiKey,
|
|
27
|
+
baseURL: config.baseURL ?? "https://api.x.ai/v1",
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
name,
|
|
32
|
+
config,
|
|
33
|
+
pricing,
|
|
34
|
+
|
|
35
|
+
async *stream(request: ProviderRequest): AsyncGenerator<StreamEvent> {
|
|
36
|
+
const tools: OpenAI.Chat.Completions.ChatCompletionTool[] = request.tools.map(
|
|
37
|
+
(t) => ({
|
|
38
|
+
type: "function" as const,
|
|
39
|
+
function: {
|
|
40
|
+
name: t.name,
|
|
41
|
+
description: t.description,
|
|
42
|
+
parameters: t.input_schema as Record<string, unknown>,
|
|
43
|
+
},
|
|
44
|
+
})
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
|
|
48
|
+
{ role: "system", content: request.systemPrompt },
|
|
49
|
+
...request.messages.flatMap(convertMessage),
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const stream = await withRetry(
|
|
53
|
+
() =>
|
|
54
|
+
client.chat.completions.create({
|
|
55
|
+
model: config.model,
|
|
56
|
+
messages,
|
|
57
|
+
tools: tools.length > 0 ? tools : undefined,
|
|
58
|
+
max_tokens: request.maxTokens ?? config.maxTokens ?? 8192,
|
|
59
|
+
stream: true,
|
|
60
|
+
stream_options: { include_usage: true },
|
|
61
|
+
}),
|
|
62
|
+
{ providerName: name },
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const toolCalls = new Map<number, { id: string; name: string; args: string }>();
|
|
66
|
+
|
|
67
|
+
for await (const chunk of stream) {
|
|
68
|
+
// Usage comes in a chunk with empty choices — handle it first
|
|
69
|
+
if (chunk.usage) {
|
|
70
|
+
const usage = chunk.usage as unknown as Record<string, unknown>;
|
|
71
|
+
const completionDetails = usage.completion_tokens_details as Record<string, number> | undefined;
|
|
72
|
+
yield {
|
|
73
|
+
type: "usage",
|
|
74
|
+
usage: {
|
|
75
|
+
inputTokens: chunk.usage.prompt_tokens,
|
|
76
|
+
outputTokens: chunk.usage.completion_tokens ?? 0,
|
|
77
|
+
costTicks: usage.cost_in_usd_ticks as number | undefined,
|
|
78
|
+
reasoningTokens: completionDetails?.reasoning_tokens,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const choice = chunk.choices[0];
|
|
84
|
+
if (!choice) continue;
|
|
85
|
+
|
|
86
|
+
const delta = choice.delta;
|
|
87
|
+
|
|
88
|
+
// Text content
|
|
89
|
+
if (delta.content) {
|
|
90
|
+
yield { type: "text_delta", text: delta.content };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Tool calls
|
|
94
|
+
if (delta.tool_calls) {
|
|
95
|
+
for (const tc of delta.tool_calls) {
|
|
96
|
+
const idx = tc.index;
|
|
97
|
+
if (!toolCalls.has(idx)) {
|
|
98
|
+
toolCalls.set(idx, { id: tc.id ?? "", name: tc.function?.name ?? "", args: "" });
|
|
99
|
+
yield {
|
|
100
|
+
type: "tool_call_start",
|
|
101
|
+
toolCall: { id: tc.id ?? "", name: tc.function?.name ?? "" },
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const existing = toolCalls.get(idx)!;
|
|
105
|
+
if (tc.id) existing.id = tc.id;
|
|
106
|
+
if (tc.function?.name) existing.name = tc.function.name;
|
|
107
|
+
if (tc.function?.arguments) {
|
|
108
|
+
existing.args += tc.function.arguments;
|
|
109
|
+
yield {
|
|
110
|
+
type: "tool_call_delta",
|
|
111
|
+
toolCall: { id: existing.id, name: existing.name },
|
|
112
|
+
text: tc.function.arguments,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Finish
|
|
119
|
+
if (choice.finish_reason) {
|
|
120
|
+
// Emit completed tool calls
|
|
121
|
+
for (const [, tc] of toolCalls) {
|
|
122
|
+
let input: Record<string, unknown> = {};
|
|
123
|
+
try {
|
|
124
|
+
input = JSON.parse(tc.args);
|
|
125
|
+
} catch {
|
|
126
|
+
input = { raw: tc.args };
|
|
127
|
+
}
|
|
128
|
+
yield {
|
|
129
|
+
type: "tool_call_end",
|
|
130
|
+
toolCall: { id: tc.id, name: tc.name, input } as ToolCall,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
yield {
|
|
135
|
+
type: "message_end",
|
|
136
|
+
stopReason:
|
|
137
|
+
choice.finish_reason === "tool_calls"
|
|
138
|
+
? "tool_use"
|
|
139
|
+
: choice.finish_reason === "length"
|
|
140
|
+
? "max_tokens"
|
|
141
|
+
: "end_turn",
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Convert our unified message format to OpenAI format.
|
|
152
|
+
* Returns an array because tool results expand into multiple messages.
|
|
153
|
+
*/
|
|
154
|
+
function convertMessage(
|
|
155
|
+
msg: ProviderRequest["messages"][number]
|
|
156
|
+
): OpenAI.Chat.Completions.ChatCompletionMessageParam[] {
|
|
157
|
+
// Handle tool results — each one becomes a separate "tool" message
|
|
158
|
+
if (msg.role === "user" && Array.isArray(msg.content)) {
|
|
159
|
+
const toolResults = msg.content.filter((b) => b.type === "tool_result");
|
|
160
|
+
if (toolResults.length > 0) {
|
|
161
|
+
return toolResults.map((b) => {
|
|
162
|
+
if (b.type !== "tool_result") throw new Error("unreachable");
|
|
163
|
+
return {
|
|
164
|
+
role: "tool" as const,
|
|
165
|
+
content: b.content,
|
|
166
|
+
tool_call_id: b.tool_use_id,
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (typeof msg.content === "string") {
|
|
173
|
+
return [{ role: msg.role === "tool" ? "user" : msg.role, content: msg.content }];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Handle content blocks — extract text, images, and tool uses
|
|
177
|
+
const textParts = msg.content
|
|
178
|
+
.filter((b) => b.type === "text")
|
|
179
|
+
.map((b) => (b as { type: "text"; text: string }).text)
|
|
180
|
+
.join("");
|
|
181
|
+
|
|
182
|
+
const imageBlocks = msg.content.filter((b) => b.type === "image_url");
|
|
183
|
+
const toolUses = msg.content.filter((b) => b.type === "tool_use");
|
|
184
|
+
|
|
185
|
+
// User message with images — send as multimodal content array
|
|
186
|
+
if (msg.role === "user" && imageBlocks.length > 0) {
|
|
187
|
+
const parts: Array<{ type: "text"; text: string } | { type: "image_url"; image_url: { url: string } }> = [];
|
|
188
|
+
if (textParts) parts.push({ type: "text", text: textParts });
|
|
189
|
+
for (const img of imageBlocks) {
|
|
190
|
+
if (img.type === "image_url") {
|
|
191
|
+
parts.push({ type: "image_url", image_url: { url: img.image_url.url } });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return [{ role: "user" as const, content: parts }];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (msg.role === "assistant" && toolUses.length > 0) {
|
|
198
|
+
return [{
|
|
199
|
+
role: "assistant",
|
|
200
|
+
content: textParts || null,
|
|
201
|
+
tool_calls: toolUses.map((tc) => {
|
|
202
|
+
if (tc.type !== "tool_use") throw new Error("unreachable");
|
|
203
|
+
return {
|
|
204
|
+
id: tc.id,
|
|
205
|
+
type: "function" as const,
|
|
206
|
+
function: {
|
|
207
|
+
name: tc.name,
|
|
208
|
+
arguments: JSON.stringify(tc.input),
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}),
|
|
212
|
+
}];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return [{ role: msg.role === "tool" ? "user" : msg.role, content: textParts }];
|
|
216
|
+
}
|
|
217
|
+
|