@heventure/model-provider-x 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.
@@ -0,0 +1,178 @@
1
+ import { emitKeypressEvents } from "node:readline";
2
+ const width = 60;
3
+ const border = `+${"-".repeat(width)}+`;
4
+ export function renderSelect(title, choices, cursor) {
5
+ return renderFrame(title, choices.map((choice, index) => renderChoiceLine(choice, index, index === cursor)), "Use Up/Down or j/k. Press Enter to confirm.");
6
+ }
7
+ export function renderMultiSelect(title, choices, cursor, selected) {
8
+ return renderFrame(title, choices.map((choice, index) => renderChoiceLine(choice, index, index === cursor, selected.has(index))), `Selected: ${selected.size}/${choices.filter((choice) => !choice.disabled).length} | Space toggles. a selects all. Enter confirms.`);
9
+ }
10
+ export function renderIntro() {
11
+ return [
12
+ border,
13
+ boxLine(color("model-provider-x", "cyan")),
14
+ boxLine(color("OpenAI-compatible provider setup", "muted")),
15
+ boxLine(""),
16
+ boxLine("Validates /models, generates JSON,"),
17
+ boxLine("and can write opencode.jsonc."),
18
+ boxLine("API keys are only written when you provide one."),
19
+ border,
20
+ ""
21
+ ].join("\n");
22
+ }
23
+ export function moveCursor(cursor, delta, length) {
24
+ if (length <= 0) {
25
+ return 0;
26
+ }
27
+ return (cursor + delta + length) % length;
28
+ }
29
+ export function toggleSelectedIndex(selected, index) {
30
+ const next = new Set(selected);
31
+ if (next.has(index)) {
32
+ next.delete(index);
33
+ }
34
+ else {
35
+ next.add(index);
36
+ }
37
+ return next;
38
+ }
39
+ export async function selectChoice(title, choices, streams = { input: process.stdin, output: process.stdout }) {
40
+ const enabledChoices = choices.filter((choice) => !choice.disabled);
41
+ if (enabledChoices.length === 0) {
42
+ throw new Error("No selectable choices are available");
43
+ }
44
+ const selected = await runKeyMenu((cursor) => renderSelect(title, choices, cursor), choices, streams, () => undefined);
45
+ return selected.value;
46
+ }
47
+ export async function multiSelectChoices(title, choices, streams = { input: process.stdin, output: process.stdout }) {
48
+ let selected = new Set(choices.map((choice, index) => (choice.disabled ? -1 : index)).filter((index) => index >= 0));
49
+ const choice = await runKeyMenu((cursor) => renderMultiSelect(title, choices, cursor, selected), choices, streams, (key, cursor) => {
50
+ if (key.name === "space") {
51
+ selected = toggleSelectedIndex(selected, cursor);
52
+ }
53
+ if (key.name === "a") {
54
+ selected =
55
+ selected.size === choices.filter((item) => !item.disabled).length
56
+ ? new Set()
57
+ : new Set(choices.map((item, index) => (item.disabled ? -1 : index)).filter((index) => index >= 0));
58
+ }
59
+ });
60
+ if (choice && selected.size === 0) {
61
+ throw new Error("Select at least one item");
62
+ }
63
+ return choices.filter((_choice, index) => selected.has(index)).map((item) => item.value);
64
+ }
65
+ export function canUseTui(input = process.stdin, output = process.stdout) {
66
+ return Boolean(input.isTTY && output.isTTY && typeof input.setRawMode === "function");
67
+ }
68
+ async function runKeyMenu(render, choices, streams, onKey) {
69
+ const input = streams.input;
70
+ const output = streams.output;
71
+ let cursor = firstEnabledIndex(choices);
72
+ emitKeypressEvents(input);
73
+ input.setRawMode(true);
74
+ input.resume();
75
+ return new Promise((resolve, reject) => {
76
+ const cleanup = () => {
77
+ input.setRawMode(false);
78
+ input.off("keypress", handleKeypress);
79
+ output.write("\n");
80
+ };
81
+ const draw = () => {
82
+ output.write("\x1b[2J\x1b[H");
83
+ output.write(render(cursor));
84
+ };
85
+ const handleKeypress = (_chunk, key) => {
86
+ try {
87
+ if (key.ctrl && key.name === "c") {
88
+ cleanup();
89
+ reject(new Error("Cancelled"));
90
+ return;
91
+ }
92
+ if (key.name === "up" || key.name === "k") {
93
+ cursor = nextEnabledIndex(choices, cursor, -1);
94
+ }
95
+ else if (key.name === "down" || key.name === "j") {
96
+ cursor = nextEnabledIndex(choices, cursor, 1);
97
+ }
98
+ else if (key.name === "return") {
99
+ cleanup();
100
+ resolve(choices[cursor]);
101
+ return;
102
+ }
103
+ else {
104
+ onKey(key, cursor);
105
+ }
106
+ draw();
107
+ }
108
+ catch (error) {
109
+ cleanup();
110
+ reject(error);
111
+ }
112
+ };
113
+ input.on("keypress", handleKeypress);
114
+ draw();
115
+ });
116
+ }
117
+ function renderFrame(title, lines, footer) {
118
+ return [
119
+ border,
120
+ boxLine(color("model-provider-x", "cyan")),
121
+ boxLine(color(title, "bold")),
122
+ border,
123
+ ...lines.map((line) => boxLine(line)),
124
+ border,
125
+ boxLine(color(footer, "muted")),
126
+ border,
127
+ ""
128
+ ].join("\n");
129
+ }
130
+ function renderChoiceLine(choice, index, active, selected) {
131
+ const cursor = active ? color(">", "cyan") : " ";
132
+ const mark = selected === undefined ? "" : `${selected ? color("[x]", "green") : "[ ]"} `;
133
+ const ordinal = `${index + 1}.`;
134
+ const hint = choice.hint ? ` - ${choice.hint}` : "";
135
+ const disabled = choice.disabled ? " (unavailable)" : "";
136
+ const label = choice.disabled ? color(choice.label, "muted") : choice.label;
137
+ return `${cursor} ${mark}${ordinal} ${label}${color(hint, "muted")}${color(disabled, "muted")}`;
138
+ }
139
+ function firstEnabledIndex(choices) {
140
+ const index = choices.findIndex((choice) => !choice.disabled);
141
+ if (index === -1) {
142
+ throw new Error("No selectable choices are available");
143
+ }
144
+ return index;
145
+ }
146
+ function nextEnabledIndex(choices, cursor, delta) {
147
+ let next = cursor;
148
+ do {
149
+ next = moveCursor(next, delta, choices.length);
150
+ } while (choices[next].disabled);
151
+ return next;
152
+ }
153
+ export function stripAnsi(value) {
154
+ return value.replace(new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g"), "");
155
+ }
156
+ function boxLine(value) {
157
+ const visible = stripAnsi(value);
158
+ const trimmed = visible.length > width - 2 ? truncateAnsi(value, width - 5) : value;
159
+ const padding = width - stripAnsi(trimmed).length - 1;
160
+ return `| ${trimmed}${" ".repeat(Math.max(0, padding))}|`;
161
+ }
162
+ function truncateAnsi(value, maxVisible) {
163
+ const plain = stripAnsi(value);
164
+ return `${plain.slice(0, maxVisible)}...`;
165
+ }
166
+ function color(value, tone) {
167
+ if (process.env.NO_COLOR) {
168
+ return value;
169
+ }
170
+ const codes = {
171
+ bold: ["\x1b[1m", "\x1b[22m"],
172
+ cyan: ["\x1b[36m", "\x1b[39m"],
173
+ green: ["\x1b[32m", "\x1b[39m"],
174
+ muted: ["\x1b[2m", "\x1b[22m"]
175
+ };
176
+ const [open, close] = codes[tone];
177
+ return `${open}${value}${close}`;
178
+ }
@@ -0,0 +1,102 @@
1
+ import { access, copyFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
2
+ import { constants } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { homedir } from "node:os";
5
+ import { applyEdits, format, modify, parse } from "jsonc-parser";
6
+ export function getDefaultConfigPath(homeDir = homedir()) {
7
+ return join(homeDir, ".config", "opencode", "opencode.jsonc");
8
+ }
9
+ export async function discoverOpenCodeConfigs(options = {}) {
10
+ const homeDir = options.homeDir ?? homedir();
11
+ const env = options.env ?? process.env;
12
+ const candidates = [
13
+ { path: join(homeDir, ".config", "opencode", "opencode.jsonc"), label: "Global JSONC config" },
14
+ { path: join(homeDir, ".config", "opencode", "opencode.json"), label: "Global JSON config" },
15
+ { path: join(homeDir, ".opencode", "opencode.jsonc"), label: "Legacy JSONC config" },
16
+ { path: join(homeDir, ".opencode", "opencode.json"), label: "Legacy JSON config" }
17
+ ];
18
+ if (env.OPENCODE_CONFIG) {
19
+ candidates.push({ path: env.OPENCODE_CONFIG, label: "OPENCODE_CONFIG" });
20
+ }
21
+ if (env.OPENCODE_CONFIG_DIR) {
22
+ candidates.push({ path: join(env.OPENCODE_CONFIG_DIR, "opencode.jsonc"), label: "OPENCODE_CONFIG_DIR JSONC" });
23
+ candidates.push({ path: join(env.OPENCODE_CONFIG_DIR, "opencode.json"), label: "OPENCODE_CONFIG_DIR JSON" });
24
+ }
25
+ const seen = new Set();
26
+ const results = [];
27
+ for (const candidate of candidates) {
28
+ if (seen.has(candidate.path)) {
29
+ continue;
30
+ }
31
+ seen.add(candidate.path);
32
+ const exists = await fileExists(candidate.path);
33
+ if (!exists) {
34
+ continue;
35
+ }
36
+ const writable = await isWritable(candidate.path);
37
+ const hasProvider = options.providerId ? await configHasProvider(candidate.path, options.providerId) : false;
38
+ results.push({ ...candidate, exists, writable, hasProvider });
39
+ }
40
+ return results;
41
+ }
42
+ export function mergeProviderIntoConfigText(text, providerId, provider) {
43
+ let source = text.trim() ? text : "{}\n";
44
+ const errors = [];
45
+ const parsed = parse(source, errors, { allowTrailingComma: true, disallowComments: false });
46
+ if (errors.length > 0 || typeof parsed !== "object" || parsed === null) {
47
+ throw new Error("Selected OpenCode config is not valid JSON/JSONC");
48
+ }
49
+ if (!isRecord(parsed.provider)) {
50
+ source = applyEdits(source, modify(source, ["provider"], {}, { formattingOptions: { insertSpaces: true, tabSize: 2 } }));
51
+ }
52
+ const edited = applyEdits(source, modify(source, ["provider", providerId], provider, {
53
+ formattingOptions: { insertSpaces: true, tabSize: 2 }
54
+ }));
55
+ return applyEdits(edited, format(edited, undefined, { insertSpaces: true, tabSize: 2 }));
56
+ }
57
+ export async function writeProviderToConfig(input) {
58
+ await mkdir(dirname(input.targetPath), { recursive: true });
59
+ const exists = await fileExists(input.targetPath);
60
+ const current = exists ? await readFile(input.targetPath, "utf8") : "{}\n";
61
+ let backupPath;
62
+ if (exists) {
63
+ backupPath = `${input.targetPath}.${timestamp()}.bak`;
64
+ await copyFile(input.targetPath, backupPath);
65
+ }
66
+ const next = mergeProviderIntoConfigText(current, input.providerId, input.provider);
67
+ await writeFile(input.targetPath, next, "utf8");
68
+ return { targetPath: input.targetPath, backupPath };
69
+ }
70
+ async function configHasProvider(path, providerId) {
71
+ try {
72
+ const text = await readFile(path, "utf8");
73
+ const parsed = parse(text, undefined, { allowTrailingComma: true, disallowComments: false });
74
+ return isRecord(parsed?.provider) && Object.prototype.hasOwnProperty.call(parsed.provider, providerId);
75
+ }
76
+ catch {
77
+ return false;
78
+ }
79
+ }
80
+ async function fileExists(path) {
81
+ try {
82
+ return (await stat(path)).isFile();
83
+ }
84
+ catch {
85
+ return false;
86
+ }
87
+ }
88
+ async function isWritable(path) {
89
+ try {
90
+ await access(path, constants.W_OK);
91
+ return true;
92
+ }
93
+ catch {
94
+ return false;
95
+ }
96
+ }
97
+ function isRecord(value) {
98
+ return typeof value === "object" && value !== null && !Array.isArray(value);
99
+ }
100
+ function timestamp() {
101
+ return new Date().toISOString().replace(/[:.]/g, "-");
102
+ }
@@ -0,0 +1,61 @@
1
+ export function normalizeBaseUrl(baseURL) {
2
+ const normalized = baseURL.trim().replace(/\/+$/, "");
3
+ if (!normalized) {
4
+ throw new Error("API base URL is required");
5
+ }
6
+ try {
7
+ new URL(normalized);
8
+ }
9
+ catch {
10
+ throw new Error("API base URL must be a valid URL");
11
+ }
12
+ return normalized;
13
+ }
14
+ export async function validateAndFetchModels(input, fetchImpl = globalThis.fetch) {
15
+ const baseURL = normalizeBaseUrl(input.baseURL);
16
+ const apiKey = input.apiKey?.trim() ?? "";
17
+ const headers = {};
18
+ if (apiKey) {
19
+ headers.Authorization = `Bearer ${apiKey}`;
20
+ }
21
+ const response = await fetchImpl(`${baseURL}/models`, { headers });
22
+ if (!response.ok) {
23
+ throw new Error(`Provider returned ${response.status ?? "an error"} ${response.statusText ?? ""}`.trim());
24
+ }
25
+ const body = await response.json();
26
+ if (!isModelListResponse(body)) {
27
+ throw new Error("Expected /models to return an object with a data array");
28
+ }
29
+ const models = [...new Set(body.data.map((model) => model.id.trim()).filter(Boolean))];
30
+ if (models.length === 0) {
31
+ throw new Error("Provider returned no model ids");
32
+ }
33
+ return { baseURL, models };
34
+ }
35
+ export function buildProviderConfig(input) {
36
+ const baseURL = normalizeBaseUrl(input.baseURL);
37
+ const apiKey = input.apiKey?.trim();
38
+ const provider = {
39
+ npm: "@ai-sdk/openai-compatible",
40
+ name: input.providerName.trim(),
41
+ options: {
42
+ baseURL
43
+ },
44
+ models: Object.fromEntries(input.models.map((model) => [model, { name: model }]))
45
+ };
46
+ if (apiKey) {
47
+ provider.options.apiKey = apiKey;
48
+ }
49
+ return {
50
+ $schema: "https://opencode.ai/config.json",
51
+ provider: {
52
+ [input.providerId.trim()]: provider
53
+ }
54
+ };
55
+ }
56
+ function isModelListResponse(body) {
57
+ return (typeof body === "object" &&
58
+ body !== null &&
59
+ Array.isArray(body.data) &&
60
+ body.data.every((model) => typeof model === "object" && model !== null && typeof model.id === "string"));
61
+ }
@@ -0,0 +1,82 @@
1
+ import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { randomBytes } from "node:crypto";
5
+ import { parse } from "jsonc-parser";
6
+ export function getDefaultToolConfigPath(homeDir = homedir()) {
7
+ return join(homeDir, ".config", "model-provider-x", "config.jsonc");
8
+ }
9
+ export async function readToolConfig(path = getDefaultToolConfigPath()) {
10
+ if (!(await fileExists(path))) {
11
+ return createDefaultToolConfig();
12
+ }
13
+ const text = await readFile(path, "utf8");
14
+ const parsed = parse(text, undefined, { allowTrailingComma: true, disallowComments: false });
15
+ return normalizeToolConfig(parsed);
16
+ }
17
+ export async function writeToolConfig(path, config) {
18
+ await mkdir(dirname(path), { recursive: true });
19
+ await writeFile(path, `${JSON.stringify(normalizeToolConfig(config), null, 2)}\n`, "utf8");
20
+ }
21
+ export async function upsertProviderProfile(path, profile, proxy) {
22
+ const current = await readToolConfig(path);
23
+ const next = normalizeToolConfig({
24
+ ...current,
25
+ profiles: {
26
+ ...current.profiles,
27
+ [profile.id]: profile
28
+ },
29
+ proxy: {
30
+ ...current.proxy,
31
+ ...proxy
32
+ }
33
+ });
34
+ await writeToolConfig(path, next);
35
+ return next;
36
+ }
37
+ function createDefaultToolConfig() {
38
+ return {
39
+ profiles: {},
40
+ proxy: {
41
+ host: "127.0.0.1",
42
+ port: 4141,
43
+ authToken: `mpx-${randomBytes(18).toString("base64url")}`
44
+ }
45
+ };
46
+ }
47
+ function normalizeToolConfig(config) {
48
+ const fallback = createDefaultToolConfig();
49
+ const profiles = isRecord(config.profiles) ? config.profiles : {};
50
+ const proxy = isRecord(config.proxy) ? config.proxy : {};
51
+ return {
52
+ profiles: Object.fromEntries(Object.entries(profiles).map(([id, profile]) => {
53
+ const value = profile;
54
+ return [
55
+ id,
56
+ {
57
+ id: String(value.id ?? id),
58
+ name: String(value.name ?? id),
59
+ baseURL: String(value.baseURL ?? ""),
60
+ apiKey: value.apiKey ? String(value.apiKey) : undefined,
61
+ models: Array.isArray(value.models) ? value.models.map(String) : []
62
+ }
63
+ ];
64
+ })),
65
+ proxy: {
66
+ host: typeof proxy.host === "string" ? proxy.host : fallback.proxy.host,
67
+ port: typeof proxy.port === "number" ? proxy.port : fallback.proxy.port,
68
+ authToken: typeof proxy.authToken === "string" ? proxy.authToken : fallback.proxy.authToken
69
+ }
70
+ };
71
+ }
72
+ async function fileExists(path) {
73
+ try {
74
+ return (await stat(path)).isFile();
75
+ }
76
+ catch {
77
+ return false;
78
+ }
79
+ }
80
+ function isRecord(value) {
81
+ return typeof value === "object" && value !== null && !Array.isArray(value);
82
+ }
@@ -0,0 +1,196 @@
1
+ export function anthropicMessageToChatRequest(request) {
2
+ const messages = [];
3
+ if (request.system) {
4
+ messages.push({ role: "system", content: contentToText(request.system) });
5
+ }
6
+ for (const message of request.messages) {
7
+ messages.push(...anthropicMessageToChatMessages(message));
8
+ }
9
+ return pruneUndefined({
10
+ model: request.model,
11
+ messages,
12
+ max_tokens: request.max_tokens,
13
+ temperature: request.temperature,
14
+ top_p: request.top_p,
15
+ stop: request.stop_sequences,
16
+ stream: request.stream,
17
+ tools: request.tools?.map((tool) => ({
18
+ type: "function",
19
+ function: {
20
+ name: tool.name,
21
+ description: tool.description,
22
+ parameters: tool.input_schema
23
+ }
24
+ }))
25
+ });
26
+ }
27
+ export function chatCompletionToAnthropicMessage(response) {
28
+ const choice = response.choices[0];
29
+ const content = chatMessageContentToAnthropicBlocks(choice?.message);
30
+ return {
31
+ id: `msg_${response.id}`,
32
+ type: "message",
33
+ role: "assistant",
34
+ content,
35
+ model: response.model,
36
+ stop_reason: mapFinishReason(choice?.finish_reason),
37
+ stop_sequence: null,
38
+ usage: {
39
+ input_tokens: response.usage?.prompt_tokens ?? 0,
40
+ output_tokens: response.usage?.completion_tokens ?? 0
41
+ }
42
+ };
43
+ }
44
+ export function createAnthropicStreamEvents(chunks, fallbackModel) {
45
+ const first = chunks.find((chunk) => chunk.id || chunk.model);
46
+ const id = `msg_${first?.id ?? `stream_${Date.now()}`}`;
47
+ const model = first?.model ?? fallbackModel;
48
+ const events = [
49
+ {
50
+ event: "message_start",
51
+ data: {
52
+ type: "message_start",
53
+ message: {
54
+ id,
55
+ type: "message",
56
+ role: "assistant",
57
+ content: [],
58
+ model,
59
+ stop_reason: null,
60
+ stop_sequence: null,
61
+ usage: { input_tokens: 0, output_tokens: 0 }
62
+ }
63
+ }
64
+ }
65
+ ];
66
+ let blockOpen = false;
67
+ let finishReason;
68
+ for (const chunk of chunks) {
69
+ const choice = chunk.choices?.[0];
70
+ const text = choice?.delta?.content;
71
+ if (text) {
72
+ if (!blockOpen) {
73
+ blockOpen = true;
74
+ events.push({ event: "content_block_start", data: { type: "content_block_start", index: 0, content_block: { type: "text", text: "" } } });
75
+ }
76
+ events.push({ event: "content_block_delta", data: { type: "content_block_delta", index: 0, delta: { type: "text_delta", text } } });
77
+ }
78
+ if (choice?.finish_reason) {
79
+ finishReason = choice.finish_reason;
80
+ }
81
+ }
82
+ if (!blockOpen) {
83
+ events.push({ event: "content_block_start", data: { type: "content_block_start", index: 0, content_block: { type: "text", text: "" } } });
84
+ }
85
+ events.push({ event: "content_block_stop", data: { type: "content_block_stop", index: 0 } });
86
+ events.push({
87
+ event: "message_delta",
88
+ data: { type: "message_delta", delta: { stop_reason: mapFinishReason(finishReason), stop_sequence: null }, usage: { output_tokens: 0 } }
89
+ });
90
+ events.push({ event: "message_stop", data: { type: "message_stop" } });
91
+ return events;
92
+ }
93
+ export function formatSseEvent(event) {
94
+ return `event: ${event.event}\ndata: ${JSON.stringify(event.data)}\n\n`;
95
+ }
96
+ function anthropicMessageToChatMessages(message) {
97
+ const blocks = normalizeContent(message.content);
98
+ const toolResultBlocks = blocks.filter(isToolResultBlock);
99
+ if (toolResultBlocks.length > 0) {
100
+ return toolResultBlocks.map((block) => ({
101
+ role: "tool",
102
+ tool_call_id: String(block.tool_use_id),
103
+ content: contentToText(block.content)
104
+ }));
105
+ }
106
+ const toolUseBlocks = blocks.filter(isToolUseBlock);
107
+ const text = blocks
108
+ .filter(isTextBlock)
109
+ .map((block) => block.text)
110
+ .join("\n");
111
+ for (const block of blocks) {
112
+ if (block.type !== "text" && block.type !== "tool_use") {
113
+ throw new Error(`Unsupported Anthropic content block type: ${block.type}`);
114
+ }
115
+ }
116
+ if (message.role === "assistant") {
117
+ return [
118
+ {
119
+ role: "assistant",
120
+ content: text,
121
+ tool_calls: toolUseBlocks.map((block) => ({
122
+ id: String(block.id),
123
+ type: "function",
124
+ function: {
125
+ name: String(block.name),
126
+ arguments: JSON.stringify(block.input ?? {})
127
+ }
128
+ }))
129
+ }
130
+ ];
131
+ }
132
+ return [{ role: "user", content: text }];
133
+ }
134
+ function chatMessageContentToAnthropicBlocks(message) {
135
+ const blocks = [];
136
+ if (message?.content) {
137
+ blocks.push({ type: "text", text: message.content });
138
+ }
139
+ for (const toolCall of message?.tool_calls ?? []) {
140
+ blocks.push({
141
+ type: "tool_use",
142
+ id: toolCall.id,
143
+ name: toolCall.function.name,
144
+ input: parseToolArguments(toolCall.function.arguments)
145
+ });
146
+ }
147
+ return blocks.length > 0 ? blocks : [{ type: "text", text: "" }];
148
+ }
149
+ function contentToText(content) {
150
+ if (!content) {
151
+ return "";
152
+ }
153
+ return normalizeContent(content)
154
+ .map((block) => {
155
+ if (block.type === "text") {
156
+ return String(block.text);
157
+ }
158
+ if (isToolResultBlock(block)) {
159
+ return contentToText(block.content);
160
+ }
161
+ throw new Error(`Unsupported Anthropic content block type: ${block.type}`);
162
+ })
163
+ .join("\n");
164
+ }
165
+ function normalizeContent(content) {
166
+ return typeof content === "string" ? [{ type: "text", text: content }] : content;
167
+ }
168
+ function isTextBlock(block) {
169
+ return block.type === "text";
170
+ }
171
+ function isToolUseBlock(block) {
172
+ return block.type === "tool_use";
173
+ }
174
+ function isToolResultBlock(block) {
175
+ return block.type === "tool_result";
176
+ }
177
+ function parseToolArguments(value) {
178
+ try {
179
+ return JSON.parse(value);
180
+ }
181
+ catch {
182
+ return {};
183
+ }
184
+ }
185
+ function mapFinishReason(reason) {
186
+ if (reason === "length") {
187
+ return "max_tokens";
188
+ }
189
+ if (reason === "tool_calls") {
190
+ return "tool_use";
191
+ }
192
+ return "end_turn";
193
+ }
194
+ function pruneUndefined(value) {
195
+ return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
196
+ }