Skip to content

Commit 44a6088

Browse files
committed
fix malformed handling
1 parent 1d28b05 commit 44a6088

File tree

4 files changed

+94
-31
lines changed

4 files changed

+94
-31
lines changed

packages/cost/models/providers/base.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,12 @@ export abstract class BaseProvider {
5555
endpoint: Endpoint,
5656
context: RequestBodyContext
5757
): string | Promise<string> {
58+
let updatedBody = context.parsedBody;
59+
if (context.bodyMapping === "RESPONSES") {
60+
updatedBody = context.toChatCompletions(updatedBody);
61+
}
5862
return JSON.stringify({
59-
...context.parsedBody,
63+
...updatedBody,
6064
model: endpoint.providerModelId,
6165
});
6266
}

packages/cost/models/providers/index.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,34 @@ export const providers = {
4040
export type ModelProviderName = keyof typeof providers;
4141

4242
// TODO: temporarily whitelist responses API providers until all mappings are done
43-
export const ResponsesAPIEnabledProviders: ModelProviderName[] = ["openai", "helicone", "vertex"];
43+
export const ResponsesAPIEnabledProviders: ModelProviderName[] = [
44+
"openai",
45+
"helicone",
46+
47+
// chat completions only
48+
"azure",
49+
"chutes",
50+
"cohere",
51+
"deepinfra",
52+
"deepseek",
53+
54+
// has known issues with returning structured JSONS
55+
// should be okay to enable, but its not stable enough to add without request
56+
// "google-ai-studio",
57+
58+
"groq",
59+
"nebius",
60+
"novita",
61+
"openrouter",
62+
"perplexity",
63+
"xai",
64+
65+
// anthropic and chat completions provider
66+
"vertex"
67+
68+
// anthropic only
69+
// none right now, need anthropic mapper
70+
];
4471

4572
// Re-export base for extending
4673
export { BaseProvider } from "./base";

packages/llm-mapper/transform/providers/responses/streamedResponse/toResponses.ts

Lines changed: 60 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export class ChatToResponsesStreamConverter {
1919
private itemAdded: boolean = false;
2020
private partAdded: boolean = false;
2121
private emittedFunctionItems: Set<string> = new Set();
22+
private completedEmitted: boolean = false;
2223

2324
constructor() {
2425
this.toolCalls = new Map();
@@ -139,28 +140,27 @@ export class ChatToResponsesStreamConverter {
139140
}
140141
}
141142

142-
// if finish reason was sent for this choice, emit text.done
143+
// if finish reason was sent for this choice, emit done + completed
143144
if (choice?.finish_reason) {
144-
const doneEvt: ResponseOutputTextDoneEvent = {
145-
type: "response.output_text.done",
146-
item_id: `msg_${this.responseId}`,
147-
output_index: 0,
148-
content_index: 0,
149-
text: this.textBuffer,
150-
};
151-
events.push(doneEvt);
152-
153-
// close part and item
154-
if (this.partAdded) {
155-
events.push({
156-
type: "response.content_part.done",
145+
if (this.itemAdded) {
146+
const doneEvt: ResponseOutputTextDoneEvent = {
147+
type: "response.output_text.done",
157148
item_id: `msg_${this.responseId}`,
158149
output_index: 0,
159150
content_index: 0,
160-
part: { type: "output_text", text: this.textBuffer, annotations: [] },
161-
} as any);
162-
}
163-
if (this.itemAdded) {
151+
text: this.textBuffer,
152+
};
153+
events.push(doneEvt);
154+
155+
if (this.partAdded) {
156+
events.push({
157+
type: "response.content_part.done",
158+
item_id: `msg_${this.responseId}`,
159+
output_index: 0,
160+
content_index: 0,
161+
part: { type: "output_text", text: this.textBuffer, annotations: [] },
162+
} as any);
163+
}
164164
events.push({
165165
type: "response.output_item.done",
166166
output_index: 0,
@@ -178,14 +178,12 @@ export class ChatToResponsesStreamConverter {
178178

179179
// Finalize any function calls
180180
this.toolCalls.forEach((tc) => {
181-
// Done event with full arguments
182181
events.push({
183182
type: "response.function_call_arguments.done",
184183
item_id: tc.item_id,
185184
output_index: 0,
186185
arguments: tc.arguments || "{}",
187186
} as any);
188-
// Output item done
189187
events.push({
190188
type: "response.output_item.done",
191189
output_index: 0,
@@ -200,11 +198,50 @@ export class ChatToResponsesStreamConverter {
200198
},
201199
} as any);
202200
});
201+
202+
// Emit completed now if usage not provided
203+
const usage = this.finalUsage || undefined;
204+
const completed: ResponseCompletedEvent = {
205+
type: "response.completed",
206+
response: {
207+
id: this.responseId,
208+
object: "response",
209+
created: this.created,
210+
created_at: this.created as any,
211+
status: "completed" as any,
212+
model: this.model,
213+
output: [
214+
...(this.itemAdded
215+
? ([
216+
{
217+
type: "message" as const,
218+
role: "assistant" as const,
219+
content: [
220+
{ type: "output_text" as const, text: this.textBuffer },
221+
],
222+
},
223+
] as any)
224+
: []),
225+
...Array.from(this.toolCalls.values()).map((tc) => ({
226+
id: `fc_${tc.id}`,
227+
type: "function_call" as const,
228+
status: "completed" as const,
229+
name: tc.name || "",
230+
call_id: tc.id,
231+
arguments: tc.arguments || "{}",
232+
parsed_arguments: null,
233+
})),
234+
],
235+
...(usage ? { usage } : {}),
236+
},
237+
};
238+
events.push(completed);
239+
this.completedEmitted = true;
203240
}
204241
}
205242

206-
// usage usagealley (haha...?) arrives in the final chunk with empty choices
207-
if (c.usage) {
243+
// usage arrives in the final chunk with empty choices
244+
if (c.usage && !this.completedEmitted) {
208245
const usage: ResponsesUsage = {
209246
input_tokens: c.usage.prompt_tokens,
210247
output_tokens: c.usage.completion_tokens,
@@ -253,6 +290,7 @@ export class ChatToResponsesStreamConverter {
253290
},
254291
};
255292
events.push(completed);
293+
this.completedEmitted = true;
256294
}
257295

258296
return events;

worker/src/lib/clients/llmmapper/router/oaiChat2responses/stream.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,12 @@ export function oaiChat2responsesStream(
3737
if (line.startsWith("data: ")) {
3838
const data = line.slice(6);
3939
if (data.trim() === "[DONE]") {
40-
// Ensure stream termination; Responses API may end with response.completed
41-
if (!emittedCompleted) {
42-
// If the converter hasn't emitted a response.completed (e.g., no usage)
43-
// we just end the stream. Clients also listen for "event: done".
44-
controller.enqueue(encoder.encode("event: done\n\n"));
45-
}
40+
// end of upstream stream, so response.completed should have been emitted by converter.
4641
continue;
4742
}
4843

4944
try {
5045
const chunk = JSON.parse(data) as ChatCompletionChunk;
51-
console.log("chunk", JSON.stringify(chunk, null, 2));
5246
const events = converter.convert(chunk);
5347
for (const ev of events) {
5448
const type = (ev as any).type;

0 commit comments

Comments
 (0)