-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvscode-lm-format.ts
More file actions
233 lines (214 loc) · 7.79 KB
/
Copy pathvscode-lm-format.ts
File metadata and controls
233 lines (214 loc) · 7.79 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
import { Anthropic } from "@anthropic-ai/sdk"
import * as vscode from "vscode"
import { apiLog } from "@shofer/core"
/**
* Safely converts a value into a plain object.
*/
function asObjectSafe(value: any): object {
// Handle null/undefined
if (!value) {
return {}
}
try {
// Handle strings that might be JSON
if (typeof value === "string") {
return JSON.parse(value)
}
// Handle pre-existing objects
if (typeof value === "object") {
return { ...value }
}
return {}
} catch (error) {
apiLog.warn("Shofer <Language Model API>: Failed to parse object:", error)
return {}
}
}
/**
* Convert an Anthropic-format image block (base64 source) into a
* `vscode.LanguageModelDataPart` so the underlying VS Code LM provider
* (e.g. Shofer llm-provider) can forward the actual image bytes to a
* vision-capable upstream rather than receiving a placeholder string.
*
* Returns a `LanguageModelTextPart` placeholder when the API surface is
* unavailable (older VS Code), the source isn't base64, or decoding fails
* — that way the message round-trips safely on hosts where the data part
* isn't supported.
*
* The cast through `any` is intentional: `LanguageModelChatMessage.User()`'s
* static signature in the stable typings only accepts text + tool-result
* parts, but at runtime modern VS Code accepts `LanguageModelDataPart` too
* (matching what `vscode.proposed.chatProvider2.d.ts` documents). Bypassing
* the typing here keeps the pinned `@types/vscode` floor unchanged.
*/
function imageBlockToContentPart(part: Anthropic.ImageBlockParam): vscode.LanguageModelTextPart | object {
const DataPart = (vscode as any).LanguageModelDataPart
const source = part.source
if (DataPart && source?.type === "base64" && source.data && source.media_type) {
try {
const bytes = Buffer.from(source.data, "base64")
return DataPart.image(bytes, source.media_type)
} catch (err) {
apiLog.warn("Shofer <Language Model API>: Failed to decode image data:", err)
}
}
return new vscode.LanguageModelTextPart(
`[Image (${source?.type || "unknown source-type"}): ${source?.media_type || "unknown media-type"} not supported by VSCode LM API]`,
)
}
export function convertToVsCodeLmMessages(
anthropicMessages: Anthropic.Messages.MessageParam[],
): vscode.LanguageModelChatMessage[] {
const vsCodeLmMessages: vscode.LanguageModelChatMessage[] = []
for (const anthropicMessage of anthropicMessages) {
// Handle simple string messages
if (typeof anthropicMessage.content === "string") {
vsCodeLmMessages.push(
anthropicMessage.role === "assistant"
? vscode.LanguageModelChatMessage.Assistant(anthropicMessage.content)
: vscode.LanguageModelChatMessage.User(anthropicMessage.content),
)
continue
}
// Handle complex message structures
switch (anthropicMessage.role) {
case "user": {
const { nonToolMessages, toolMessages } = anthropicMessage.content.reduce<{
nonToolMessages: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[]
toolMessages: Anthropic.ToolResultBlockParam[]
}>(
(acc, part) => {
if (part.type === "tool_result") {
acc.toolMessages.push(part)
} else if (part.type === "text" || part.type === "image") {
acc.nonToolMessages.push(part)
}
return acc
},
{ nonToolMessages: [], toolMessages: [] },
)
// Process tool messages first then non-tool messages
const contentParts = [
// Convert tool messages to ToolResultParts
...toolMessages.map((toolMessage) => {
// Process tool result content into TextParts (image
// parts inside tool results aren't widely supported
// by the LM tool-result API, so they fall back to a
// text marker).
const toolContentParts: vscode.LanguageModelTextPart[] =
typeof toolMessage.content === "string"
? [new vscode.LanguageModelTextPart(toolMessage.content)]
: (toolMessage.content?.map((part) => {
if (part.type === "image") {
return new vscode.LanguageModelTextPart(
`[Image (${part.source?.type || "Unknown source-type"}): ${part.source?.media_type || "unknown media-type"} not supported in VSCode LM tool results]`,
)
}
return new vscode.LanguageModelTextPart(part.text)
}) ?? [new vscode.LanguageModelTextPart("")])
return new vscode.LanguageModelToolResultPart(toolMessage.tool_use_id, toolContentParts)
}),
// Convert non-tool messages to TextParts/DataParts after tool messages
...nonToolMessages.map((part) => {
if (part.type === "image") {
return imageBlockToContentPart(part)
}
return new vscode.LanguageModelTextPart(part.text)
}),
]
// Add single user message with all content parts. Cast is
// needed because `User()`'s static signature predates the
// `LanguageModelDataPart` content type (see helper docstring).
vsCodeLmMessages.push(vscode.LanguageModelChatMessage.User(contentParts as any))
break
}
case "assistant": {
const { nonToolMessages, toolMessages } = anthropicMessage.content.reduce<{
nonToolMessages: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[]
toolMessages: Anthropic.ToolUseBlockParam[]
}>(
(acc, part) => {
if (part.type === "tool_use") {
acc.toolMessages.push(part)
} else if (part.type === "text" || part.type === "image") {
acc.nonToolMessages.push(part)
}
return acc
},
{ nonToolMessages: [], toolMessages: [] },
)
// Process non-tool messages first, then tool messages
// Tool calls must come at the end so they are properly followed by user message with tool results
const contentParts = [
// Convert non-tool messages to TextParts first
...nonToolMessages.map((part) => {
if (part.type === "image") {
return new vscode.LanguageModelTextPart("[Image generation not supported by VSCode LM API]")
}
return new vscode.LanguageModelTextPart(part.text)
}),
// Convert tool messages to ToolCallParts after text
...toolMessages.map(
(toolMessage) =>
new vscode.LanguageModelToolCallPart(
toolMessage.id,
toolMessage.name,
asObjectSafe(toolMessage.input),
),
),
]
// Add the assistant message to the list of messages
vsCodeLmMessages.push(vscode.LanguageModelChatMessage.Assistant(contentParts))
break
}
}
}
return vsCodeLmMessages
}
export function convertToAnthropicRole(vsCodeLmMessageRole: vscode.LanguageModelChatMessageRole): string | null {
switch (vsCodeLmMessageRole) {
case vscode.LanguageModelChatMessageRole.Assistant:
return "assistant"
case vscode.LanguageModelChatMessageRole.User:
return "user"
default:
return null
}
}
/**
* Extracts the text content from a VS Code Language Model chat message.
* @param message A VS Code Language Model chat message.
* @returns The extracted text content.
*/
export function extractTextCountFromMessage(message: vscode.LanguageModelChatMessage): string {
let text = ""
if (Array.isArray(message.content)) {
for (const item of message.content) {
if (item instanceof vscode.LanguageModelTextPart) {
text += item.value
}
if (item instanceof vscode.LanguageModelToolResultPart) {
text += item.callId
for (const part of item.content) {
if (part instanceof vscode.LanguageModelTextPart) {
text += part.value
}
}
}
if (item instanceof vscode.LanguageModelToolCallPart) {
text += item.name
text += item.callId
if (item.input && Object.keys(item.input).length > 0) {
try {
text += JSON.stringify(item.input)
} catch (error) {
apiLog.error("Shofer <Language Model API>: Failed to stringify tool call input:", error)
}
}
}
}
} else if (typeof message.content === "string") {
text += message.content
}
return text
}