Skip to content

Commit 2c7aaf3

Browse files
address dallin's feedback
1 parent 851a01c commit 2c7aaf3

File tree

3 files changed

+60
-106
lines changed

3 files changed

+60
-106
lines changed

extensions/cli/package-lock.json

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

extensions/cli/src/stream/streamChatResponse.test.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,6 @@ import type { ChatHistoryItem } from "core/index.js";
44
import type { ChatCompletionChunk } from "openai/resources/chat/completions.mjs";
55
import { vi } from "vitest";
66

7-
vi.mock("fdir", () => ({
8-
fdir: class {
9-
withBasePath() {
10-
return this;
11-
}
12-
filter() {
13-
return this;
14-
}
15-
crawl() {
16-
return new Set<string>();
17-
}
18-
},
19-
}));
20-
217
import { toolPermissionManager } from "../permissions/permissionManager.js";
228
import { ToolCall } from "../tools/index.js";
239
import { readFileTool } from "../tools/readFile.js";

packages/openai-adapters/src/apis/openaiResponses.ts

Lines changed: 58 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
1+
import type { CompletionUsage } from "openai/resources/index.js";
12
import {
23
ChatCompletion,
4+
ChatCompletionAssistantMessageParam,
35
ChatCompletionChunk,
6+
ChatCompletionContentPart,
7+
ChatCompletionContentPartImage,
8+
ChatCompletionContentPartInputAudio,
9+
ChatCompletionContentPartRefusal,
10+
ChatCompletionContentPartText,
411
ChatCompletionCreateParams,
5-
ChatCompletionCreateParamsNonStreaming,
612
ChatCompletionCreateParamsStreaming,
713
ChatCompletionMessageParam,
14+
ChatCompletionMessageToolCall,
815
ChatCompletionTool,
916
} from "openai/resources/index.js";
10-
import type { CompletionUsage } from "openai/resources/index.js";
1117
import {
1218
Response,
1319
ResponseCreateParams,
1420
ResponseFunctionCallArgumentsDoneEvent,
15-
ResponseFunctionCallArgumentsDeltaEvent,
1621
ResponseIncompleteEvent,
1722
ResponseInput,
1823
ResponseInputAudio,
@@ -25,9 +30,7 @@ import {
2530
ResponseOutputRefusal,
2631
ResponseOutputText,
2732
ResponseReasoningSummaryTextDeltaEvent,
28-
ResponseReasoningTextDeltaEvent,
2933
ResponseStreamEvent,
30-
ResponseTextDeltaEvent,
3134
ResponseUsage,
3235
} from "openai/resources/responses/responses.js";
3336

@@ -37,97 +40,55 @@ export function isResponsesModel(model: string): boolean {
3740
return !!model && RESPONSES_MODEL_REGEX.test(model);
3841
}
3942

40-
function ensureText(value: string | null | undefined): string {
41-
if (value === undefined || value === null) {
42-
return " ";
43-
}
44-
return value === "" ? " " : value;
45-
}
46-
4743
function convertTextPart(text: string): ResponseInputText {
4844
return {
45+
text,
4946
type: "input_text",
50-
text: ensureText(text),
5147
};
5248
}
5349

5450
function convertImagePart(
55-
image: ChatCompletionMessageParam["content"],
56-
): ResponseInputImage | undefined {
57-
if (
58-
Array.isArray(image) ||
59-
!image ||
60-
typeof image !== "object" ||
61-
(image as any).type !== "image_url"
62-
) {
63-
return undefined;
64-
}
65-
const imageUrlPart = image as any;
51+
image: ChatCompletionContentPartImage,
52+
): ResponseInputImage {
6653
const converted: ResponseInputImage = {
6754
type: "input_image",
68-
image_url: imageUrlPart.image_url?.url,
69-
detail: imageUrlPart.image_url?.detail ?? "auto",
55+
image_url: image.image_url.url,
56+
detail: image.image_url.detail ?? "auto",
7057
};
71-
if (imageUrlPart.image_url?.file_id) {
72-
(converted as any).file_id = imageUrlPart.image_url.file_id;
58+
if ((image.image_url as any).file_id) {
59+
(converted as any).file_id = (image.image_url as any).file_id;
7360
}
7461
return converted;
7562
}
7663

7764
function convertAudioPart(
78-
part: ChatCompletionMessageParam["content"],
79-
): ResponseInputAudio | undefined {
80-
if (
81-
Array.isArray(part) ||
82-
!part ||
83-
typeof part !== "object" ||
84-
(part as any).type !== "input_audio"
85-
) {
86-
return undefined;
87-
}
88-
const audio = part as any;
89-
if (!audio.input_audio) {
90-
return undefined;
91-
}
65+
part: ChatCompletionContentPartInputAudio,
66+
): ResponseInputAudio {
9267
return {
9368
type: "input_audio",
9469
input_audio: {
95-
data: audio.input_audio.data,
96-
format: audio.input_audio.format,
70+
data: part.input_audio.data,
71+
format: part.input_audio.format,
9772
},
9873
};
9974
}
10075

10176
function convertFilePart(
102-
part: ChatCompletionMessageParam["content"],
103-
): ResponseInputFile | undefined {
104-
if (
105-
Array.isArray(part) ||
106-
!part ||
107-
typeof part !== "object" ||
108-
(part as any).type !== "file"
109-
) {
110-
return undefined;
111-
}
112-
113-
const filePart = part as any;
114-
const file = filePart.file;
115-
if (!file) {
116-
return undefined;
117-
}
77+
part: ChatCompletionContentPart.File,
78+
): ResponseInputFile {
11879
return {
11980
type: "input_file",
120-
file_id: file.file_id ?? undefined,
121-
file_data: file.file_data ?? undefined,
122-
filename: file.filename ?? undefined,
123-
file_url: file.file_url ?? undefined,
81+
file_id: part.file.file_id ?? undefined,
82+
file_data: part.file.file_data ?? undefined,
83+
filename: part.file.filename ?? undefined,
84+
file_url: (part.file as any).file_url ?? undefined,
12485
};
12586
}
12687

12788
function convertMessageContentPart(
128-
part: any,
89+
part: ChatCompletionContentPart | ChatCompletionContentPartRefusal,
12990
): ResponseInputContent | undefined {
130-
switch (part?.type) {
91+
switch (part.type) {
13192
case "text":
13293
return convertTextPart(part.text);
13394
case "image_url":
@@ -136,6 +97,9 @@ function convertMessageContentPart(
13697
return convertAudioPart(part);
13798
case "file":
13899
return convertFilePart(part);
100+
case "refusal":
101+
// Skip refusal parts - they're not input content
102+
return undefined;
139103
default:
140104
return undefined;
141105
}
@@ -168,14 +132,13 @@ function createOutputTextPart(
168132
text: string,
169133
source?: Partial<ResponseOutputText>,
170134
): AssistantContentPart {
171-
const normalizedText = ensureText(text);
172135
const annotations =
173136
Array.isArray(source?.annotations) && source.annotations.length > 0
174137
? source.annotations
175138
: [];
176139
const part: ResponseOutputText = {
140+
text,
177141
type: "output_text",
178-
text: normalizedText,
179142
annotations,
180143
};
181144
if (Array.isArray(source?.logprobs) && source.logprobs.length > 0) {
@@ -186,8 +149,8 @@ function createOutputTextPart(
186149

187150
function createRefusalPart(refusal: string): AssistantContentPart {
188151
return {
152+
refusal,
189153
type: "refusal",
190-
refusal: ensureText(refusal),
191154
};
192155
}
193156

@@ -203,29 +166,33 @@ function collectAssistantContentParts(
203166
}
204167
} else if (Array.isArray(content)) {
205168
for (const rawPart of content) {
206-
const part: any = rawPart as any;
169+
// Content array should be ChatCompletionContentPartText | ChatCompletionContentPartRefusal
170+
// but we handle "output_text" type which may come from Response API conversions
171+
const part = rawPart as
172+
| ChatCompletionContentPartText
173+
| ChatCompletionContentPartRefusal
174+
| { type: "output_text"; text: string };
207175
if (!part) {
208176
continue;
209177
}
210-
if (typeof part === "string") {
211-
if (part.trim().length > 0) {
212-
parts.push(createOutputTextPart(part));
213-
}
214-
continue;
215-
}
216-
const partType: string | undefined = part.type;
178+
179+
const partType = part.type;
217180
if (partType === "text") {
218-
const textValue = part.text;
219-
if (typeof textValue === "string" && textValue.trim().length > 0) {
220-
parts.push(createOutputTextPart(textValue, part));
181+
const textPart = part as ChatCompletionContentPartText;
182+
if (
183+
typeof textPart.text === "string" &&
184+
textPart.text.trim().length > 0
185+
) {
186+
parts.push(createOutputTextPart(textPart.text));
221187
}
222188
} else if (partType === "output_text") {
223-
const textValue = part.text;
189+
const textValue = (part as { type: "output_text"; text: string }).text;
224190
if (typeof textValue === "string" && textValue.trim().length > 0) {
225-
parts.push(createOutputTextPart(textValue, part));
191+
parts.push(createOutputTextPart(textValue));
226192
}
227193
} else if (partType === "refusal") {
228-
const refusalText = part.refusal ?? part.text;
194+
const refusalPart = part as ChatCompletionContentPartRefusal;
195+
const refusalText = refusalPart.refusal;
229196
if (typeof refusalText === "string" && refusalText.trim().length > 0) {
230197
parts.push(createRefusalPart(refusalText));
231198
}
@@ -364,9 +331,10 @@ export function toResponsesInput(
364331
}
365332

366333
if (message.role === "assistant") {
334+
const assistantMessage = message as ChatCompletionAssistantMessageParam;
367335
const assistantContentParts = collectAssistantContentParts(
368-
message.content,
369-
(message as any).refusal ?? (message as any).declination ?? null,
336+
assistantMessage.content,
337+
assistantMessage.refusal ?? null,
370338
);
371339
if (assistantContentParts.length > 0) {
372340
const providedId = (message as any).id;
@@ -382,16 +350,15 @@ export function toResponsesInput(
382350
status: "completed",
383351
} as ResponseOutputMessage as any);
384352
}
385-
if (message.tool_calls?.length) {
386-
message.tool_calls.forEach((toolCall, index) => {
387-
if (toolCall.type === "function" && (toolCall as any).function) {
388-
const func = (toolCall as any).function;
353+
if (assistantMessage.tool_calls?.length) {
354+
assistantMessage.tool_calls.forEach((toolCall, index) => {
355+
if (toolCall.type === "function") {
389356
const callId = toolCall.id ?? `tool_call_${index}`;
390357
const functionCall: any = {
391358
type: "function_call",
392359
call_id: callId,
393-
name: func.name ?? "",
394-
arguments: func.arguments ?? "{}",
360+
name: toolCall.function.name ?? "",
361+
arguments: toolCall.function.arguments ?? "{}",
395362
};
396363
if (
397364
typeof toolCall.id === "string" &&

0 commit comments

Comments
 (0)