pi-codebuddy-sdk 0.1.13 → 0.1.14
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/package.json +1 -1
- package/src/index.ts +90 -86
- package/src/provider-debug.patch +1 -0
- package/src/provider.ts +167 -194
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,97 +1,101 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* pi-codebuddy-sdk — CodeBuddy Agent SDK as a Pi provider.
|
|
3
|
+
*
|
|
4
|
+
* Architecture follows pi-claude-bridge:
|
|
5
|
+
* - Stateless: one sdk.query() per Pi turn
|
|
6
|
+
* - No Pi systemPrompt forwarding — CodeBuddy uses its own default
|
|
7
|
+
* - No MCP tool bridge — CodeBuddy SDK tools are used natively
|
|
8
|
+
* - Only the latest user message is forwarded (Pi manages conversation)
|
|
9
|
+
*
|
|
10
|
+
* Two files: index.ts (registration + model discovery), provider.ts (streaming).
|
|
11
|
+
*/
|
|
12
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import type { ProviderApi, SimpleStreamOptions } from "@earendil-works/pi-ai/compat";
|
|
14
|
+
import { streamCodebuddy } from "./provider.js";
|
|
2
15
|
|
|
3
|
-
//
|
|
4
|
-
// only {id, name} without capability metadata.
|
|
5
|
-
const THINKING_PATTERNS = [/claude/i, /gemini/i, /gpt-5/i];
|
|
6
|
-
const IMAGE_PATTERNS = [/claude/i, /gemini/i, /gpt/i];
|
|
7
|
-
// ponytail: rough context window estimates by model family
|
|
8
|
-
const CONTEXT_ESTIMATES: Record<string, [number, number]> = {
|
|
9
|
-
// [contextWindow, maxTokens]
|
|
10
|
-
claude: [200000, 8192],
|
|
11
|
-
gemini: [1048576, 8192],
|
|
12
|
-
gpt: [200000, 16384],
|
|
13
|
-
deepseek: [131072, 8192],
|
|
14
|
-
glm: [131072, 8192],
|
|
15
|
-
minimax: [131072, 8192],
|
|
16
|
-
kimi: [131072, 8192],
|
|
17
|
-
hy: [131072, 8192], // Hunyuan
|
|
18
|
-
};
|
|
16
|
+
// ── provider registration ──
|
|
19
17
|
|
|
20
|
-
function
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
? CONTEXT_ESTIMATES[family]
|
|
30
|
-
: [128000, 8192];
|
|
31
|
-
|
|
32
|
-
return {
|
|
33
|
-
reasoning,
|
|
34
|
-
input: (hasImage ? ["text", "image"] : ["text"]) as ("text" | "image")[],
|
|
35
|
-
contextWindow,
|
|
36
|
-
maxTokens,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
18
|
+
export default function activate(api: ExtensionAPI) {
|
|
19
|
+
api.registerProvider("codebuddy", {
|
|
20
|
+
name: "CodeBuddy",
|
|
21
|
+
baseUrl: "codebuddy-local",
|
|
22
|
+
apiKey: "codebuddy-local-auth",
|
|
23
|
+
api: "codebuddy-sdk",
|
|
24
|
+
models: [],
|
|
25
|
+
streamSimple: streamCodebuddy,
|
|
26
|
+
});
|
|
39
27
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
q.supportedModels(),
|
|
52
|
-
new Promise<never>((_, reject) =>
|
|
53
|
-
setTimeout(() => reject(new Error("CodeBuddy model discovery timed out after 15s")), 15000),
|
|
54
|
-
),
|
|
55
|
-
]);
|
|
56
|
-
if (models && models.length > 0) {
|
|
57
|
-
return models.map((m) => {
|
|
58
|
-
const caps = detectCapabilities(m.id);
|
|
59
|
-
return {
|
|
60
|
-
id: m.id,
|
|
61
|
-
name: m.name || m.id,
|
|
62
|
-
reasoning: caps.reasoning,
|
|
63
|
-
input: caps.input,
|
|
64
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
65
|
-
contextWindow: caps.contextWindow,
|
|
66
|
-
maxTokens: caps.maxTokens,
|
|
67
|
-
};
|
|
28
|
+
// Lazy model discovery at startup
|
|
29
|
+
queueMicrotask(async () => {
|
|
30
|
+
try {
|
|
31
|
+
const models = await discoverModels();
|
|
32
|
+
api.registerProvider("codebuddy", {
|
|
33
|
+
name: "CodeBuddy",
|
|
34
|
+
baseUrl: "codebuddy-local",
|
|
35
|
+
apiKey: "codebuddy-local-auth",
|
|
36
|
+
api: "codebuddy-sdk",
|
|
37
|
+
models,
|
|
38
|
+
streamSimple: streamCodebuddy,
|
|
68
39
|
});
|
|
40
|
+
} catch {
|
|
41
|
+
// Model discovery failed — provider registered with empty models,
|
|
42
|
+
// user can still try manually.
|
|
69
43
|
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Fall back to hardcoded minimal list so Pi can still show a provider.
|
|
73
|
-
console.warn(
|
|
74
|
-
`[pi-codebuddy-sdk] Model discovery failed: ${e instanceof Error ? e.message : String(e)}. Using ${3} fallback models.`,
|
|
75
|
-
);
|
|
76
|
-
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
77
46
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
47
|
+
// ── model discovery ──
|
|
48
|
+
|
|
49
|
+
let _sdk: typeof import("@tencent-ai/agent-sdk") | undefined;
|
|
50
|
+
async function getSdk() {
|
|
51
|
+
if (!_sdk) _sdk = await import("@tencent-ai/agent-sdk");
|
|
52
|
+
return _sdk;
|
|
84
53
|
}
|
|
85
54
|
|
|
86
|
-
export
|
|
87
|
-
const
|
|
55
|
+
export async function discoverModels(): Promise<ProviderApi["models"]> {
|
|
56
|
+
const sdk = await getSdk();
|
|
57
|
+
const q = sdk.query({ prompt: " ", options: { maxTurns: 0 } });
|
|
58
|
+
const supported = await q.supportedModels();
|
|
59
|
+
await q.interrupt();
|
|
88
60
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
61
|
+
const models = supported.map((m) => {
|
|
62
|
+
const caps = detectCapabilities(m.id);
|
|
63
|
+
return {
|
|
64
|
+
id: m.id,
|
|
65
|
+
name: m.name || m.id,
|
|
66
|
+
api: "codebuddy-sdk" as const,
|
|
67
|
+
provider: "codebuddy" as const,
|
|
68
|
+
reasoning: caps.reasoning,
|
|
69
|
+
input: caps.input,
|
|
70
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
71
|
+
contextWindow: caps.contextWindow,
|
|
72
|
+
maxTokens: caps.maxTokens,
|
|
73
|
+
};
|
|
96
74
|
});
|
|
75
|
+
|
|
76
|
+
return models;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── capability detection (pattern-based, like pi-claude-bridge) ──
|
|
80
|
+
|
|
81
|
+
const THINKING_PATTERNS = [/claude/i, /gemini/i, /gpt-5/i];
|
|
82
|
+
const IMAGE_PATTERNS = [/claude/i, /gemini/i, /gpt/i];
|
|
83
|
+
const CONTEXT_ESTIMATES: Record<string, { window: number; maxTokens: number }> = {
|
|
84
|
+
claude: { window: 200000, maxTokens: 8192 },
|
|
85
|
+
gemini: { window: 1048576, maxTokens: 8192 },
|
|
86
|
+
gpt: { window: 200000, maxTokens: 16384 },
|
|
87
|
+
deepseek: { window: 131072, maxTokens: 8192 },
|
|
88
|
+
glm: { window: 131072, maxTokens: 8192 },
|
|
89
|
+
minimax: { window: 131072, maxTokens: 8192 },
|
|
90
|
+
kimi: { window: 131072, maxTokens: 8192 },
|
|
91
|
+
hy: { window: 131072, maxTokens: 8192 },
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
function detectCapabilities(id: string) {
|
|
95
|
+
const reasoning = THINKING_PATTERNS.some((p) => p.test(id));
|
|
96
|
+
const supportsImages = IMAGE_PATTERNS.some((p) => p.test(id));
|
|
97
|
+
const input = supportsImages ? (["text", "image"] as const) : (["text"] as const);
|
|
98
|
+
const family = Object.keys(CONTEXT_ESTIMATES).find((k) => id.toLowerCase().includes(k));
|
|
99
|
+
const est = family ? CONTEXT_ESTIMATES[family] : { window: 131072, maxTokens: 8192 };
|
|
100
|
+
return { reasoning, input, contextWindow: est.window, maxTokens: est.maxTokens };
|
|
97
101
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Temporary: add debug logging to extractUserMessage
|
package/src/provider.ts
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Provider entry point — pi-claude-bridge pattern.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* extract latest user message → send to SDK → stream events back to Pi.
|
|
4
|
+
* For each Pi turn:
|
|
5
|
+
* 1. Extract the latest user message (strip context-mode injection)
|
|
6
|
+
* 2. Call sdk.query() — stateless, CodeBuddy uses its own system prompt
|
|
7
|
+
* 3. Stream events back to Pi (text, thinking, toolcalls)
|
|
9
8
|
*/
|
|
10
9
|
import {
|
|
11
10
|
createAssistantMessageEventStream,
|
|
12
|
-
type Api,
|
|
13
11
|
type AssistantMessage,
|
|
14
12
|
type AssistantMessageEventStream,
|
|
15
13
|
type Context,
|
|
@@ -20,21 +18,20 @@ import {
|
|
|
20
18
|
type ToolCall,
|
|
21
19
|
} from "@earendil-works/pi-ai/compat";
|
|
22
20
|
import type {
|
|
21
|
+
Effort,
|
|
23
22
|
Message as CbMessage,
|
|
24
23
|
ThinkingConfig,
|
|
25
|
-
Effort,
|
|
26
|
-
RawMessageStreamEvent,
|
|
27
24
|
} from "@tencent-ai/agent-sdk";
|
|
28
25
|
|
|
29
26
|
type CodebuddySdk = typeof import("@tencent-ai/agent-sdk");
|
|
30
27
|
|
|
31
|
-
async function
|
|
28
|
+
async function loadSdk(): Promise<CodebuddySdk> {
|
|
32
29
|
return import("@tencent-ai/agent-sdk");
|
|
33
30
|
}
|
|
34
31
|
|
|
35
32
|
// ── helpers ──
|
|
36
33
|
|
|
37
|
-
function makePartial(model: Model<
|
|
34
|
+
function makePartial(model: Model<any>): AssistantMessage {
|
|
38
35
|
return {
|
|
39
36
|
role: "assistant",
|
|
40
37
|
content: [],
|
|
@@ -47,143 +44,149 @@ function makePartial(model: Model<Api>): AssistantMessage {
|
|
|
47
44
|
};
|
|
48
45
|
}
|
|
49
46
|
|
|
50
|
-
function makeErrorPartial(model: Model<
|
|
47
|
+
function makeErrorPartial(model: Model<any>, error: unknown): AssistantMessage {
|
|
51
48
|
return {
|
|
52
49
|
...makePartial(model),
|
|
53
50
|
stopReason: "error",
|
|
54
|
-
errorMessage:
|
|
51
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
55
52
|
};
|
|
56
53
|
}
|
|
57
54
|
|
|
58
|
-
|
|
59
|
-
if (msg.type === "result" && msg.usage) {
|
|
60
|
-
return {
|
|
61
|
-
usage: {
|
|
62
|
-
input: msg.usage.input_tokens ?? 0,
|
|
63
|
-
output: msg.usage.output_tokens ?? 0,
|
|
64
|
-
cacheRead: msg.usage.cache_read_input_tokens ?? 0,
|
|
65
|
-
cacheWrite: msg.usage.cache_creation_input_tokens ?? 0,
|
|
66
|
-
totalTokens: (msg.usage.input_tokens ?? 0) + (msg.usage.output_tokens ?? 0) + (msg.usage.cache_read_input_tokens ?? 0) + (msg.usage.cache_creation_input_tokens ?? 0),
|
|
67
|
-
cost: {
|
|
68
|
-
input: 0,
|
|
69
|
-
output: 0,
|
|
70
|
-
cacheRead: 0,
|
|
71
|
-
cacheWrite: 0,
|
|
72
|
-
total: msg.total_cost_usd ?? 0,
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
55
|
+
// ── extract user message ──
|
|
79
56
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Extract the latest user message from Pi Context.
|
|
59
|
+
* Strips context-mode session state injection.
|
|
60
|
+
*
|
|
61
|
+
* Like pi-claude-bridge's extractUserPrompt() but with context-mode defense.
|
|
62
|
+
*/
|
|
63
|
+
function extractUserMessage(context: Context): string {
|
|
64
|
+
const msgs = context.messages;
|
|
65
|
+
|
|
66
|
+
// Pi injects context-mode state as the LAST user message (after the real one).
|
|
67
|
+
// Search from the end, skip obvious context-mode injections.
|
|
68
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
69
|
+
if (msgs[i].role !== "user") continue;
|
|
70
|
+
const raw = contentToText((msgs[i] as any).content);
|
|
71
|
+
if (!raw) continue;
|
|
72
|
+
|
|
73
|
+
// Skip messages that are entirely context-mode injection
|
|
74
|
+
if (isContextModeInjection(raw)) continue;
|
|
75
|
+
|
|
76
|
+
return stripPiInjection(raw);
|
|
93
77
|
}
|
|
94
78
|
return "Continue.";
|
|
95
79
|
}
|
|
96
80
|
|
|
97
|
-
|
|
81
|
+
/** Detect if a user message is a Pi context-mode state injection (not a real message). */
|
|
82
|
+
function isContextModeInjection(text: string): boolean {
|
|
83
|
+
const t = text.trim();
|
|
84
|
+
if (t.startsWith("context-mode active.")) return true;
|
|
85
|
+
if (t.includes("<session_state") || t.includes("<session_mode")) return true;
|
|
86
|
+
if (t.startsWith("<summary>") && t.includes("</summary>")) return true;
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function contentToText(content: unknown): string {
|
|
98
91
|
if (typeof content === "string") return content;
|
|
99
92
|
if (!Array.isArray(content)) return "";
|
|
100
93
|
return content
|
|
101
94
|
.filter((c: any) => c?.type === "text")
|
|
102
|
-
.map((c: any) => c.text)
|
|
95
|
+
.map((c: any) => c.text as string)
|
|
103
96
|
.join("\n");
|
|
104
97
|
}
|
|
105
98
|
|
|
106
99
|
/**
|
|
107
|
-
*
|
|
108
|
-
* Strip it to keep CodeBuddy from treating it as conversation content.
|
|
100
|
+
* Strip context-mode session state that Pi prepends to user messages.
|
|
109
101
|
*
|
|
110
|
-
*
|
|
102
|
+
* Pi injects several formats:
|
|
111
103
|
* context-mode active. Hierarchy: ...
|
|
112
|
-
* <session_state source="compaction">
|
|
113
|
-
* <
|
|
114
|
-
*
|
|
115
|
-
*
|
|
104
|
+
* <session_state source="compaction"> ... </session_state>
|
|
105
|
+
* <summary>Compacted: ...</summary>
|
|
106
|
+
*
|
|
107
|
+
* Strip all of them, keep only the actual user message.
|
|
116
108
|
*/
|
|
117
|
-
function
|
|
118
|
-
//
|
|
119
|
-
|
|
109
|
+
function stripPiInjection(text: string): string {
|
|
110
|
+
// Fast path
|
|
111
|
+
if (!text.includes("context-mode") && !text.includes("<session_state") && !text.includes("<summary>")) {
|
|
112
|
+
return text;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 1. Remove <summary> blocks (compaction summaries contain context-mode state)
|
|
116
|
+
text = text.replace(/<summary>[\s\S]*?<\/summary>\s*/g, "");
|
|
117
|
+
|
|
118
|
+
// 2. Remove <session_state> block and everything between "context-mode" and </session_state>
|
|
120
119
|
const sessionEnd = text.lastIndexOf("</session_state>");
|
|
121
120
|
if (sessionEnd >= 0) {
|
|
122
121
|
const after = text.slice(sessionEnd + "</session_state>".length).trimStart();
|
|
123
|
-
if (after)
|
|
122
|
+
if (after) {
|
|
123
|
+
text = after;
|
|
124
|
+
} else {
|
|
125
|
+
// All content was in the session_state block
|
|
126
|
+
const before = text.slice(0, text.indexOf("<session_state")).trim();
|
|
127
|
+
if (before) text = before;
|
|
128
|
+
}
|
|
124
129
|
}
|
|
125
130
|
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
131
|
+
// 3. Remove "context-mode active." line and everything after until a blank line
|
|
132
|
+
// Pattern: "context-mode active. ... Hierarchy: ..." followed by blank line, then real message
|
|
133
|
+
const cmRe = /context-mode active\.[\s\S]*?\n\s*\n([\s\S]*)$/;
|
|
134
|
+
const cmMatch = text.match(cmRe);
|
|
135
|
+
if (cmMatch?.[1]?.trim()) {
|
|
136
|
+
text = cmMatch[1].trim();
|
|
132
137
|
}
|
|
133
138
|
|
|
134
|
-
return
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
// Try to find the actual user message after context-mode XML injection
|
|
139
|
-
const patterns = [
|
|
140
|
-
/context-mode active\.[\s\S]*?<\/session_state>\s*\n+([\s\S]+)$/,
|
|
141
|
-
/<session_state[\s\S]*?<\/session_state>\s*\n+([\s\S]+)$/,
|
|
142
|
-
];
|
|
143
|
-
for (const re of patterns) {
|
|
144
|
-
const m = text.match(re);
|
|
145
|
-
if (m?.[1]?.trim()) return m[1].trim();
|
|
139
|
+
// 4. If "context-mode" is still the only content, return empty
|
|
140
|
+
if (text.trim() === "context-mode active." || text.trim().startsWith("context-mode active.")) {
|
|
141
|
+
const afterCm = text.replace(/context-mode active\.[\s\S]*/, "").trim();
|
|
142
|
+
if (afterCm) text = afterCm;
|
|
146
143
|
}
|
|
147
|
-
|
|
144
|
+
|
|
145
|
+
return text.trim() || text;
|
|
148
146
|
}
|
|
149
147
|
|
|
150
|
-
// ── thinking config
|
|
148
|
+
// ── thinking config ──
|
|
151
149
|
|
|
152
|
-
const
|
|
153
|
-
minimal: 1600,
|
|
154
|
-
low: 4000,
|
|
155
|
-
medium: 0,
|
|
156
|
-
high: 0,
|
|
157
|
-
xhigh: 32000,
|
|
150
|
+
const THINKING_BUDGETS: Record<string, number> = {
|
|
151
|
+
minimal: 1600, low: 4000, medium: 0, high: 0, xhigh: 32000,
|
|
158
152
|
};
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
minimal: 'low',
|
|
162
|
-
low: 'low',
|
|
163
|
-
medium: 'medium',
|
|
164
|
-
high: 'high',
|
|
165
|
-
xhigh: 'xhigh',
|
|
153
|
+
const EFFORT_MAP: Record<string, Effort | undefined> = {
|
|
154
|
+
minimal: "low", low: "low", medium: "medium", high: "high", xhigh: "xhigh",
|
|
166
155
|
};
|
|
167
156
|
|
|
168
|
-
function
|
|
169
|
-
reasoning: string | undefined,
|
|
170
|
-
thinkingBudgets?: SimpleStreamOptions["thinkingBudgets"],
|
|
171
|
-
): { thinking?: ThinkingConfig; effort?: Effort } {
|
|
157
|
+
function buildThinking(reasoning?: SimpleStreamOptions["reasoning"], budgets?: SimpleStreamOptions["thinkingBudgets"]): { thinking?: ThinkingConfig; effort?: Effort } {
|
|
172
158
|
if (!reasoning) return {};
|
|
173
159
|
if (reasoning === "off") return { thinking: { type: "disabled" } };
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
?? DEFAULT_THINKING_BUDGETS[reasoning] ?? 0;
|
|
177
|
-
const effort = DEFAULT_EFFORT[reasoning];
|
|
178
|
-
|
|
160
|
+
const budget = budgets?.[reasoning as keyof typeof budgets] ?? THINKING_BUDGETS[reasoning] ?? 0;
|
|
161
|
+
const effort = EFFORT_MAP[reasoning];
|
|
179
162
|
if (budget > 0) return { thinking: { type: "enabled", budgetTokens: budget }, effort };
|
|
180
163
|
return { thinking: { type: "adaptive" }, effort };
|
|
181
164
|
}
|
|
182
165
|
|
|
183
|
-
// ──
|
|
166
|
+
// ── usage extraction ──
|
|
167
|
+
|
|
168
|
+
function extractUsage(msg: CbMessage): Partial<AssistantMessage> | null {
|
|
169
|
+
if (msg.type !== "result" || !msg.usage) return null;
|
|
170
|
+
return {
|
|
171
|
+
usage: {
|
|
172
|
+
input: msg.usage.input_tokens ?? 0,
|
|
173
|
+
output: msg.usage.output_tokens ?? 0,
|
|
174
|
+
cacheRead: msg.usage.cache_read_input_tokens ?? 0,
|
|
175
|
+
cacheWrite: msg.usage.cache_creation_input_tokens ?? 0,
|
|
176
|
+
totalTokens:
|
|
177
|
+
(msg.usage.input_tokens ?? 0) +
|
|
178
|
+
(msg.usage.output_tokens ?? 0) +
|
|
179
|
+
(msg.usage.cache_read_input_tokens ?? 0) +
|
|
180
|
+
(msg.usage.cache_creation_input_tokens ?? 0),
|
|
181
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: msg.total_cost_usd ?? 0 },
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── main ──
|
|
184
187
|
|
|
185
188
|
export function streamCodebuddy(
|
|
186
|
-
model: Model<
|
|
189
|
+
model: Model<any>,
|
|
187
190
|
context: Context,
|
|
188
191
|
options?: SimpleStreamOptions,
|
|
189
192
|
): AssistantMessageEventStream {
|
|
@@ -194,13 +197,10 @@ export function streamCodebuddy(
|
|
|
194
197
|
try {
|
|
195
198
|
stream.push({ type: "start", partial });
|
|
196
199
|
|
|
197
|
-
const sdk = await
|
|
198
|
-
const userText =
|
|
200
|
+
const sdk = await loadSdk();
|
|
201
|
+
const userText = extractUserMessage(context);
|
|
199
202
|
|
|
200
|
-
const { thinking, effort } =
|
|
201
|
-
options?.reasoning,
|
|
202
|
-
options?.thinkingBudgets,
|
|
203
|
-
);
|
|
203
|
+
const { thinking, effort } = buildThinking(options?.reasoning, options?.thinkingBudgets);
|
|
204
204
|
|
|
205
205
|
const q = sdk.query({
|
|
206
206
|
prompt: userText,
|
|
@@ -214,7 +214,7 @@ export function streamCodebuddy(
|
|
|
214
214
|
},
|
|
215
215
|
});
|
|
216
216
|
|
|
217
|
-
// ── abort
|
|
217
|
+
// ── abort ──
|
|
218
218
|
let aborted = false;
|
|
219
219
|
const onAbort = async () => {
|
|
220
220
|
if (aborted) return;
|
|
@@ -225,86 +225,74 @@ export function streamCodebuddy(
|
|
|
225
225
|
};
|
|
226
226
|
if (options?.signal) {
|
|
227
227
|
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
228
|
-
if (options.signal.aborted) {
|
|
229
|
-
await onAbort();
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
228
|
+
if (options.signal.aborted) { await onAbort(); return; }
|
|
232
229
|
}
|
|
233
230
|
|
|
234
|
-
// ── per-
|
|
235
|
-
type BlockKind =
|
|
231
|
+
// ── per-block tracking (for stream_event delta accumulation) ──
|
|
232
|
+
type BlockKind = "text" | "thinking" | "tool_use" | "redacted_thinking";
|
|
236
233
|
const blockKind = new Map<number, BlockKind>();
|
|
237
234
|
const blockAccum = new Map<number, string>();
|
|
238
235
|
const blockMeta = new Map<number, { id: string; name: string; signature?: string }>();
|
|
239
236
|
|
|
240
237
|
let hasStreamEvents = false;
|
|
241
238
|
|
|
242
|
-
function
|
|
239
|
+
function onStart(idx: number, kind: BlockKind, init: string, meta?: { id: string; name: string; signature?: string }) {
|
|
243
240
|
blockKind.set(idx, kind);
|
|
244
|
-
blockAccum.set(idx,
|
|
241
|
+
blockAccum.set(idx, init);
|
|
245
242
|
if (meta) blockMeta.set(idx, meta);
|
|
246
243
|
|
|
247
|
-
if (kind ===
|
|
248
|
-
const tc: ThinkingContent = { type:
|
|
244
|
+
if (kind === "redacted_thinking") {
|
|
245
|
+
const tc: ThinkingContent = { type: "thinking", thinking: "[redacted]", redacted: true, thinkingSignature: meta?.signature };
|
|
249
246
|
partial.content = [...partial.content, tc];
|
|
250
|
-
stream.push({ type:
|
|
251
|
-
stream.push({ type:
|
|
252
|
-
} else if (kind ===
|
|
253
|
-
const tc: ThinkingContent = { type:
|
|
247
|
+
stream.push({ type: "thinking_start", contentIndex: idx, partial });
|
|
248
|
+
stream.push({ type: "thinking_end", contentIndex: idx, content: "[redacted]", partial });
|
|
249
|
+
} else if (kind === "thinking") {
|
|
250
|
+
const tc: ThinkingContent = { type: "thinking", thinking: init, thinkingSignature: meta?.signature };
|
|
254
251
|
partial.content = [...partial.content, tc];
|
|
255
|
-
stream.push({ type:
|
|
256
|
-
} else if (kind ===
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
partial.content = [...partial.content, toolCall];
|
|
263
|
-
stream.push({ type: 'toolcall_start', contentIndex: idx, partial });
|
|
252
|
+
stream.push({ type: "thinking_start", contentIndex: idx, partial });
|
|
253
|
+
} else if (kind === "text") {
|
|
254
|
+
partial.content = [...partial.content, { type: "text", text: init }];
|
|
255
|
+
stream.push({ type: "text_start", contentIndex: idx, partial });
|
|
256
|
+
} else if (kind === "tool_use" && meta) {
|
|
257
|
+
partial.content = [...partial.content, { type: "toolCall", id: meta.id, name: meta.name, arguments: {} }];
|
|
258
|
+
stream.push({ type: "toolcall_start", contentIndex: idx, partial });
|
|
264
259
|
}
|
|
265
260
|
}
|
|
266
261
|
|
|
267
|
-
function
|
|
262
|
+
function onDelta(idx: number, deltaType: string, value: string) {
|
|
268
263
|
const kind = blockKind.get(idx);
|
|
269
264
|
if (!kind) return;
|
|
270
265
|
|
|
271
|
-
if (deltaType ===
|
|
272
|
-
const
|
|
273
|
-
blockAccum.set(idx,
|
|
266
|
+
if (deltaType === "text_delta" && kind === "text") {
|
|
267
|
+
const next = (blockAccum.get(idx) || "") + value;
|
|
268
|
+
blockAccum.set(idx, next);
|
|
274
269
|
const blocks = [...partial.content];
|
|
275
|
-
blocks[idx] = { type:
|
|
270
|
+
blocks[idx] = { type: "text", text: next };
|
|
276
271
|
partial.content = blocks;
|
|
277
|
-
stream.push({ type:
|
|
278
|
-
} else if (deltaType ===
|
|
279
|
-
const
|
|
280
|
-
blockAccum.set(idx,
|
|
272
|
+
stream.push({ type: "text_delta", contentIndex: idx, delta: value, partial });
|
|
273
|
+
} else if (deltaType === "thinking_delta" && kind === "thinking") {
|
|
274
|
+
const next = (blockAccum.get(idx) || "") + value;
|
|
275
|
+
blockAccum.set(idx, next);
|
|
281
276
|
const blocks = [...partial.content];
|
|
282
|
-
blocks[idx] = { type:
|
|
277
|
+
blocks[idx] = { type: "thinking", thinking: next, thinkingSignature: blockMeta.get(idx)?.signature };
|
|
283
278
|
partial.content = blocks;
|
|
284
|
-
stream.push({ type:
|
|
285
|
-
} else if (deltaType ===
|
|
286
|
-
const
|
|
287
|
-
blockAccum.set(idx,
|
|
279
|
+
stream.push({ type: "thinking_delta", contentIndex: idx, delta: value, partial });
|
|
280
|
+
} else if (deltaType === "input_json_delta" && kind === "tool_use") {
|
|
281
|
+
const json = (blockAccum.get(idx) || "") + value;
|
|
282
|
+
blockAccum.set(idx, json);
|
|
288
283
|
const blocks = [...partial.content];
|
|
289
|
-
try {
|
|
290
|
-
blocks[idx] = { ...blocks[idx], arguments: JSON.parse(newJson) };
|
|
291
|
-
} catch { /* partial JSON */ }
|
|
284
|
+
try { blocks[idx] = { ...blocks[idx], arguments: JSON.parse(json) }; } catch { /* partial */ }
|
|
292
285
|
partial.content = blocks;
|
|
293
|
-
stream.push({ type:
|
|
286
|
+
stream.push({ type: "toolcall_delta", contentIndex: idx, delta: value, partial } as any);
|
|
294
287
|
}
|
|
295
288
|
}
|
|
296
289
|
|
|
297
|
-
function
|
|
290
|
+
function onStop(idx: number) {
|
|
298
291
|
const kind = blockKind.get(idx);
|
|
299
|
-
const accum = blockAccum.get(idx) ||
|
|
300
|
-
if (kind ===
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
stream.push({ type: 'thinking_end', contentIndex: idx, content: accum, partial });
|
|
304
|
-
} else if (kind === 'tool_use') {
|
|
305
|
-
const toolCall = partial.content[idx] as ToolCall;
|
|
306
|
-
stream.push({ type: 'toolcall_end', contentIndex: idx, toolCall, partial });
|
|
307
|
-
}
|
|
292
|
+
const accum = blockAccum.get(idx) || "";
|
|
293
|
+
if (kind === "text") stream.push({ type: "text_end", contentIndex: idx, content: accum, partial });
|
|
294
|
+
else if (kind === "thinking") stream.push({ type: "thinking_end", contentIndex: idx, content: accum, partial });
|
|
295
|
+
else if (kind === "tool_use") stream.push({ type: "toolcall_end", contentIndex: idx, toolCall: partial.content[idx] as ToolCall, partial });
|
|
308
296
|
}
|
|
309
297
|
|
|
310
298
|
// ── iterate ──
|
|
@@ -316,29 +304,24 @@ export function streamCodebuddy(
|
|
|
316
304
|
continue;
|
|
317
305
|
}
|
|
318
306
|
|
|
307
|
+
// Token-level streaming via stream_event (preferred)
|
|
319
308
|
if (msg.type === "stream_event") {
|
|
320
309
|
hasStreamEvents = true;
|
|
321
310
|
const evt = msg.event;
|
|
322
311
|
|
|
323
312
|
if (evt.type === "content_block_start") {
|
|
324
|
-
const
|
|
325
|
-
if (
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
} else if (block.type === "redacted_thinking") {
|
|
330
|
-
onBlockStart(evt.index, 'redacted_thinking', '[redacted]', { id: '', name: '', signature: block.data });
|
|
331
|
-
} else if (block.type === "tool_use") {
|
|
332
|
-
const initJson = JSON.stringify(block.input || {});
|
|
333
|
-
onBlockStart(evt.index, 'tool_use', initJson, { id: block.id, name: block.name });
|
|
334
|
-
}
|
|
313
|
+
const b = evt.content_block;
|
|
314
|
+
if (b.type === "text") onStart(evt.index, "text", b.text || "");
|
|
315
|
+
else if (b.type === "thinking") onStart(evt.index, "thinking", b.thinking || "", { id: "", name: "", signature: b.signature });
|
|
316
|
+
else if (b.type === "redacted_thinking") onStart(evt.index, "redacted_thinking", "[redacted]", { id: "", name: "", signature: b.data });
|
|
317
|
+
else if (b.type === "tool_use") onStart(evt.index, "tool_use", JSON.stringify(b.input || {}), { id: b.id, name: b.name });
|
|
335
318
|
} else if (evt.type === "content_block_delta") {
|
|
336
319
|
const d = evt.delta;
|
|
337
|
-
if (d.type ===
|
|
338
|
-
else if (d.type ===
|
|
339
|
-
else if (d.type ===
|
|
320
|
+
if (d.type === "text_delta") onDelta(evt.index, "text_delta", d.text);
|
|
321
|
+
else if (d.type === "thinking_delta") onDelta(evt.index, "thinking_delta", d.thinking);
|
|
322
|
+
else if (d.type === "input_json_delta") onDelta(evt.index, "input_json_delta", d.partial_json);
|
|
340
323
|
} else if (evt.type === "content_block_stop") {
|
|
341
|
-
|
|
324
|
+
onStop(evt.index);
|
|
342
325
|
}
|
|
343
326
|
continue;
|
|
344
327
|
}
|
|
@@ -348,25 +331,16 @@ export function streamCodebuddy(
|
|
|
348
331
|
let ci = 0;
|
|
349
332
|
for (const block of msg.message.content) {
|
|
350
333
|
const idx = ci++;
|
|
351
|
-
if (block.type === "thinking") {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
onBlockStart(idx, 'redacted_thinking', '[redacted]', { id: '', name: '', signature: block.data });
|
|
356
|
-
} else if (block.type === "text") {
|
|
357
|
-
onBlockStart(idx, 'text', block.text);
|
|
358
|
-
onBlockStop(idx);
|
|
359
|
-
} else if (block.type === "tool_use") {
|
|
360
|
-
const initJson = JSON.stringify(block.input ?? {});
|
|
361
|
-
onBlockStart(idx, 'tool_use', initJson, { id: block.id, name: block.name });
|
|
362
|
-
onBlockStop(idx);
|
|
363
|
-
}
|
|
334
|
+
if (block.type === "thinking") { onStart(idx, "thinking", block.thinking); onStop(idx); }
|
|
335
|
+
else if (block.type === "redacted_thinking") onStart(idx, "redacted_thinking", "[redacted]", { id: "", name: "", signature: block.data });
|
|
336
|
+
else if (block.type === "text") { onStart(idx, "text", block.text); onStop(idx); }
|
|
337
|
+
else if (block.type === "tool_use") { onStart(idx, "tool_use", JSON.stringify(block.input ?? {}), { id: block.id, name: block.name }); onStop(idx); }
|
|
364
338
|
}
|
|
365
339
|
}
|
|
366
340
|
|
|
367
341
|
if (msg.type === "result") {
|
|
368
|
-
const
|
|
369
|
-
if (
|
|
342
|
+
const u = extractUsage(msg);
|
|
343
|
+
if (u) Object.assign(partial, u);
|
|
370
344
|
|
|
371
345
|
if (msg.subtype === "success") {
|
|
372
346
|
partial.stopReason = "stop";
|
|
@@ -380,8 +354,7 @@ export function streamCodebuddy(
|
|
|
380
354
|
}
|
|
381
355
|
}
|
|
382
356
|
} catch (error) {
|
|
383
|
-
|
|
384
|
-
stream.push({ type: "error", reason: "error", error: errPartial });
|
|
357
|
+
stream.push({ type: "error", reason: "error", error: makeErrorPartial(model, error) });
|
|
385
358
|
}
|
|
386
359
|
});
|
|
387
360
|
|