-
Notifications
You must be signed in to change notification settings - Fork 1k
fix(responses): restore native function argument and namespace parity #3703
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
Merged
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
890413a
docs: specify native function completion parity interfaces
invalid-email-address a51674f
Merge branch 'codex/grok-responses-patch-5598' into codex/grok-native…
invalid-email-address 7b5cb0d
fix(responses): restore native function argument and namespace parity
invalid-email-address e84d3d2
test(responses): supply route log context in parity regression
invalid-email-address f121348
fix(responses): preserve sparse and discovered tool completion parity
invalid-email-address a3f0cab
test(responses): retain dotted namespace alias in compaction inventory
invalid-email-address ea67341
Merge branch 'codex/grok-responses-patch-5598' into codex/grok-native…
invalid-email-address 96ee3ef
fix(responses): compare duplicate repair schemas structurally
invalid-email-address 2d2c4b8
fix(responses): reject mismatched namespace call kinds
invalid-email-address 848543e
fix(responses): preserve original custom kind through namespace lowering
invalid-email-address e71fe15
Merge branch 'codex/grok-responses-patch-5598' into codex/grok-native…
invalid-email-address d88b4fb
Merge branch 'codex/grok-responses-patch-5598' into codex/grok-native…
invalid-email-address ad335db
Merge branch 'codex/grok-responses-patch-5598' into codex/grok-native…
invalid-email-address File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,173 @@ | ||
| import { coerceIntegerToolArguments } from "../lib/tool-argument-integers"; | ||
| import { namespacedToolName } from "../types/tools"; | ||
| import { rewriteRoutedNamespaceToolsForUpstream } from "./namespace-tool-compat"; | ||
| import { collectResponsesToolGroups } from "./tool-groups"; | ||
|
|
||
| export interface FunctionCallRepairSchema { | ||
| name: string; | ||
| namespace?: string; | ||
| parameters?: Record<string, unknown>; | ||
| } | ||
|
|
||
| /** Keys are canonical original identities, never a bare-name fallback for a namespace. */ | ||
| export type FunctionCallRepairSchemas = ReadonlyMap<string, FunctionCallRepairSchema>; | ||
|
|
||
| function isObject(value: unknown): value is Record<string, unknown> { | ||
| return value !== null && typeof value === "object" && !Array.isArray(value); | ||
| } | ||
|
|
||
| /** JSON object member order is immaterial; array elements retain their exact order. */ | ||
| function sameSchemaValue(left: unknown, right: unknown): boolean { | ||
| if (left === right) return true; | ||
| if (Array.isArray(left) || Array.isArray(right)) { | ||
| return Array.isArray(left) && Array.isArray(right) && left.length === right.length | ||
| && left.every((value, index) => sameSchemaValue(value, right[index])); | ||
| } | ||
| if (!isObject(left) || !isObject(right)) return false; | ||
| const keys = Object.keys(left); | ||
| return keys.length === Object.keys(right).length | ||
| && keys.every(key => Object.hasOwn(right, key) && sameSchemaValue(left[key], right[key])); | ||
| } | ||
|
|
||
| function namespaceOf(value: unknown): string | undefined { | ||
| return typeof value === "string" && value !== "functions" ? value : undefined; | ||
| } | ||
|
|
||
| function selectorAllows( | ||
| selector: unknown, | ||
| lowered: unknown, | ||
| wireName: string, | ||
| identity: FunctionCallRepairSchema, | ||
| ): boolean { | ||
| if (!isObject(selector) || selector.type !== "function" || typeof selector.name !== "string") return false; | ||
| if ("namespace" in selector) { | ||
| if (typeof selector.namespace !== "string" || selector.namespace.length === 0) return false; | ||
| return namespaceOf(selector.namespace) === identity.namespace && selector.name === identity.name; | ||
| } | ||
| return isObject(lowered) && lowered.type === "function" && lowered.name === wireName; | ||
| } | ||
|
|
||
| /** Caller supplies currentTurnWireToolCatalogBody BEFORE provider schema lowering. */ | ||
| export function collectFunctionCallRepairSchemas(body: unknown): Map<string, FunctionCallRepairSchema> { | ||
| const schemas = new Map<string, FunctionCallRepairSchema>(); | ||
| if (!isObject(body)) return schemas; | ||
| const groups = collectResponsesToolGroups(body); | ||
| if (Array.isArray(body.input)) { | ||
| for (const entry of body.input) { | ||
| if (isObject(entry) && entry.type === "tool_search_output" && Array.isArray(entry.tools)) groups.push(entry.tools); | ||
| } | ||
| } | ||
| // Reuse namespace selector resolution, retaining schemas from the original objects below. | ||
| // This local catalog view includes loaded definitions without revisiting replay history or | ||
| // teaching the shared tool-group collector a new transport-wide interpretation. | ||
| const lowered = rewriteRoutedNamespaceToolsForUpstream({ ...body, tools: groups.flat(), input: [] }).body; | ||
| const choice = body.tool_choice; | ||
| const loweredChoice = isObject(lowered) ? lowered.tool_choice : undefined; | ||
| const occupied = new Map<string, { kind: unknown; identity: FunctionCallRepairSchema } | null>(); | ||
| const register = (tool: unknown, namespace?: string): void => { | ||
| if (!isObject(tool) || typeof tool.name !== "string" || tool.name.length === 0) return; | ||
| const identity: FunctionCallRepairSchema = { | ||
| name: tool.name, | ||
| ...(namespace ? { namespace } : {}), | ||
| ...(isObject(tool.parameters) ? { parameters: tool.parameters } : {}), | ||
| }; | ||
| const key = namespacedToolName(namespace, tool.name); | ||
| if (occupied.has(key)) { | ||
| const previous = occupied.get(key); | ||
| // Conflicting duplicate declarations cannot choose a schema by insertion order. | ||
| if (!previous || previous.kind !== tool.type | ||
| || previous.identity.namespace !== namespace | ||
| || previous.identity.name !== tool.name | ||
| || !sameSchemaValue(previous.identity.parameters, identity.parameters)) { | ||
| occupied.set(key, null); | ||
| } | ||
| } else occupied.set(key, { kind: tool.type, identity }); | ||
| }; | ||
| for (const group of groups) { | ||
| for (const tool of group) { | ||
| if (!isObject(tool)) continue; | ||
| if (tool.type === "namespace") { | ||
| if (typeof tool.name !== "string" || !tool.name || !Array.isArray(tool.tools)) continue; | ||
| for (const child of tool.tools) register(child, namespaceOf(tool.name)); | ||
| } else if (tool.type === "function" && isObject(tool.function)) { | ||
| register({ ...tool.function, type: "function" }); | ||
| } else register(tool); | ||
| } | ||
| } | ||
| for (const [key, entry] of occupied) { | ||
| if (!entry || entry.kind !== "function") continue; | ||
| let allowed = choice === undefined || choice === "auto" || choice === "required"; | ||
| if (isObject(choice)) { | ||
| if (choice.type === "allowed_tools" && Array.isArray(choice.tools)) { | ||
| const selectors = isObject(loweredChoice) && Array.isArray(loweredChoice.tools) ? loweredChoice.tools : []; | ||
| allowed = choice.tools.some((selector, index) => selectorAllows(selector, selectors[index], key, entry.identity)); | ||
| } else allowed = selectorAllows(choice, loweredChoice, key, entry.identity); | ||
| } | ||
| if (allowed) schemas.set(key, entry.identity); | ||
| } | ||
| return schemas; | ||
| } | ||
|
|
||
| function repairItem(item: unknown, schemas: FunctionCallRepairSchemas, completed: boolean): unknown { | ||
| if (!isObject(item) || item.type !== "function_call" || typeof item.name !== "string" | ||
| || typeof item.arguments !== "string") return item; | ||
| if (item.status !== "completed" && !(item.status === undefined && completed)) return item; | ||
| if ("namespace" in item && (typeof item.namespace !== "string" || !item.namespace)) return item; | ||
| const namespace = namespaceOf(item.namespace); | ||
| const schema = schemas.get(namespacedToolName(namespace, item.name)); | ||
| if (!schema) return item; | ||
| // An explicit namespace is an identity coordinate, not another spelling to guess at. | ||
| if ("namespace" in item && (schema.namespace !== namespace || schema.name !== item.name)) return item; | ||
| const raw = item.arguments; | ||
| if (raw !== "") { | ||
| try { | ||
| let unsafe = false; | ||
| JSON.parse(raw, (_key, value: unknown) => { | ||
| if (typeof value === "number" && (!Number.isFinite(value) | ||
| || (Number.isInteger(value) && !Number.isSafeInteger(value)))) unsafe = true; | ||
| return value; | ||
| }); | ||
| // Re-stringifying another repaired field must not round an unsafe sibling number. | ||
| if (unsafe) return item; | ||
| } catch { return item; } | ||
| } | ||
| const argumentsText = coerceIntegerToolArguments(raw || "{}", schema.parameters, schema.namespace ? undefined : schema.name); | ||
| return argumentsText === raw ? item : { ...item, arguments: argumentsText }; | ||
| } | ||
|
|
||
| /** Only executable completion slots are visited; metadata and custom input are opaque. */ | ||
| export function repairFunctionCalls( | ||
| value: unknown, | ||
| schemas: FunctionCallRepairSchemas, | ||
| ): { value: unknown; changed: boolean } { | ||
| if (schemas.size === 0 || !isObject(value)) return { value, changed: false }; | ||
| if (typeof value.status === "string" && ["failed", "incomplete", "cancelled", "in_progress", "queued"].includes(value.status)) return { value, changed: false }; | ||
| let next: unknown = value; | ||
| if (value.type === "function_call") next = repairItem(value, schemas, false); | ||
| else if (value.type === "response.output_item.done") { | ||
| const item = repairItem(value.item, schemas, true); | ||
| if (item !== value.item) next = { ...value, item }; | ||
| } else if (value.type === "response.completed" && isObject(value.response)) { | ||
| const response = value.response; | ||
| if ((response.status === undefined || response.status === "completed") && Array.isArray(response.output)) { | ||
| const original = response.output; | ||
| const output = original.map(item => repairItem(item, schemas, true)); | ||
| if (output.some((item, index) => item !== original[index])) next = { ...value, response: { ...response, output } }; | ||
| } | ||
| } else if (typeof value.type !== "string" || !value.type.startsWith("response.")) { | ||
| if (Array.isArray(value.output)) { | ||
| const original = value.output; | ||
| const output = original.map(item => repairItem(item, schemas, value.status === "completed")); | ||
| if (output.some((item, index) => item !== original[index])) next = { ...value, output }; | ||
| } | ||
| } | ||
| return { value: next, changed: next !== value }; | ||
| } | ||
|
|
||
| export function repairFunctionCallsInJson(text: string, schemas: FunctionCallRepairSchemas): string { | ||
| if (schemas.size === 0) return text; | ||
| let payload: unknown; | ||
| try { payload = JSON.parse(text); } catch { return text; } | ||
| const repaired = repairFunctionCalls(payload, schemas); | ||
| return repaired.changed ? JSON.stringify(repaired.value) : text; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.