@kontextmind/kxm 0.7.17 → 0.7.18

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 (30) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/package.json +8 -6
  3. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  4. package/plugins/kxm/dist/cli.js +43 -39
  5. package/plugins/kxm/dist/core.js +3 -2
  6. package/plugins/kxm/dist/extension.js +34472 -11328
  7. package/plugins/kxm/dist/mcp-server.js +1 -1
  8. package/plugins/kxm/dist/runtime.js +43 -39
  9. package/plugins/kxm/dist/server.js +43 -39
  10. package/plugins/kxm/dist/vnext-runtime-supervisor.js +40 -37
  11. package/plugins/kxm/package.json +4 -1
  12. package/plugins/kxm/src/extension.ts +9 -0
  13. package/plugins/kxm/src/mcp-server.ts +1 -1
  14. package/plugins/kxm/src/providers/claude-bridge/LICENSE +21 -0
  15. package/plugins/kxm/src/providers/claude-bridge/README.md +18 -0
  16. package/plugins/kxm/src/providers/claude-bridge/config.ts +53 -0
  17. package/plugins/kxm/src/providers/claude-bridge/convert.ts +180 -0
  18. package/plugins/kxm/src/providers/claude-bridge/index.ts +28 -0
  19. package/plugins/kxm/src/providers/claude-bridge/mcp-server.ts +59 -0
  20. package/plugins/kxm/src/providers/claude-bridge/models.ts +166 -0
  21. package/plugins/kxm/src/providers/claude-bridge/pi-compat.ts +215 -0
  22. package/plugins/kxm/src/providers/claude-bridge/prompt-stream.ts +89 -0
  23. package/plugins/kxm/src/providers/claude-bridge/provider.ts +286 -0
  24. package/plugins/kxm/src/providers/claude-bridge/query-state.ts +72 -0
  25. package/plugins/kxm/src/providers/claude-bridge/redact.ts +26 -0
  26. package/plugins/kxm/src/providers/claude-bridge/register.ts +124 -0
  27. package/plugins/kxm/src/providers/claude-bridge/stream.ts +390 -0
  28. package/plugins/kxm/src/providers/claude-bridge/tools.ts +140 -0
  29. package/plugins/kxm/src/redact.ts +3 -2
  30. package/scripts/build-runtime.mjs +5 -1
