-
Notifications
You must be signed in to change notification settings - Fork 662
fix(adapters): reject malformed nested response shapes #1332
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -163,6 +163,18 @@ function invalidChoicesEvent(usage?: OcxUsage): Extract<AdapterEvent, { type: "e | |
| }; | ||
| } | ||
|
|
||
| function invalidToolCallsEvent(usage?: OcxUsage): Extract<AdapterEvent, { type: "error" }> { | ||
| return { | ||
| type: "error", | ||
| message: "upstream response contained invalid tool calls", | ||
| ...(usage !== undefined ? { usage } : {}), | ||
| }; | ||
| } | ||
|
|
||
| function isRecord(value: unknown): value is Record<string, unknown> { | ||
| return value !== null && typeof value === "object" && !Array.isArray(value); | ||
| } | ||
|
|
||
| function developerSystemText(message: OcxMessage): string | undefined { | ||
| if (message.role !== "developer") return undefined; | ||
| if (typeof message.content === "string") return message.content; | ||
|
|
@@ -916,9 +928,23 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd | |
| yield { type: "text_delta", text: delta.content }; | ||
| } | ||
|
|
||
| const toolCalls = delta.tool_calls as { index?: number; id?: string; function?: { name?: string; arguments?: string } }[] | undefined; | ||
| if (toolCalls) { | ||
| for (const tc of toolCalls) { | ||
| const rawToolCalls = delta.tool_calls; | ||
| if (rawToolCalls !== undefined) { | ||
| // A claimed tool-call payload is not benign padding. Dropping it can leave the | ||
| // matching result permanently orphaned, so malformed nested shapes fail closed | ||
| // through the adapter error channel instead of escaping as TypeError (#1325). | ||
| if (!Array.isArray(rawToolCalls)) { | ||
| return yield* terminateWithError(invalidToolCallsEvent(pendingUsage)); | ||
| } | ||
| for (const rawToolCall of rawToolCalls) { | ||
| if (!isRecord(rawToolCall)) { | ||
| return yield* terminateWithError(invalidToolCallsEvent(pendingUsage)); | ||
| } | ||
| const tc = rawToolCall as { | ||
| index?: number; | ||
| id?: string; | ||
| function?: { name?: string; arguments?: string }; | ||
| }; | ||
|
Comment on lines
+931
to
+947
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Validate nested streaming tool-call fields and final call state. Lines 943-947 only validate that each entry is an object. A fragment such as Accept partial fragments when fields are absent. If a field is present, require its expected type. Before 🤖 Prompt for AI Agents |
||
| const key = typeof tc.index === "number" | ||
| ? `i:${tc.index}` | ||
| : tc.id | ||
|
|
@@ -1073,11 +1099,21 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd | |
| const reasoningText = reasoningTextFrom(msg); | ||
| if (reasoningText !== undefined) events.push({ type: "reasoning_raw_delta", text: reasoningText }); | ||
| if (typeof msg.content === "string") events.push({ type: "text_delta", text: msg.content }); | ||
| const toolCalls = msg.tool_calls as { id: string; function: { name: string; arguments: string } }[] | undefined; | ||
| if (toolCalls) { | ||
| for (const tc of toolCalls) { | ||
| events.push({ type: "tool_call_start", id: tc.id, name: tc.function.name }); | ||
| events.push({ type: "tool_call_delta", arguments: tc.function.arguments }); | ||
| const rawToolCalls = msg.tool_calls; | ||
| if (rawToolCalls !== undefined) { | ||
| if (!Array.isArray(rawToolCalls)) return [invalidToolCallsEvent(usage)]; | ||
| for (const rawToolCall of rawToolCalls) { | ||
| if (!isRecord(rawToolCall) || !isRecord(rawToolCall.function)) { | ||
| return [invalidToolCallsEvent(usage)]; | ||
| } | ||
| const id = rawToolCall.id; | ||
| const name = rawToolCall.function.name; | ||
| const args = rawToolCall.function.arguments; | ||
| if (typeof id !== "string" || typeof name !== "string" || typeof args !== "string") { | ||
| return [invalidToolCallsEvent(usage)]; | ||
| } | ||
| events.push({ type: "tool_call_start", id, name }); | ||
| events.push({ type: "tool_call_delta", arguments: args }); | ||
| events.push({ type: "tool_call_end" }); | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Validate
candidate.content.partsbefore iterating.The candidate guard does not validate
content.parts. A frame withparts: {}throws at Line 615. A frame withparts: [null]throws at Line 616. The outer catch rethrows these errors because they are not translator-budget errors.Allow absent
contentorpartsframes. Ifpartsis present, reject a non-array container or non-object part through the terminal adapter error channel. Add regression cases for both shapes.🤖 Prompt for AI Agents