@nvae/llmswitch 0.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.
@@ -0,0 +1,33 @@
1
+ import { applyClaudeProfile, deactivateClaudeProfile } from "./claude.js";
2
+ import { applyCodexProfile, deactivateCodexProfile } from "./codex.js";
3
+ import { applyOpenCodeProfile, deactivateOpenCodeProfile } from "./opencode.js";
4
+ import { assertCompatible } from "../formats/compatibility.js";
5
+ import { clearActiveProfile } from "../store/profiles.js";
6
+ export async function applyProfile(tool, profile) {
7
+ assertCompatible(tool, profile.apiFormat);
8
+ switch (tool) {
9
+ case "claude":
10
+ return applyClaudeProfile(profile);
11
+ case "codex":
12
+ return applyCodexProfile(profile);
13
+ case "opencode":
14
+ return applyOpenCodeProfile(profile);
15
+ }
16
+ }
17
+ /** Disable the currently applied provider and clear tool-side managed config. */
18
+ export async function deactivateProfile(tool, profileName) {
19
+ let result;
20
+ switch (tool) {
21
+ case "claude":
22
+ result = await deactivateClaudeProfile();
23
+ break;
24
+ case "codex":
25
+ result = await deactivateCodexProfile(profileName);
26
+ break;
27
+ case "opencode":
28
+ result = deactivateOpenCodeProfile(profileName);
29
+ break;
30
+ }
31
+ clearActiveProfile(tool);
32
+ return result;
33
+ }
@@ -0,0 +1,21 @@
1
+ export function deepMerge(base, patch) {
2
+ const out = { ...base };
3
+ for (const [key, value] of Object.entries(patch)) {
4
+ if (value === undefined)
5
+ continue;
6
+ const prev = out[key];
7
+ if (isPlainObject(prev) &&
8
+ isPlainObject(value)) {
9
+ out[key] = deepMerge(prev, value);
10
+ }
11
+ else {
12
+ out[key] = value;
13
+ }
14
+ }
15
+ return out;
16
+ }
17
+ function isPlainObject(value) {
18
+ return (typeof value === "object" &&
19
+ value !== null &&
20
+ !Array.isArray(value));
21
+ }
@@ -0,0 +1,162 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { assertCompatible } from "../formats/compatibility.js";
4
+ import { normalizeBaseUrlForFormat } from "../utils/base-url.js";
5
+ import { atomicWriteFile, backupFile, ensureDir } from "../utils/fs.js";
6
+ import { applyProxyToEnvRecord, clearProxyEnvKeys } from "../utils/proxy.js";
7
+ import { getBackupsDir, getOpenCodeAuthPath, getOpenCodeConfigDir, getOpenCodeConfigPath, } from "../utils/paths.js";
8
+ import { setActiveProfile } from "../store/profiles.js";
9
+ function providerId(name) {
10
+ return `llms-${name}`.replace(/[^a-zA-Z0-9_-]/g, "-");
11
+ }
12
+ function npmForFormat(format) {
13
+ switch (format) {
14
+ case "anthropic":
15
+ return "@ai-sdk/anthropic";
16
+ case "openai-responses":
17
+ return "@ai-sdk/openai";
18
+ case "openai-chat":
19
+ return "@ai-sdk/openai-compatible";
20
+ }
21
+ }
22
+ export function readOpenCodeConfig(path = getOpenCodeConfigPath()) {
23
+ if (!existsSync(path)) {
24
+ return { $schema: "https://opencode.ai/config.json" };
25
+ }
26
+ return JSON.parse(readFileSync(path, "utf8"));
27
+ }
28
+ export function readOpenCodeAuth(path = getOpenCodeAuthPath()) {
29
+ if (!existsSync(path))
30
+ return {};
31
+ return JSON.parse(readFileSync(path, "utf8"));
32
+ }
33
+ export function buildOpenCodeProviderBlock(profile) {
34
+ const models = {};
35
+ for (const id of profile.models.list) {
36
+ models[id] = { name: id };
37
+ }
38
+ if (!models[profile.models.default]) {
39
+ models[profile.models.default] = { name: profile.models.default };
40
+ }
41
+ const options = {
42
+ baseURL: normalizeBaseUrlForFormat(profile.apiFormat, profile.baseUrl),
43
+ };
44
+ if (profile.apiKey) {
45
+ options.apiKey = profile.apiKey;
46
+ }
47
+ if (profile.headers && Object.keys(profile.headers).length > 0) {
48
+ options.headers = { ...profile.headers };
49
+ }
50
+ return {
51
+ npm: npmForFormat(profile.apiFormat),
52
+ name: profile.displayName || profile.name,
53
+ options,
54
+ models,
55
+ };
56
+ }
57
+ export function buildOpenCodeConfig(existing, profile) {
58
+ assertCompatible("opencode", profile.apiFormat);
59
+ const id = providerId(profile.name);
60
+ const providers = {
61
+ ...(existing.provider || {}),
62
+ };
63
+ providers[id] = buildOpenCodeProviderBlock(profile);
64
+ // Optional top-level env for proxy (OpenCode may pass through)
65
+ const env = {
66
+ ...(existing.env || {}),
67
+ };
68
+ clearProxyEnvKeys(env);
69
+ applyProxyToEnvRecord(env, profile.proxy);
70
+ const next = {
71
+ ...existing,
72
+ $schema: existing.$schema || "https://opencode.ai/config.json",
73
+ provider: providers,
74
+ model: `${id}/${profile.models.default}`,
75
+ };
76
+ if (Object.keys(env).length > 0) {
77
+ next.env = env;
78
+ }
79
+ else {
80
+ delete next.env;
81
+ }
82
+ return next;
83
+ }
84
+ export function buildOpenCodeAuth(existing, profile) {
85
+ const id = providerId(profile.name);
86
+ const next = { ...existing };
87
+ if (profile.apiKey) {
88
+ next[id] = {
89
+ type: "api",
90
+ key: profile.apiKey,
91
+ };
92
+ }
93
+ return next;
94
+ }
95
+ export function applyOpenCodeProfile(profile) {
96
+ assertCompatible("opencode", profile.apiFormat);
97
+ ensureDir(getOpenCodeConfigDir());
98
+ ensureDir(dirname(getOpenCodeAuthPath()));
99
+ const configPath = getOpenCodeConfigPath();
100
+ const authPath = getOpenCodeAuthPath();
101
+ const existing = readOpenCodeConfig(configPath);
102
+ const backupPath = backupFile(configPath, getBackupsDir("opencode"), "opencode");
103
+ backupFile(authPath, getBackupsDir("opencode"), "auth");
104
+ const nextConfig = buildOpenCodeConfig(existing, profile);
105
+ atomicWriteFile(configPath, JSON.stringify(nextConfig, null, 2) + "\n");
106
+ const nextAuth = buildOpenCodeAuth(readOpenCodeAuth(authPath), profile);
107
+ atomicWriteFile(authPath, JSON.stringify(nextAuth, null, 2) + "\n");
108
+ setActiveProfile("opencode", profile.name);
109
+ return {
110
+ tool: "opencode",
111
+ profile: profile.name,
112
+ configPath,
113
+ backupPath,
114
+ restartHint: "请重新启动 OpenCode 会话以使配置与代理生效。",
115
+ };
116
+ }
117
+ export function deactivateOpenCodeProfile(profileName) {
118
+ ensureDir(getOpenCodeConfigDir());
119
+ ensureDir(dirname(getOpenCodeAuthPath()));
120
+ const configPath = getOpenCodeConfigPath();
121
+ const authPath = getOpenCodeAuthPath();
122
+ const existing = readOpenCodeConfig(configPath);
123
+ const backupPath = backupFile(configPath, getBackupsDir("opencode"), "opencode");
124
+ backupFile(authPath, getBackupsDir("opencode"), "auth");
125
+ const providers = {
126
+ ...(existing.provider || {}),
127
+ };
128
+ const id = profileName ? providerId(profileName) : null;
129
+ if (id)
130
+ delete providers[id];
131
+ const env = {
132
+ ...(existing.env || {}),
133
+ };
134
+ clearProxyEnvKeys(env);
135
+ const next = {
136
+ ...existing,
137
+ provider: providers,
138
+ };
139
+ if (id && typeof existing.model === "string" && existing.model.startsWith(`${id}/`)) {
140
+ delete next.model;
141
+ }
142
+ if (Object.keys(env).length > 0)
143
+ next.env = env;
144
+ else
145
+ delete next.env;
146
+ atomicWriteFile(configPath, JSON.stringify(next, null, 2) + "\n");
147
+ if (id) {
148
+ const auth = readOpenCodeAuth(authPath);
149
+ if (auth[id]) {
150
+ const nextAuth = { ...auth };
151
+ delete nextAuth[id];
152
+ atomicWriteFile(authPath, JSON.stringify(nextAuth, null, 2) + "\n");
153
+ }
154
+ }
155
+ return {
156
+ tool: "opencode",
157
+ profile: profileName || "",
158
+ configPath,
159
+ backupPath,
160
+ restartHint: "已禁用供应商。请重新启动 OpenCode 会话使变更生效。",
161
+ };
162
+ }
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Translate Anthropic Messages API requests → OpenAI Chat Completions.
3
+ */
4
+ function asRecord(value) {
5
+ if (value && typeof value === "object" && !Array.isArray(value)) {
6
+ return value;
7
+ }
8
+ return null;
9
+ }
10
+ function extractTextBlocks(content) {
11
+ if (typeof content === "string")
12
+ return content;
13
+ if (!Array.isArray(content))
14
+ return "";
15
+ const parts = [];
16
+ for (const part of content) {
17
+ const row = asRecord(part);
18
+ if (!row)
19
+ continue;
20
+ if (row.type === "text" && typeof row.text === "string") {
21
+ parts.push(row.text);
22
+ }
23
+ else if (typeof row.text === "string") {
24
+ parts.push(row.text);
25
+ }
26
+ }
27
+ return parts.join("");
28
+ }
29
+ function systemToMessage(system) {
30
+ if (typeof system === "string" && system.trim()) {
31
+ return { role: "system", content: system };
32
+ }
33
+ if (Array.isArray(system)) {
34
+ const text = extractTextBlocks(system);
35
+ if (text.trim())
36
+ return { role: "system", content: text };
37
+ }
38
+ return null;
39
+ }
40
+ function mapAssistantContent(content) {
41
+ if (typeof content === "string") {
42
+ return { role: "assistant", content };
43
+ }
44
+ if (!Array.isArray(content)) {
45
+ return { role: "assistant", content: "" };
46
+ }
47
+ const textParts = [];
48
+ const toolCalls = [];
49
+ for (const part of content) {
50
+ const row = asRecord(part);
51
+ if (!row)
52
+ continue;
53
+ if (row.type === "text" && typeof row.text === "string") {
54
+ textParts.push(row.text);
55
+ continue;
56
+ }
57
+ if (row.type === "tool_use") {
58
+ const id = String(row.id || `tool_${toolCalls.length}`);
59
+ const name = String(row.name || "tool");
60
+ let args = "{}";
61
+ try {
62
+ args = JSON.stringify(row.input ?? {});
63
+ }
64
+ catch {
65
+ args = "{}";
66
+ }
67
+ toolCalls.push({
68
+ id,
69
+ type: "function",
70
+ function: { name, arguments: args },
71
+ });
72
+ }
73
+ }
74
+ const msg = {
75
+ role: "assistant",
76
+ content: textParts.length ? textParts.join("") : toolCalls.length ? null : "",
77
+ };
78
+ if (toolCalls.length)
79
+ msg.tool_calls = toolCalls;
80
+ return msg;
81
+ }
82
+ function mapUserContent(content) {
83
+ if (typeof content === "string") {
84
+ return [{ role: "user", content }];
85
+ }
86
+ if (!Array.isArray(content)) {
87
+ return [{ role: "user", content: "" }];
88
+ }
89
+ const textParts = [];
90
+ const toolResults = [];
91
+ for (const part of content) {
92
+ const row = asRecord(part);
93
+ if (!row)
94
+ continue;
95
+ if (row.type === "text" && typeof row.text === "string") {
96
+ textParts.push(row.text);
97
+ continue;
98
+ }
99
+ if (row.type === "tool_result") {
100
+ const toolCallId = String(row.tool_use_id || row.id || "");
101
+ let resultContent = "";
102
+ if (typeof row.content === "string")
103
+ resultContent = row.content;
104
+ else if (Array.isArray(row.content)) {
105
+ resultContent = extractTextBlocks(row.content);
106
+ }
107
+ else if (row.content != null) {
108
+ try {
109
+ resultContent = JSON.stringify(row.content);
110
+ }
111
+ catch {
112
+ resultContent = String(row.content);
113
+ }
114
+ }
115
+ toolResults.push({
116
+ role: "tool",
117
+ tool_call_id: toolCallId,
118
+ content: resultContent,
119
+ });
120
+ }
121
+ }
122
+ const out = [];
123
+ if (textParts.length) {
124
+ out.push({ role: "user", content: textParts.join("") });
125
+ }
126
+ out.push(...toolResults);
127
+ if (!out.length)
128
+ out.push({ role: "user", content: "" });
129
+ return out;
130
+ }
131
+ export function anthropicMessagesToChatMessages(body) {
132
+ const messages = [];
133
+ const system = systemToMessage(body.system);
134
+ if (system)
135
+ messages.push(system);
136
+ const rawMessages = Array.isArray(body.messages) ? body.messages : [];
137
+ for (const item of rawMessages) {
138
+ const row = asRecord(item);
139
+ if (!row)
140
+ continue;
141
+ const role = row.role;
142
+ if (role === "assistant") {
143
+ messages.push(mapAssistantContent(row.content));
144
+ continue;
145
+ }
146
+ if (role === "user") {
147
+ messages.push(...mapUserContent(row.content));
148
+ continue;
149
+ }
150
+ }
151
+ return messages;
152
+ }
153
+ export function mapAnthropicTools(tools) {
154
+ if (!Array.isArray(tools) || tools.length === 0)
155
+ return undefined;
156
+ const out = [];
157
+ for (const tool of tools) {
158
+ const row = asRecord(tool);
159
+ if (!row)
160
+ continue;
161
+ const name = String(row.name || "").trim();
162
+ if (!name)
163
+ continue;
164
+ out.push({
165
+ type: "function",
166
+ function: {
167
+ name,
168
+ description: typeof row.description === "string" ? row.description : undefined,
169
+ parameters: row.input_schema ?? row.parameters ?? { type: "object", properties: {} },
170
+ },
171
+ });
172
+ }
173
+ return out.length ? out : undefined;
174
+ }
175
+ export function mapAnthropicToolChoice(toolChoice) {
176
+ if (toolChoice == null)
177
+ return undefined;
178
+ if (toolChoice === "auto" || toolChoice === "none" || toolChoice === "required") {
179
+ return toolChoice;
180
+ }
181
+ const obj = asRecord(toolChoice);
182
+ if (!obj)
183
+ return "auto";
184
+ if (obj.type === "auto")
185
+ return "auto";
186
+ if (obj.type === "any")
187
+ return "required";
188
+ if (obj.type === "none")
189
+ return "none";
190
+ if (obj.type === "tool") {
191
+ const name = String(obj.name || "");
192
+ if (!name)
193
+ return "auto";
194
+ return { type: "function", function: { name } };
195
+ }
196
+ return "auto";
197
+ }
198
+ export function anthropicToChatRequest(body) {
199
+ const stream = Boolean(body.stream);
200
+ const req = {
201
+ model: String(body.model || ""),
202
+ messages: anthropicMessagesToChatMessages(body),
203
+ stream,
204
+ };
205
+ if (stream) {
206
+ req.stream_options = { include_usage: true };
207
+ }
208
+ const tools = mapAnthropicTools(body.tools);
209
+ if (tools)
210
+ req.tools = tools;
211
+ if (body.tool_choice !== undefined) {
212
+ req.tool_choice = mapAnthropicToolChoice(body.tool_choice);
213
+ }
214
+ if (typeof body.temperature === "number")
215
+ req.temperature = body.temperature;
216
+ if (typeof body.top_p === "number")
217
+ req.top_p = body.top_p;
218
+ if (typeof body.max_tokens === "number") {
219
+ req.max_tokens = body.max_tokens;
220
+ req.max_completion_tokens = body.max_tokens;
221
+ }
222
+ if (typeof body.stop_sequences !== "undefined") {
223
+ req.stop = body.stop_sequences;
224
+ }
225
+ return req;
226
+ }
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Translate Chat Completions (stream/non-stream) → Anthropic Messages API.
3
+ */
4
+ import { parseChatSseLine } from "./translate-response.js";
5
+ function newId(prefix) {
6
+ return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
7
+ }
8
+ function asRecord(value) {
9
+ if (value && typeof value === "object" && !Array.isArray(value)) {
10
+ return value;
11
+ }
12
+ return null;
13
+ }
14
+ function numberOr(value) {
15
+ return typeof value === "number" ? value : undefined;
16
+ }
17
+ function parseToolArguments(raw) {
18
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
19
+ return raw;
20
+ }
21
+ if (typeof raw !== "string" || !raw.trim())
22
+ return {};
23
+ try {
24
+ const parsed = JSON.parse(raw);
25
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
26
+ return parsed;
27
+ }
28
+ return {};
29
+ }
30
+ catch {
31
+ return {};
32
+ }
33
+ }
34
+ export function mapFinishReason(reason) {
35
+ if (reason === "tool_calls" || reason === "function_call")
36
+ return "tool_use";
37
+ if (reason === "length")
38
+ return "max_tokens";
39
+ if (reason === "stop" || reason === "end_turn")
40
+ return "end_turn";
41
+ return reason == null ? null : "end_turn";
42
+ }
43
+ export function chatCompletionToAnthropicMessage(chat, fallbackModel) {
44
+ const choice = Array.isArray(chat.choices)
45
+ ? asRecord(chat.choices[0])
46
+ : null;
47
+ const message = choice ? asRecord(choice.message) : null;
48
+ const content = [];
49
+ const text = typeof message?.content === "string"
50
+ ? message.content
51
+ : message?.content == null
52
+ ? ""
53
+ : String(message.content);
54
+ if (text) {
55
+ content.push({ type: "text", text });
56
+ }
57
+ const toolCalls = Array.isArray(message?.tool_calls)
58
+ ? message.tool_calls
59
+ : [];
60
+ for (const call of toolCalls) {
61
+ const row = asRecord(call);
62
+ if (!row)
63
+ continue;
64
+ const fn = asRecord(row.function);
65
+ content.push({
66
+ type: "tool_use",
67
+ id: String(row.id || newId("toolu")),
68
+ name: String(fn?.name || "tool"),
69
+ input: parseToolArguments(fn?.arguments),
70
+ });
71
+ }
72
+ if (!content.length) {
73
+ content.push({ type: "text", text: "" });
74
+ }
75
+ const usage = asRecord(chat.usage);
76
+ const stopReason = mapFinishReason(choice?.finish_reason) ||
77
+ (toolCalls.length ? "tool_use" : "end_turn");
78
+ return {
79
+ id: typeof chat.id === "string" ? `msg_${chat.id}` : newId("msg"),
80
+ type: "message",
81
+ role: "assistant",
82
+ model: String(chat.model || fallbackModel || ""),
83
+ content,
84
+ stop_reason: stopReason,
85
+ stop_sequence: null,
86
+ usage: {
87
+ input_tokens: numberOr(usage?.prompt_tokens) ?? numberOr(usage?.input_tokens) ?? 0,
88
+ output_tokens: numberOr(usage?.completion_tokens) ??
89
+ numberOr(usage?.output_tokens) ??
90
+ 0,
91
+ },
92
+ };
93
+ }
94
+ export function createAnthropicStreamState(model, messageId) {
95
+ return {
96
+ messageId: messageId || newId("msg"),
97
+ model,
98
+ textIndex: null,
99
+ textStarted: false,
100
+ fullText: "",
101
+ toolCalls: new Map(),
102
+ nextBlockIndex: 0,
103
+ started: false,
104
+ stopped: false,
105
+ stopReason: null,
106
+ };
107
+ }
108
+ function sseData(payload) {
109
+ return `event: ${String(payload.type)}\ndata: ${JSON.stringify(payload)}\n\n`;
110
+ }
111
+ function ensureMessageStart(state, out) {
112
+ if (state.started)
113
+ return;
114
+ state.started = true;
115
+ out.push(sseData({
116
+ type: "message_start",
117
+ message: {
118
+ id: state.messageId,
119
+ type: "message",
120
+ role: "assistant",
121
+ model: state.model,
122
+ content: [],
123
+ stop_reason: null,
124
+ stop_sequence: null,
125
+ usage: { input_tokens: 0, output_tokens: 0 },
126
+ },
127
+ }));
128
+ }
129
+ function ensureTextBlock(state, out) {
130
+ if (state.textStarted)
131
+ return;
132
+ state.textStarted = true;
133
+ state.textIndex = state.nextBlockIndex++;
134
+ out.push(sseData({
135
+ type: "content_block_start",
136
+ index: state.textIndex,
137
+ content_block: { type: "text", text: "" },
138
+ }));
139
+ }
140
+ function closeTextBlock(state, out) {
141
+ if (!state.textStarted || state.textIndex == null)
142
+ return;
143
+ out.push(sseData({
144
+ type: "content_block_stop",
145
+ index: state.textIndex,
146
+ }));
147
+ state.textStarted = false;
148
+ state.textIndex = null;
149
+ }
150
+ export function chatChunkToAnthropicEvents(chunk, state) {
151
+ const out = [];
152
+ ensureMessageStart(state, out);
153
+ if (chunk.model && typeof chunk.model === "string") {
154
+ state.model = chunk.model;
155
+ }
156
+ const usage = asRecord(chunk.usage);
157
+ if (usage) {
158
+ state.usage = {
159
+ input_tokens: numberOr(usage.prompt_tokens) ?? numberOr(usage.input_tokens) ?? 0,
160
+ output_tokens: numberOr(usage.completion_tokens) ??
161
+ numberOr(usage.output_tokens) ??
162
+ 0,
163
+ };
164
+ }
165
+ const choice = Array.isArray(chunk.choices)
166
+ ? asRecord(chunk.choices[0])
167
+ : null;
168
+ if (!choice)
169
+ return out;
170
+ const finish = mapFinishReason(choice.finish_reason);
171
+ if (finish)
172
+ state.stopReason = finish;
173
+ const delta = asRecord(choice.delta) || asRecord(choice.message);
174
+ if (!delta)
175
+ return out;
176
+ if (typeof delta.content === "string" && delta.content.length) {
177
+ ensureTextBlock(state, out);
178
+ state.fullText += delta.content;
179
+ out.push(sseData({
180
+ type: "content_block_delta",
181
+ index: state.textIndex,
182
+ delta: { type: "text_delta", text: delta.content },
183
+ }));
184
+ }
185
+ const toolCalls = Array.isArray(delta.tool_calls) ? delta.tool_calls : [];
186
+ for (const call of toolCalls) {
187
+ const row = asRecord(call);
188
+ if (!row)
189
+ continue;
190
+ const index = typeof row.index === "number" ? row.index : state.toolCalls.size;
191
+ let entry = state.toolCalls.get(index);
192
+ const fn = asRecord(row.function);
193
+ if (!entry) {
194
+ // Close text before tool blocks when tools start
195
+ closeTextBlock(state, out);
196
+ entry = {
197
+ blockIndex: state.nextBlockIndex++,
198
+ id: String(row.id || newId("toolu")),
199
+ name: String(fn?.name || ""),
200
+ arguments: "",
201
+ started: false,
202
+ };
203
+ state.toolCalls.set(index, entry);
204
+ }
205
+ if (row.id)
206
+ entry.id = String(row.id);
207
+ if (fn?.name)
208
+ entry.name = String(fn.name);
209
+ if (typeof fn?.arguments === "string") {
210
+ entry.arguments += fn.arguments;
211
+ }
212
+ if (!entry.started && entry.name) {
213
+ entry.started = true;
214
+ out.push(sseData({
215
+ type: "content_block_start",
216
+ index: entry.blockIndex,
217
+ content_block: {
218
+ type: "tool_use",
219
+ id: entry.id,
220
+ name: entry.name,
221
+ input: {},
222
+ },
223
+ }));
224
+ }
225
+ if (entry.started && typeof fn?.arguments === "string" && fn.arguments) {
226
+ out.push(sseData({
227
+ type: "content_block_delta",
228
+ index: entry.blockIndex,
229
+ delta: {
230
+ type: "input_json_delta",
231
+ partial_json: fn.arguments,
232
+ },
233
+ }));
234
+ }
235
+ }
236
+ return out;
237
+ }
238
+ export function forceCompleteAnthropicStream(state) {
239
+ if (state.stopped)
240
+ return [];
241
+ state.stopped = true;
242
+ const out = [];
243
+ ensureMessageStart(state, out);
244
+ closeTextBlock(state, out);
245
+ for (const entry of state.toolCalls.values()) {
246
+ if (entry.started) {
247
+ out.push(sseData({
248
+ type: "content_block_stop",
249
+ index: entry.blockIndex,
250
+ }));
251
+ }
252
+ }
253
+ const stopReason = state.stopReason ||
254
+ (state.toolCalls.size > 0 ? "tool_use" : "end_turn");
255
+ out.push(sseData({
256
+ type: "message_delta",
257
+ delta: { stop_reason: stopReason, stop_sequence: null },
258
+ usage: {
259
+ output_tokens: state.usage?.output_tokens ?? 0,
260
+ },
261
+ }));
262
+ out.push(sseData({ type: "message_stop" }));
263
+ return out;
264
+ }
265
+ export { parseChatSseLine };