@@ -0,0 +1,180 @@
1
+ // @ts-nocheck — vendored snapshot; host tsconfig is stricter than upstream.
2
+ import type { ImageContent, Message, TextContent, UserMessage } from "./pi-compat.ts";
3
+ import { PROVIDER_ID } from "./models.ts";
4
+ import { MCP_TOOL_PREFIX, mapPiToolNameToSdk, sanitizeToolId } from "./tools.ts";
5
+
6
+ export { PROVIDER_ID };
7
+
8
+ export function messageContentToText(
9
+ content: string | Array<{ type: string; text?: string; data?: string; mimeType?: string }>,
10
+ ): string {
11
+ if (typeof content === "string") return content;
12
+ if (!Array.isArray(content)) return "";
13
+ const parts = [];
14
+ let hasText = false;
15
+ for (const block of content) {
16
+ if (block.type === "text" && block.text) {
17
+ parts.push(block.text);
18
+ hasText = true;
19
+ } else if (block.type !== "text" && block.type !== "image") {
20
+ parts.push(`[${block.type}]`);
21
+ }
22
+ }
23
+ return hasText ? parts.join("\n") : "";
24
+ }
25
+
26
+ function toolResultContent(
27
+ content: string | Array<{ type: string; text?: string; data?: string; mimeType?: string }>,
28
+ ): string | Array<Record<string, unknown>> {
29
+ if (typeof content === "string" || !Array.isArray(content)) return messageContentToText(content) || "";
30
+ const images = content.filter((b) => b.type === "image" && b.data && b.mimeType);
31
+ if (!images.length) return messageContentToText(content) || "";
32
+ const blocks: Array<Record<string, unknown>> = [];
33
+ for (const block of content) {
34
+ if (block.type === "text" && block.text) blocks.push({ type: "text", text: block.text });
35
+ else if (block.type === "image" && block.data && block.mimeType) {
36
+ blocks.push({ type: "image", source: { type: "base64", media_type: block.mimeType, data: block.data } });
37
+ } else if (block.type !== "text" && block.type !== "image") {
38
+ blocks.push({ type: "text", text: `[${block.type}]` });
39
+ }
40
+ }
41
+ return blocks;
42
+ }
43
+
44
+ export type DroppedContent = {
45
+ thinking: number;
46
+ abortedTurns: number;
47
+ providers: Set<string>;
48
+ other: Map<string, number>;
49
+ };
50
+
51
+ export type AnthropicMessage = { role: string; content: unknown };
52
+
53
+ export function convertPiMessages(
54
+ messages: Message[],
55
+ customToolNameToSdk?: Map<string, string>,
56
+ ): { anthropicMessages: AnthropicMessage[]; sanitizedIds: Map<string, string>; dropped: DroppedContent } {
57
+ const anthropicMessages: AnthropicMessage[] = [];
58
+ const sanitizedIds = new Map<string, string>();
59
+ const dropped: DroppedContent = { thinking: 0, abortedTurns: 0, providers: new Set(), other: new Map() };
60
+ let turnResults: { role: "user"; content: Array<Record<string, unknown>> } | null = null;
61
+ let turnAssistantIdx: number | null = null;
62
+
63
+ for (const msg of messages) {
64
+ if (msg.role === "user") {
65
+ if (typeof msg.content === "string") {
66
+ anthropicMessages.push({ role: "user", content: msg.content || "[empty]" });
67
+ } else if (Array.isArray(msg.content)) {
68
+ const parts = [];
69
+ for (const block of msg.content) {
70
+ if (block.type === "text" && block.text) parts.push({ type: "text", text: block.text });
71
+ else if (block.type === "image" && block.data && block.mimeType) {
72
+ parts.push({ type: "image", source: { type: "base64", media_type: block.mimeType, data: block.data } });
73
+ }
74
+ }
75
+ anthropicMessages.push({ role: "user", content: parts.length ? parts : "[image]" });
76
+ } else {
77
+ anthropicMessages.push({ role: "user", content: "[empty]" });
78
+ }
79
+ } else if (msg.role === "assistant") {
80
+ const content = Array.isArray(msg.content) ? msg.content : [];
81
+ const blocks = [];
82
+ for (const block of content) {
83
+ if (block.type === "text" && block.text) {
84
+ blocks.push({ type: "text", text: block.text });
85
+ } else if (block.type === "thinking") {
86
+ const sig = block.thinkingSignature;
87
+ if (msg.provider === PROVIDER_ID && sig) {
88
+ blocks.push({ type: "thinking", thinking: block.thinking ?? "", signature: sig });
89
+ } else {
90
+ dropped.thinking++;
91
+ dropped.providers.add(msg.provider ?? "unknown");
92
+ }
93
+ } else if (block.type === "toolCall") {
94
+ const toolName = mapPiToolNameToSdk(block.name, customToolNameToSdk);
95
+ blocks.push({
96
+ type: "tool_use",
97
+ id: sanitizeToolId(block.id, sanitizedIds),
98
+ name: toolName,
99
+ input: block.arguments ?? {},
100
+ });
101
+ } else {
102
+ dropped.other.set(block.type, (dropped.other.get(block.type) ?? 0) + 1);
103
+ }
104
+ }
105
+ if (!content.length) {
106
+ dropped.abortedTurns++;
107
+ continue;
108
+ }
109
+ if (!blocks.length) blocks.push({ type: "text", text: "[incompatible content omitted]" });
110
+ turnResults = null;
111
+ turnAssistantIdx = anthropicMessages.length;
112
+ anthropicMessages.push({ role: "assistant", content: blocks });
113
+ } else if (msg.role === "toolResult") {
114
+ const block = {
115
+ type: "tool_result",
116
+ tool_use_id: sanitizeToolId(msg.toolCallId, sanitizedIds),
117
+ content: toolResultContent(msg.content),
118
+ is_error: msg.isError,
119
+ };
120
+ if (turnResults) {
121
+ turnResults.content.push(block);
122
+ } else {
123
+ turnResults = { role: "user", content: [block] };
124
+ anthropicMessages.splice(turnAssistantIdx === null ? anthropicMessages.length : turnAssistantIdx + 1, 0, turnResults);
125
+ }
126
+ }
127
+ }
128
+
129
+ return { anthropicMessages, sanitizedIds, dropped };
130
+ }
131
+
132
+ export function turnStart(messages: Message[]): number {
133
+ let i = messages.length;
134
+ while (i > 0 && messages[i - 1].role === "user") i--;
135
+ return i;
136
+ }
137
+
138
+ export function extractUserPrompt(messages: Message[]): string | null {
139
+ const turn = messages.slice(turnStart(messages)) as UserMessage[];
140
+ if (turn.length === 0) return null;
141
+ return turn
142
+ .map((m) => (typeof m.content === "string" ? m.content : messageContentToText(m.content)))
143
+ .filter((text) => text)
144
+ .join("\n");
145
+ }
146
+
147
+ export function extractUserPromptBlocks(
148
+ messages: Message[],
149
+ ): Array<{ type: "text"; text: string } | { type: "image"; source: { type: "base64"; media_type: string; data: string } }> | null {
150
+ const turn = messages.slice(turnStart(messages)) as UserMessage[];
151
+ if (turn.length === 0) return null;
152
+ let hasImage = false;
153
+ const blocks = [];
154
+ for (const message of turn) {
155
+ const content: Array<TextContent | ImageContent> =
156
+ typeof message.content === "string" ? [{ type: "text", text: message.content }] : message.content;
157
+ if (!Array.isArray(content)) {
158
+ throw new Error(
159
+ `extractUserPromptBlocks: user message content must be a string or block array, got ${typeof content}`,
160
+ );
161
+ }
162
+ for (const block of content) {
163
+ if (block.type === "text" && block.text) {
164
+ blocks.push({ type: "text", text: block.text });
165
+ } else if (block.type === "image") {
166
+ if (!block.data || !block.mimeType) continue;
167
+ hasImage = true;
168
+ blocks.push({
169
+ type: "image",
170
+ source: { type: "base64" as const, media_type: block.mimeType, data: block.data },
171
+ });
172
+ }
173
+ }
174
+ }
175
+ return hasImage ? blocks : null;
176
+ }
177
+
178
+ export function alreadySdkToolName(name: string): boolean {
179
+ return name.toLowerCase().startsWith(MCP_TOOL_PREFIX);
180
+ }
@@ -0,0 +1,28 @@
1
+ export {
2
+ CLAUDE_BRIDGE_API,
3
+ CLAUDE_BRIDGE_MODELS,
4
+ MODEL_IDS_IN_ORDER,
5
+ PROVIDER_ID,
6
+ PROVIDER_NAME,
7
+ applyLongContext,
8
+ claudeCodeModelId,
9
+ registeredClaudeBridgeModels,
10
+ resolveClaudeCodeRuntimeModel,
11
+ resolveEffort,
12
+ resolveModel,
13
+ REASONING_TO_EFFORT,
14
+ } from "./models.ts";
15
+ export {
16
+ CLAUDE_BRIDGE_DOUBLE_REGISTRATION_WARNING,
17
+ CLAUDE_BRIDGE_HOST_PROBE_METHODS,
18
+ STANDALONE_CLAUDE_BRIDGE_TOOLS,
19
+ claudeBridgeRegistrationNotice,
20
+ claudeBridgeStandaloneToolsPresent,
21
+ registerClaudeBridgeProvider,
22
+ shouldSkipClaudeBridgeRegistration,
23
+ type ClaudeBridgeRegistration,
24
+ } from "./register.ts";
25
+ export { consumeQuery, processAssistantMessage, processStreamEvent } from "./stream.ts";
26
+ export { streamClaudeBridge, resetClaudeBridgeSession } from "./provider.ts";
27
+ export { convertPiMessages } from "./convert.ts";
28
+ export { looksLikeClaudeBridgeSecret, redactClaudeBridgeSecrets } from "./redact.ts";
@@ -0,0 +1,59 @@
1
+ // @ts-nocheck — vendored snapshot; host tsconfig is stricter than upstream.
2
+ // In-process MCP server that exposes pi tools to Claude Code.
3
+ // From pi-claude-bridge 0.7.0. Not a user-facing MCP product.
4
+
5
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
7
+ import type { McpResult } from "./tools.ts";
8
+
9
+ const TOOL_USE_ID_META = "claudecode/toolUseId";
10
+
11
+ export interface McpToolDef {
12
+ name: string;
13
+ description: string;
14
+ inputSchema: unknown;
15
+ handler: (toolCallId: string) => Promise<McpResult>;
16
+ }
17
+
18
+ export function assertObjectSchema(tool: McpToolDef): void {
19
+ const schema = tool.inputSchema as Record<string, unknown> | undefined;
20
+ if (!schema || schema.type !== "object") {
21
+ throw new Error(`${tool.name}: MCP tool parameters must be an object schema, got ${JSON.stringify(schema)}`);
22
+ }
23
+ }
24
+
25
+ export function createToolServer(name: string, tools: McpToolDef[]) {
26
+ const server = new McpServer({ name, version: "1.0.0" }, { capabilities: { tools: {} } });
27
+ const byName = new Map(tools.map((tool) => [tool.name, tool]));
28
+ for (const tool of tools) assertObjectSchema(tool);
29
+
30
+ server.server.setRequestHandler(ListToolsRequestSchema, () => ({
31
+ tools: tools.map((tool) => ({
32
+ name: tool.name,
33
+ description: tool.description,
34
+ inputSchema: tool.inputSchema as Record<string, unknown>,
35
+ })),
36
+ }));
37
+
38
+ server.server.setRequestHandler(CallToolRequestSchema, async (request) => dispatchToolCall(byName, request));
39
+
40
+ return { type: "sdk" as const, name, instance: server };
41
+ }
42
+
43
+ export async function dispatchToolCall(
44
+ byName: Map<string, McpToolDef>,
45
+ request: { params: { name: string; _meta?: Record<string, unknown> } },
46
+ ) {
47
+ const tool = byName.get(request.params.name);
48
+ if (!tool) throw new Error(`Unknown tool: ${request.params.name}`);
49
+ const toolCallId = request.params._meta?.[TOOL_USE_ID_META];
50
+ if (typeof toolCallId !== "string") {
51
+ throw new Error(
52
+ `${tool.name}: tools/call is missing _meta["${TOOL_USE_ID_META}"] — cannot pair the result with its tool call`,
53
+ );
54
+ }
55
+ const { content, isError } = await tool.handler(toolCallId);
56
+ return { content, isError };
57
+ }
58
+
59
+ export { TOOL_USE_ID_META };
@@ -0,0 +1,166 @@
1
+ // Canonical selection + display order for the model picker.
2
+ // Extracted from pi-claude-bridge 0.7.0; catalog is static (no pi-ai getModels).
3
+
4
+ import type { Model, ProviderModelConfig, ThinkingLevel, ThinkingLevelMap } from "./pi-compat.ts";
5
+
6
+ export const PROVIDER_ID = "claude-bridge";
7
+ export const PROVIDER_NAME = "Claude Bridge";
8
+ export const CLAUDE_BRIDGE_API = "claude-bridge";
9
+
10
+ export const MODEL_IDS_IN_ORDER = [
11
+ "claude-fable-5-1",
12
+ "claude-fable-5",
13
+ "claude-opus-5",
14
+ "claude-opus-4-8",
15
+ "claude-opus-4-7",
16
+ "claude-opus-4-6",
17
+ "claude-sonnet-5",
18
+ "claude-sonnet-4-6",
19
+ "claude-haiku-4-5",
20
+ ] as const;
21
+
22
+ export type ClaudeBridgeModelId = (typeof MODEL_IDS_IN_ORDER)[number];
23
+
24
+ const DISPLAY_NAMES: Record<ClaudeBridgeModelId, string> = {
25
+ "claude-fable-5-1": "Claude Fable 5.1",
26
+ "claude-fable-5": "Claude Fable 5",
27
+ "claude-opus-5": "Claude Opus 5",
28
+ "claude-opus-4-8": "Claude Opus 4.8",
29
+ "claude-opus-4-7": "Claude Opus 4.7",
30
+ "claude-opus-4-6": "Claude Opus 4.6",
31
+ "claude-sonnet-5": "Claude Sonnet 5",
32
+ "claude-sonnet-4-6": "Claude Sonnet 4.6",
33
+ "claude-haiku-4-5": "Claude Haiku 4.5",
34
+ };
35
+
36
+ /** Per-model effort overrides (pi-ai 0.72+ shipped these for later Opus). */
37
+ const THINKING_LEVEL_MAPS: Partial<Record<ClaudeBridgeModelId, ThinkingLevelMap>> = {
38
+ "claude-opus-4-8": { xhigh: "xhigh", max: "max" },
39
+ "claude-opus-4-7": { xhigh: "xhigh", max: "max" },
40
+ "claude-opus-5": { xhigh: "xhigh", max: "max" },
41
+ };
42
+
43
+ export type LongContextSettings = {
44
+ plan: "pro" | "max";
45
+ longContextExtraUsage: boolean;
46
+ };
47
+
48
+ export type ClaudeCodeRuntimeModel = {
49
+ cliModelId: string;
50
+ contextWindow: number;
51
+ };
52
+
53
+ const TWO_HUNDRED_K_CONTEXT = 200_000;
54
+ const ONE_M_CONTEXT = 1_000_000;
55
+
56
+ export const ZERO_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } as const;
57
+
58
+ export const DEFAULT_LONG_CONTEXT: LongContextSettings = {
59
+ plan: "pro",
60
+ longContextExtraUsage: false,
61
+ };
62
+
63
+ /** Pi reasoning levels → Claude Agent SDK effort levels. */
64
+ export const REASONING_TO_EFFORT: Record<string, string> = {
65
+ minimal: "low",
66
+ low: "low",
67
+ medium: "medium",
68
+ high: "high",
69
+ xhigh: "max",
70
+ };
71
+
72
+ export function resolveEffort(
73
+ reasoning: ThinkingLevel | undefined,
74
+ thinkingLevelMap?: ThinkingLevelMap,
75
+ ): string | undefined {
76
+ if (!reasoning) return undefined;
77
+ const mapped = thinkingLevelMap?.[reasoning];
78
+ if (typeof mapped === "string") return mapped;
79
+ return REASONING_TO_EFFORT[reasoning];
80
+ }
81
+
82
+ export function resolveClaudeCodeRuntimeModel(
83
+ modelId: string,
84
+ settings: LongContextSettings,
85
+ ): ClaudeCodeRuntimeModel {
86
+ switch (modelId) {
87
+ case "claude-opus-5":
88
+ return { cliModelId: "claude-opus-5[1m]", contextWindow: ONE_M_CONTEXT };
89
+ case "claude-opus-4-8":
90
+ return { cliModelId: "claude-opus-4-8[1m]", contextWindow: ONE_M_CONTEXT };
91
+ case "claude-opus-4-7":
92
+ return { cliModelId: "claude-opus-4-7", contextWindow: ONE_M_CONTEXT };
93
+ case "claude-opus-4-6": {
94
+ const useOneM = settings.plan === "max" || settings.longContextExtraUsage;
95
+ return {
96
+ cliModelId: useOneM ? "claude-opus-4-6[1m]" : "claude-opus-4-6",
97
+ contextWindow: useOneM ? ONE_M_CONTEXT : TWO_HUNDRED_K_CONTEXT,
98
+ };
99
+ }
100
+ case "claude-fable-5-1":
101
+ return { cliModelId: "claude-fable-5-1[1m]", contextWindow: ONE_M_CONTEXT };
102
+ case "claude-fable-5":
103
+ return { cliModelId: "claude-fable-5[1m]", contextWindow: ONE_M_CONTEXT };
104
+ case "claude-sonnet-5":
105
+ return { cliModelId: "claude-sonnet-5[1m]", contextWindow: ONE_M_CONTEXT };
106
+ case "claude-sonnet-4-6":
107
+ return {
108
+ cliModelId: settings.longContextExtraUsage ? "claude-sonnet-4-6[1m]" : "claude-sonnet-4-6",
109
+ contextWindow: settings.longContextExtraUsage ? ONE_M_CONTEXT : TWO_HUNDRED_K_CONTEXT,
110
+ };
111
+ case "claude-haiku-4-5":
112
+ return { cliModelId: "claude-haiku-4-5", contextWindow: TWO_HUNDRED_K_CONTEXT };
113
+ default:
114
+ return { cliModelId: modelId, contextWindow: TWO_HUNDRED_K_CONTEXT };
115
+ }
116
+ }
117
+
118
+ export function claudeCodeModelId(model: { id: string }, settings: LongContextSettings): string {
119
+ return resolveClaudeCodeRuntimeModel(model.id, settings).cliModelId;
120
+ }
121
+
122
+ export function resolveModel<T extends { id: string }>(models: T[], input: string): T | undefined {
123
+ const lower = input.toLowerCase();
124
+ return models.find((m) => m.id === lower || m.id.includes(lower));
125
+ }
126
+
127
+ export function applyLongContext<T extends { id: string; name: string; contextWindow?: number | null }>(
128
+ models: T[],
129
+ settings: LongContextSettings,
130
+ ): T[] {
131
+ return models.map((m) => {
132
+ const { contextWindow } = resolveClaudeCodeRuntimeModel(m.id, settings);
133
+ const name =
134
+ contextWindow > TWO_HUNDRED_K_CONTEXT && !/\b1M\b/i.test(m.name) ? `${m.name} 1M` : m.name;
135
+ return contextWindow === m.contextWindow && name === m.name ? m : { ...m, contextWindow, name };
136
+ });
137
+ }
138
+
139
+ function staticModel(id: ClaudeBridgeModelId): ProviderModelConfig {
140
+ const runtime = resolveClaudeCodeRuntimeModel(id, DEFAULT_LONG_CONTEXT);
141
+ const name = DISPLAY_NAMES[id];
142
+ const thinkingLevelMap = THINKING_LEVEL_MAPS[id];
143
+ return {
144
+ id,
145
+ name: runtime.contextWindow > TWO_HUNDRED_K_CONTEXT && !/\b1M\b/i.test(name) ? `${name} 1M` : name,
146
+ reasoning: true,
147
+ input: ["text", "image"],
148
+ cost: { ...ZERO_COST },
149
+ contextWindow: runtime.contextWindow,
150
+ maxTokens: 32_000,
151
+ ...(thinkingLevelMap ? { thinkingLevelMap } : {}),
152
+ };
153
+ }
154
+
155
+ export const CLAUDE_BRIDGE_MODELS: ProviderModelConfig[] = MODEL_IDS_IN_ORDER.map(staticModel);
156
+
157
+ export function registeredClaudeBridgeModels(
158
+ settings: LongContextSettings = DEFAULT_LONG_CONTEXT,
159
+ ): Model[] {
160
+ return applyLongContext(CLAUDE_BRIDGE_MODELS, settings).map((model) => ({
161
+ ...model,
162
+ api: CLAUDE_BRIDGE_API,
163
+ provider: PROVIDER_ID,
164
+ baseUrl: PROVIDER_ID,
165
+ }));
166
+ }
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Structural Pi stream types used by the vendored claude-bridge provider.
3
+ * No @earendil-works/pi-ai import (nested copy is forbidden).
4
+ */
5
+
6
+ export type Api = string;
7
+ export type ProviderId = string;
8
+ export type ThinkingLevel = "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
9
+ export type ModelThinkingLevel = "off" | ThinkingLevel;
10
+ export type ThinkingLevelMap = Partial<Record<ModelThinkingLevel, string | null>>;
11
+
12
+ export type ModelCostRates = {
13
+ input: number;
14
+ output: number;
15
+ cacheRead: number;
16
+ cacheWrite: number;
17
+ };
18
+
19
+ export type ModelCost = ModelCostRates;
20
+
21
+ export type ProviderModelConfig = {
22
+ id: string;
23
+ name: string;
24
+ api?: Api;
25
+ baseUrl?: string;
26
+ reasoning: boolean;
27
+ thinkingLevelMap?: ThinkingLevelMap;
28
+ input: Array<"text" | "image">;
29
+ cost: ModelCost;
30
+ contextWindow: number;
31
+ maxTokens: number;
32
+ };
33
+
34
+ export type Model<TApi extends Api = Api> = ProviderModelConfig & {
35
+ api: TApi;
36
+ provider: ProviderId;
37
+ baseUrl: string;
38
+ };
39
+
40
+ export type TextContent = {
41
+ type: "text";
42
+ text: string;
43
+ textSignature?: string;
44
+ };
45
+
46
+ export type ThinkingContent = {
47
+ type: "thinking";
48
+ thinking: string;
49
+ thinkingSignature?: string;
50
+ redacted?: boolean;
51
+ };
52
+
53
+ export type ImageContent = {
54
+ type: "image";
55
+ data: string;
56
+ mimeType: string;
57
+ };
58
+
59
+ export type ToolCall = {
60
+ type: "toolCall";
61
+ id: string;
62
+ name: string;
63
+ arguments: Record<string, unknown>;
64
+ thoughtSignature?: string;
65
+ };
66
+
67
+ export type Usage = {
68
+ input: number;
69
+ output: number;
70
+ cacheRead: number;
71
+ cacheWrite: number;
72
+ reasoning?: number;
73
+ totalTokens: number;
74
+ cost: {
75
+ input: number;
76
+ output: number;
77
+ cacheRead: number;
78
+ cacheWrite: number;
79
+ total: number;
80
+ };
81
+ };
82
+
83
+ export type StopReason = "pending" | "stop" | "length" | "toolUse" | "error" | "aborted" | "deferred";
84
+
85
+ export type UserMessage = {
86
+ role: "user";
87
+ content: string | Array<TextContent | ImageContent>;
88
+ timestamp: number;
89
+ };
90
+
91
+ export type AssistantMessage = {
92
+ role: "assistant";
93
+ content: Array<TextContent | ThinkingContent | ToolCall>;
94
+ api: Api;
95
+ provider: ProviderId;
96
+ model: string;
97
+ usage: Usage;
98
+ stopReason: StopReason;
99
+ errorMessage?: string;
100
+ timestamp: number;
101
+ };
102
+
103
+ export type ToolResultMessage = {
104
+ role: "toolResult";
105
+ toolCallId: string;
106
+ toolName?: string;
107
+ content: string | Array<TextContent | ImageContent>;
108
+ isError?: boolean;
109
+ timestamp?: number;
110
+ };
111
+
112
+ export type Message = UserMessage | AssistantMessage | ToolResultMessage;
113
+
114
+ export type Tool = {
115
+ name: string;
116
+ description: string;
117
+ parameters: unknown;
118
+ };
119
+
120
+ export type Context = {
121
+ systemPrompt?: string;
122
+ messages: Message[];
123
+ tools?: Tool[];
124
+ };
125
+
126
+ export type AssistantMessageEvent =
127
+ | { type: "start"; partial: AssistantMessage }
128
+ | { type: "text_start"; contentIndex: number; partial: AssistantMessage }
129
+ | { type: "text_delta"; contentIndex: number; delta: string; partial: AssistantMessage }
130
+ | { type: "text_end"; contentIndex: number; content: string; partial: AssistantMessage }
131
+ | { type: "thinking_start"; contentIndex: number; partial: AssistantMessage }
132
+ | { type: "thinking_delta"; contentIndex: number; delta: string; partial: AssistantMessage }
133
+ | { type: "thinking_end"; contentIndex: number; content: string; partial: AssistantMessage }
134
+ | { type: "toolcall_start"; contentIndex: number; partial: AssistantMessage }
135
+ | { type: "toolcall_delta"; contentIndex: number; delta: string; partial: AssistantMessage }
136
+ | { type: "toolcall_end"; contentIndex: number; toolCall: ToolCall; partial: AssistantMessage }
137
+ | { type: "done"; reason: StopReason; message: AssistantMessage }
138
+ | { type: "error"; reason: StopReason; error: AssistantMessage };
139
+
140
+ export class AssistantMessageEventStream {
141
+ queue: AssistantMessageEvent[] = [];
142
+ waiting: Array<(result: { value?: AssistantMessageEvent; done: boolean }) => void> = [];
143
+ done = false;
144
+ finalResultPromise: Promise<AssistantMessage>;
145
+ private resolveFinalResult!: (message: AssistantMessage) => void;
146
+
147
+ constructor() {
148
+ this.finalResultPromise = new Promise((resolve) => {
149
+ this.resolveFinalResult = resolve;
150
+ });
151
+ }
152
+
153
+ push(event: AssistantMessageEvent): void {
154
+ if (this.done) return;
155
+ if (event.type === "done") {
156
+ this.done = true;
157
+ this.resolveFinalResult(event.message);
158
+ } else if (event.type === "error") {
159
+ this.done = true;
160
+ this.resolveFinalResult(event.error);
161
+ }
162
+ const waiter = this.waiting.shift();
163
+ if (waiter) waiter({ value: event, done: false });
164
+ else this.queue.push(event);
165
+ }
166
+
167
+ end(result?: AssistantMessage): void {
168
+ this.done = true;
169
+ if (result !== undefined) this.resolveFinalResult(result);
170
+ while (this.waiting.length > 0) {
171
+ this.waiting.shift()?.({ done: true });
172
+ }
173
+ }
174
+
175
+ async *[Symbol.asyncIterator](): AsyncGenerator<AssistantMessageEvent> {
176
+ while (true) {
177
+ if (this.queue.length > 0) {
178
+ yield this.queue.shift()!;
179
+ } else if (this.done) {
180
+ return;
181
+ } else {
182
+ const next = await new Promise<{ value?: AssistantMessageEvent; done: boolean }>((resolve) => {
183
+ this.waiting.push(resolve);
184
+ });
185
+ if (next.done) return;
186
+ if (next.value) yield next.value;
187
+ }
188
+ }
189
+ }
190
+
191
+ result(): Promise<AssistantMessage> {
192
+ return this.finalResultPromise;
193
+ }
194
+ }
195
+
196
+ export function createAssistantMessageEventStream(): AssistantMessageEventStream {
197
+ return new AssistantMessageEventStream();
198
+ }
199
+
200
+ export function emptyUsage(): Usage {
201
+ return {
202
+ input: 0,
203
+ output: 0,
204
+ cacheRead: 0,
205
+ cacheWrite: 0,
206
+ totalTokens: 0,
207
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
208
+ };
209
+ }
210
+
211
+ export type SimpleStreamOptions = {
212
+ signal?: AbortSignal;
213
+ cwd?: string;
214
+ reasoning?: ThinkingLevel;
215
+ };