aicodeswitch 1.4.1 → 1.5.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,440 @@
1
+ import type { TokenUsage } from '../../types';
2
+
3
+ type OpenAIInputContentItem = {
4
+ type?: string;
5
+ text?: string;
6
+ image_url?: string | { url?: string };
7
+ [key: string]: unknown;
8
+ };
9
+
10
+ type OpenAIInputMessage = {
11
+ role?: string;
12
+ content?: string | OpenAIInputContentItem[];
13
+ };
14
+
15
+ type ClaudeContentBlock =
16
+ | { type: 'text'; text: string }
17
+ | { type: 'image'; source: { type: 'url' | 'base64'; url?: string; media_type?: string; data?: string } }
18
+ | { type: 'tool_use'; id?: string; name: string; input?: unknown }
19
+ | { type: 'tool_result'; tool_use_id?: string; content?: unknown }
20
+ | Record<string, any>;
21
+
22
+ type ClaudeMessage = {
23
+ role: 'user' | 'assistant' | 'system' | 'tool';
24
+ content: string | ClaudeContentBlock[] | null;
25
+ };
26
+
27
+ type ClaudeRequest = {
28
+ model?: string;
29
+ messages?: ClaudeMessage[];
30
+ system?: string | ClaudeContentBlock[];
31
+ max_tokens?: number;
32
+ temperature?: number;
33
+ top_p?: number;
34
+ stream?: boolean;
35
+ tools?: Array<{ name: string; description?: string; input_schema?: unknown }>;
36
+ tool_choice?: unknown;
37
+ stop_sequences?: string[];
38
+ [key: string]: unknown;
39
+ };
40
+
41
+ const decodeDataUrl = (dataUrl: string) => {
42
+ const match = dataUrl.match(/^data:(.*?);base64,(.*)$/);
43
+ if (!match) return null;
44
+ return { mediaType: match[1], data: match[2] };
45
+ };
46
+
47
+ const buildClaudeContentFromOpenAIContent = (
48
+ content: string | OpenAIInputContentItem[] | undefined
49
+ ): string | ClaudeContentBlock[] | null => {
50
+ if (typeof content === 'string') {
51
+ return content;
52
+ }
53
+ if (!Array.isArray(content)) return null;
54
+
55
+ const blocks: ClaudeContentBlock[] = [];
56
+ for (const item of content) {
57
+ const itemType = item?.type;
58
+ if (itemType === 'input_text' || itemType === 'output_text' || itemType === 'text') {
59
+ if (typeof item.text === 'string') {
60
+ blocks.push({ type: 'text', text: item.text });
61
+ }
62
+ continue;
63
+ }
64
+ if (itemType === 'input_image' || itemType === 'image_url' || itemType === 'image') {
65
+ const url = typeof item.image_url === 'string' ? item.image_url : item.image_url?.url;
66
+ if (url && url.startsWith('data:')) {
67
+ const decoded = decodeDataUrl(url);
68
+ if (decoded) {
69
+ blocks.push({
70
+ type: 'image',
71
+ source: { type: 'base64', media_type: decoded.mediaType, data: decoded.data },
72
+ });
73
+ continue;
74
+ }
75
+ }
76
+ if (url) {
77
+ blocks.push({ type: 'image', source: { type: 'url', url } });
78
+ }
79
+ }
80
+ }
81
+
82
+ if (blocks.length === 0) return null;
83
+ return blocks;
84
+ };
85
+
86
+ const extractTextFromOpenAIContent = (content: string | OpenAIInputContentItem[] | undefined) => {
87
+ if (typeof content === 'string') return content;
88
+ if (!Array.isArray(content)) return '';
89
+ const parts: string[] = [];
90
+ for (const item of content) {
91
+ if ((item?.type === 'input_text' || item?.type === 'output_text' || item?.type === 'text') && typeof item.text === 'string') {
92
+ parts.push(item.text);
93
+ }
94
+ }
95
+ return parts.join('');
96
+ };
97
+
98
+ const normalizeOpenAIInputMessages = (input: any): OpenAIInputMessage[] => {
99
+ if (!input) return [];
100
+ if (typeof input === 'string') {
101
+ return [{ role: 'user', content: input }];
102
+ }
103
+ if (Array.isArray(input)) {
104
+ return input as OpenAIInputMessage[];
105
+ }
106
+ if (typeof input === 'object' && (input as OpenAIInputMessage).role) {
107
+ return [input as OpenAIInputMessage];
108
+ }
109
+ return [];
110
+ };
111
+
112
+ const mapOpenAIToolChoiceToClaude = (toolChoice: any): unknown => {
113
+ if (toolChoice === 'auto' || toolChoice === 'none' || toolChoice === 'required') {
114
+ return toolChoice;
115
+ }
116
+ if (toolChoice && typeof toolChoice === 'object') {
117
+ const functionName = toolChoice.function?.name || toolChoice.name;
118
+ if (functionName) {
119
+ return { type: 'tool', name: functionName };
120
+ }
121
+ }
122
+ return toolChoice;
123
+ };
124
+
125
+ const mapClaudeToolChoiceToOpenAI = (toolChoice: any): unknown => {
126
+ if (toolChoice === 'auto' || toolChoice === 'none' || toolChoice === 'required') {
127
+ return toolChoice;
128
+ }
129
+ if (toolChoice && typeof toolChoice === 'object') {
130
+ const name = (toolChoice as any).name;
131
+ if (name) {
132
+ return { type: 'function', function: { name } };
133
+ }
134
+ }
135
+ return toolChoice;
136
+ };
137
+
138
+ const mapClaudeContentToOpenAIInput = (content: string | ClaudeContentBlock[] | null) => {
139
+ if (typeof content === 'string' || content === null) {
140
+ return content;
141
+ }
142
+ if (!Array.isArray(content)) return null;
143
+
144
+ const parts: any[] = [];
145
+ const textParts: string[] = [];
146
+ for (const block of content) {
147
+ if (!block || typeof block !== 'object') continue;
148
+ if (block.type === 'text' && typeof (block as any).text === 'string') {
149
+ textParts.push((block as any).text);
150
+ continue;
151
+ }
152
+ if (block.type === 'image' && (block as any).source) {
153
+ const source = (block as any).source;
154
+ if (source.type === 'url' && source.url) {
155
+ parts.push({ type: 'input_image', image_url: source.url });
156
+ }
157
+ if (source.type === 'base64' && source.data) {
158
+ const mediaType = source.media_type || 'application/octet-stream';
159
+ parts.push({ type: 'input_image', image_url: `data:${mediaType};base64,${source.data}` });
160
+ }
161
+ }
162
+ if (block.type === 'tool_result') {
163
+ const toolContent = (block as any).content;
164
+ if (toolContent !== undefined) {
165
+ textParts.push(typeof toolContent === 'string' ? toolContent : JSON.stringify(toolContent));
166
+ }
167
+ }
168
+ }
169
+
170
+ if (parts.length === 0) {
171
+ return textParts.join('') || null;
172
+ }
173
+
174
+ if (textParts.length > 0) {
175
+ parts.unshift({ type: 'input_text', text: textParts.join('') });
176
+ }
177
+ return parts;
178
+ };
179
+
180
+ const extractTextFromClaudeContent = (content: string | ClaudeContentBlock[] | null | undefined) => {
181
+ if (typeof content === 'string') return content;
182
+ if (!Array.isArray(content)) return undefined;
183
+ const parts: string[] = [];
184
+ for (const block of content) {
185
+ if (block && typeof block === 'object' && block.type === 'text' && typeof (block as any).text === 'string') {
186
+ parts.push((block as any).text);
187
+ }
188
+ }
189
+ return parts.join('');
190
+ };
191
+
192
+ export const transformOpenAIResponsesRequestToClaude = (body: any, targetModel?: string): ClaudeRequest => {
193
+ const inputMessages = normalizeOpenAIInputMessages(body?.input);
194
+ const messages: ClaudeMessage[] = inputMessages.map((message) => {
195
+ const role =
196
+ message.role === 'assistant'
197
+ ? 'assistant'
198
+ : message.role === 'system' || message.role === 'developer'
199
+ ? 'system'
200
+ : message.role === 'tool'
201
+ ? 'tool'
202
+ : 'user';
203
+ const contentBlocks = buildClaudeContentFromOpenAIContent(message.content);
204
+ return {
205
+ role,
206
+ content: contentBlocks ?? extractTextFromOpenAIContent(message.content) ?? null,
207
+ };
208
+ });
209
+
210
+ const tools = Array.isArray(body?.tools)
211
+ ? body.tools
212
+ .map((tool: any) => tool?.function ? ({
213
+ name: tool.function.name,
214
+ description: tool.function.description,
215
+ input_schema: tool.function.parameters,
216
+ }) : null)
217
+ .filter(Boolean)
218
+ : undefined;
219
+
220
+ return {
221
+ model: targetModel || body?.model,
222
+ messages,
223
+ system: body?.instructions,
224
+ max_tokens: body?.max_output_tokens ?? body?.max_tokens,
225
+ temperature: body?.temperature,
226
+ top_p: body?.top_p,
227
+ stream: body?.stream,
228
+ tools,
229
+ tool_choice: body?.tool_choice ? mapOpenAIToolChoiceToClaude(body.tool_choice) : undefined,
230
+ stop_sequences: body?.stop,
231
+ };
232
+ };
233
+
234
+ export const transformOpenAIResponsesRequestToOpenAIChat = (body: any, targetModel?: string) => {
235
+ const inputMessages = normalizeOpenAIInputMessages(body?.input);
236
+ const messages = inputMessages.map((message) => ({
237
+ role: message.role || 'user',
238
+ content: extractTextFromOpenAIContent(message.content),
239
+ }));
240
+
241
+ if (typeof body?.instructions === 'string' && body.instructions.trim().length > 0) {
242
+ messages.unshift({ role: 'system', content: body.instructions });
243
+ }
244
+
245
+ const openaiBody: any = {
246
+ model: targetModel || body?.model,
247
+ messages,
248
+ };
249
+
250
+ if (typeof body?.temperature === 'number') openaiBody.temperature = body.temperature;
251
+ if (typeof body?.top_p === 'number') openaiBody.top_p = body.top_p;
252
+ if (typeof body?.max_output_tokens === 'number') openaiBody.max_tokens = body.max_output_tokens;
253
+ if (typeof body?.max_tokens === 'number' && openaiBody.max_tokens === undefined) openaiBody.max_tokens = body.max_tokens;
254
+ if (Array.isArray(body?.stop)) openaiBody.stop = body.stop;
255
+
256
+ if (body?.tools) {
257
+ openaiBody.tools = body.tools;
258
+ }
259
+ if (body?.tool_choice) {
260
+ openaiBody.tool_choice = body.tool_choice;
261
+ }
262
+ if (body?.stream === true) {
263
+ openaiBody.stream = true;
264
+ openaiBody.stream_options = { include_usage: true };
265
+ }
266
+
267
+ return openaiBody;
268
+ };
269
+
270
+ export const transformClaudeRequestToOpenAIResponses = (body: ClaudeRequest, targetModel?: string) => {
271
+ const messages = Array.isArray(body.messages) ? body.messages : [];
272
+ const input = messages.map((message) => ({
273
+ role: message.role,
274
+ content: mapClaudeContentToOpenAIInput(message.content),
275
+ }));
276
+
277
+ const tools = Array.isArray(body.tools)
278
+ ? body.tools.map((tool) => ({
279
+ type: 'function',
280
+ function: {
281
+ name: tool.name,
282
+ description: tool.description,
283
+ parameters: tool.input_schema,
284
+ },
285
+ }))
286
+ : undefined;
287
+
288
+ const openaiBody: any = {
289
+ model: targetModel || body.model,
290
+ input,
291
+ instructions: extractTextFromClaudeContent(body.system),
292
+ stream: body.stream,
293
+ tools,
294
+ tool_choice: body.tool_choice ? mapClaudeToolChoiceToOpenAI(body.tool_choice) : undefined,
295
+ temperature: body.temperature,
296
+ top_p: body.top_p,
297
+ max_output_tokens: body.max_tokens,
298
+ };
299
+
300
+ if (Array.isArray(body.stop_sequences)) {
301
+ openaiBody.stop = body.stop_sequences;
302
+ }
303
+
304
+ return openaiBody;
305
+ };
306
+
307
+ const extractOutputItems = (body: any) => {
308
+ const outputItems = Array.isArray(body?.output) ? body.output : [];
309
+ const contentBlocks: ClaudeContentBlock[] = [];
310
+
311
+ for (const item of outputItems) {
312
+ if (!item || typeof item !== 'object') continue;
313
+
314
+ if (item.type === 'message') {
315
+ const content = Array.isArray(item.content) ? item.content : [];
316
+ for (const part of content) {
317
+ if (part?.type === 'output_text' && typeof part.text === 'string') {
318
+ contentBlocks.push({ type: 'text', text: part.text });
319
+ }
320
+ }
321
+ }
322
+
323
+ if (item.type === 'output_text' && typeof item.text === 'string') {
324
+ contentBlocks.push({ type: 'text', text: item.text });
325
+ }
326
+
327
+ if (item.type === 'tool_call' || item.type === 'function_call' || item?.name) {
328
+ let parsedArgs: unknown = item.arguments ?? item.input;
329
+ if (typeof parsedArgs === 'string') {
330
+ try {
331
+ parsedArgs = JSON.parse(parsedArgs);
332
+ } catch {
333
+ // keep string
334
+ }
335
+ }
336
+ contentBlocks.push({
337
+ type: 'tool_use',
338
+ id: item.id || item.tool_call_id,
339
+ name: item.name || item.function?.name || 'tool',
340
+ input: parsedArgs,
341
+ });
342
+ }
343
+ }
344
+
345
+ return contentBlocks;
346
+ };
347
+
348
+ export const transformOpenAIResponsesToClaude = (body: any) => {
349
+ const responseBody = body?.response ?? body;
350
+ const contentBlocks = extractOutputItems(responseBody);
351
+ const usage = responseBody?.usage;
352
+ const inputTokens = usage?.input_tokens ?? usage?.prompt_tokens ?? 0;
353
+ const outputTokens = usage?.output_tokens ?? usage?.completion_tokens ?? 0;
354
+ const cacheRead = usage?.cache_read_input_tokens ?? usage?.prompt_tokens_details?.cached_tokens ?? 0;
355
+
356
+ return {
357
+ id: responseBody?.id,
358
+ type: 'message',
359
+ role: 'assistant',
360
+ model: responseBody?.model,
361
+ content: contentBlocks,
362
+ stop_reason: 'end_turn',
363
+ stop_sequence: null,
364
+ usage: {
365
+ input_tokens: inputTokens,
366
+ output_tokens: outputTokens,
367
+ cache_read_input_tokens: cacheRead,
368
+ },
369
+ };
370
+ };
371
+
372
+ export const transformClaudeResponseToOpenAIResponses = (body: any) => {
373
+ const contentBlocks = Array.isArray(body?.content) ? body.content : [];
374
+ const outputTextParts: string[] = [];
375
+ const toolCalls: Array<{ id: string; name: string; arguments: string }> = [];
376
+
377
+ for (const block of contentBlocks) {
378
+ if (!block || typeof block !== 'object') continue;
379
+ if (block.type === 'text' && typeof (block as any).text === 'string') {
380
+ outputTextParts.push((block as any).text);
381
+ }
382
+ if (block.type === 'tool_use') {
383
+ const args = (block as any).input ?? {};
384
+ toolCalls.push({
385
+ id: (block as any).id || `tool_${toolCalls.length + 1}`,
386
+ name: (block as any).name || 'tool',
387
+ arguments: typeof args === 'string' ? args : JSON.stringify(args),
388
+ });
389
+ }
390
+ }
391
+
392
+ const outputText = outputTextParts.join('');
393
+ const outputItems: any[] = [];
394
+ if (outputText) {
395
+ outputItems.push({
396
+ type: 'message',
397
+ role: 'assistant',
398
+ content: [{ type: 'output_text', text: outputText }],
399
+ });
400
+ }
401
+
402
+ for (const toolCall of toolCalls) {
403
+ outputItems.push({
404
+ type: 'tool_call',
405
+ id: toolCall.id,
406
+ name: toolCall.name,
407
+ arguments: toolCall.arguments,
408
+ });
409
+ }
410
+
411
+ const inputTokens = body?.usage?.input_tokens ?? 0;
412
+ const cacheRead = body?.usage?.cache_read_input_tokens ?? 0;
413
+ const outputTokens = body?.usage?.output_tokens ?? 0;
414
+ const usage = {
415
+ input_tokens: inputTokens + cacheRead,
416
+ output_tokens: outputTokens,
417
+ total_tokens: inputTokens + cacheRead + outputTokens,
418
+ };
419
+
420
+ return {
421
+ id: body?.id,
422
+ object: 'response',
423
+ model: body?.model,
424
+ output: outputItems,
425
+ output_text: outputText,
426
+ status: 'completed',
427
+ usage,
428
+ };
429
+ };
430
+
431
+ export const extractTokenUsageFromOpenAIResponsesUsage = (usage: any): TokenUsage | undefined => {
432
+ if (!usage) return undefined;
433
+ const inputTokens = usage.input_tokens ?? usage.prompt_tokens ?? 0;
434
+ const outputTokens = usage.output_tokens ?? usage.completion_tokens ?? 0;
435
+ return {
436
+ inputTokens,
437
+ outputTokens,
438
+ totalTokens: usage.total_tokens ?? inputTokens + outputTokens,
439
+ };
440
+ };