Merge pull request #124 from salmanrajz/fix/recursive-schema-normalization
fix: make normalizeSchemaForOpenAI recursive for nested objects
This commit is contained in:
@@ -1,50 +1,53 @@
|
|||||||
import { c as _c } from "react-compiler-runtime";
|
import React from 'react'
|
||||||
import React from 'react';
|
import { Box, Link, Text } from '../ink.js'
|
||||||
import { Box, Link, Text } from '../ink.js';
|
import { Select } from './CustomSelect/index.js'
|
||||||
import { Select } from './CustomSelect/index.js';
|
import { Dialog } from './design-system/Dialog.js'
|
||||||
import { Dialog } from './design-system/Dialog.js';
|
import { getAPIProvider } from '../utils/model/providers.js'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onDone: () => void;
|
onDone: () => void
|
||||||
};
|
}
|
||||||
export function CostThresholdDialog(t0) {
|
|
||||||
const $ = _c(7);
|
function getProviderLabel(): string {
|
||||||
const {
|
const provider = getAPIProvider()
|
||||||
onDone
|
switch (provider) {
|
||||||
} = t0;
|
case 'firstParty':
|
||||||
let t1;
|
return 'Anthropic API'
|
||||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
case 'bedrock':
|
||||||
t1 = <Box flexDirection="column"><Text>Learn more about how to monitor your spending:</Text><Link url="https://code.claude.com/docs/en/costs" /></Box>;
|
return 'AWS Bedrock'
|
||||||
$[0] = t1;
|
case 'vertex':
|
||||||
} else {
|
return 'Google Vertex'
|
||||||
t1 = $[0];
|
case 'foundry':
|
||||||
}
|
return 'Azure Foundry'
|
||||||
let t2;
|
case 'openai':
|
||||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
return 'OpenAI-compatible API'
|
||||||
t2 = [{
|
case 'gemini':
|
||||||
value: "ok",
|
return 'Gemini API'
|
||||||
label: "Got it, thanks!"
|
default:
|
||||||
}];
|
return 'API'
|
||||||
$[1] = t2;
|
}
|
||||||
} else {
|
}
|
||||||
t2 = $[1];
|
|
||||||
}
|
export function CostThresholdDialog({ onDone }: Props): React.ReactNode {
|
||||||
let t3;
|
const providerLabel = getProviderLabel()
|
||||||
if ($[2] !== onDone) {
|
return (
|
||||||
t3 = <Select options={t2} onChange={onDone} />;
|
<Dialog
|
||||||
$[2] = onDone;
|
title={`You've spent $5 on the ${providerLabel} this session.`}
|
||||||
$[3] = t3;
|
onCancel={onDone}
|
||||||
} else {
|
>
|
||||||
t3 = $[3];
|
<Box flexDirection="column">
|
||||||
}
|
<Text>Learn more about how to monitor your spending:</Text>
|
||||||
let t4;
|
<Link url="https://code.claude.com/docs/en/costs" />
|
||||||
if ($[4] !== onDone || $[5] !== t3) {
|
</Box>
|
||||||
t4 = <Dialog title="You've spent $5 on the Anthropic API this session." onCancel={onDone}>{t1}{t3}</Dialog>;
|
<Select
|
||||||
$[4] = onDone;
|
options={[
|
||||||
$[5] = t3;
|
{
|
||||||
$[6] = t4;
|
value: 'ok',
|
||||||
} else {
|
label: 'Got it, thanks!',
|
||||||
t4 = $[6];
|
},
|
||||||
}
|
]}
|
||||||
return t4;
|
onChange={onDone}
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIkJveCIsIkxpbmsiLCJUZXh0IiwiU2VsZWN0IiwiRGlhbG9nIiwiUHJvcHMiLCJvbkRvbmUiLCJDb3N0VGhyZXNob2xkRGlhbG9nIiwidDAiLCIkIiwiX2MiLCJ0MSIsIlN5bWJvbCIsImZvciIsInQyIiwidmFsdWUiLCJsYWJlbCIsInQzIiwidDQiXSwic291cmNlcyI6WyJDb3N0VGhyZXNob2xkRGlhbG9nLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgUmVhY3QgZnJvbSAncmVhY3QnXG5pbXBvcnQgeyBCb3gsIExpbmssIFRleHQgfSBmcm9tICcuLi9pbmsuanMnXG5pbXBvcnQgeyBTZWxlY3QgfSBmcm9tICcuL0N1c3RvbVNlbGVjdC9pbmRleC5qcydcbmltcG9ydCB7IERpYWxvZyB9IGZyb20gJy4vZGVzaWduLXN5c3RlbS9EaWFsb2cuanMnXG5cbnR5cGUgUHJvcHMgPSB7XG4gIG9uRG9uZTogKCkgPT4gdm9pZFxufVxuXG5leHBvcnQgZnVuY3Rpb24gQ29zdFRocmVzaG9sZERpYWxvZyh7IG9uRG9uZSB9OiBQcm9wcyk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIHJldHVybiAoXG4gICAgPERpYWxvZ1xuICAgICAgdGl0bGU9XCJZb3UndmUgc3BlbnQgJDUgb24gdGhlIEFudGhyb3BpYyBBUEkgdGhpcyBzZXNzaW9uLlwiXG4gICAgICBvbkNhbmNlbD17b25Eb25lfVxuICAgID5cbiAgICAgIDxCb3ggZmxleERpcmVjdGlvbj1cImNvbHVtblwiPlxuICAgICAgICA8VGV4dD5MZWFybiBtb3JlIGFib3V0IGhvdyB0byBtb25pdG9yIHlvdXIgc3BlbmRpbmc6PC9UZXh0PlxuICAgICAgICA8TGluayB1cmw9XCJodHRwczovL2NvZGUuY2xhdWRlLmNvbS9kb2NzL2VuL2Nvc3RzXCIgLz5cbiAgICAgIDwvQm94PlxuICAgICAgPFNlbGVjdFxuICAgICAgICBvcHRpb25zPXtbXG4gICAgICAgICAge1xuICAgICAgICAgICAgdmFsdWU6ICdvaycsXG4gICAgICAgICAgICBsYWJlbDogJ0dvdCBpdCwgdGhhbmtzIScsXG4gICAgICAgICAgfSxcbiAgICAgICAgXX1cbiAgICAgICAgb25DaGFuZ2U9e29uRG9uZX1cbiAgICAgIC8+XG4gICAgPC9EaWFsb2c+XG4gIClcbn1cbiJdLCJtYXBwaW5ncyI6IjtBQUFBLE9BQU9BLEtBQUssTUFBTSxPQUFPO0FBQ3pCLFNBQVNDLEdBQUcsRUFBRUMsSUFBSSxFQUFFQyxJQUFJLFFBQVEsV0FBVztBQUMzQyxTQUFTQyxNQUFNLFFBQVEseUJBQXlCO0FBQ2hELFNBQVNDLE1BQU0sUUFBUSwyQkFBMkI7QUFFbEQsS0FBS0MsS0FBSyxHQUFHO0VBQ1hDLE1BQU0sRUFBRSxHQUFHLEdBQUcsSUFBSTtBQUNwQixDQUFDO0FBRUQsT0FBTyxTQUFBQyxvQkFBQUMsRUFBQTtFQUFBLE1BQUFDLENBQUEsR0FBQUMsRUFBQTtFQUE2QjtJQUFBSjtFQUFBLElBQUFFLEVBQWlCO0VBQUEsSUFBQUcsRUFBQTtFQUFBLElBQUFGLENBQUEsUUFBQUcsTUFBQSxDQUFBQyxHQUFBO0lBTS9DRixFQUFBLElBQUMsR0FBRyxDQUFlLGFBQVEsQ0FBUixRQUFRLENBQ3pCLENBQUMsSUFBSSxDQUFDLDhDQUE4QyxFQUFuRCxJQUFJLENBQ0wsQ0FBQyxJQUFJLENBQUssR0FBdUMsQ0FBdkMsdUNBQXVDLEdBQ25ELEVBSEMsR0FBRyxDQUdFO0lBQUFGLENBQUEsTUFBQUUsRUFBQTtFQUFBO0lBQUFBLEVBQUEsR0FBQUYsQ0FBQTtFQUFBO0VBQUEsSUFBQUssRUFBQTtFQUFBLElBQUFMLENBQUEsUUFBQUcsTUFBQSxDQUFBQyxHQUFBO0lBRUtDLEVBQUEsSUFDUDtNQUFBQyxLQUFBLEVBQ1MsSUFBSTtNQUFBQyxLQUFBLEVBQ0o7SUFDVCxDQUFDLENBQ0Y7SUFBQVAsQ0FBQSxNQUFBSyxFQUFBO0VBQUE7SUFBQUEsRUFBQSxHQUFBTCxDQUFBO0VBQUE7RUFBQSxJQUFBUSxFQUFBO0VBQUEsSUFBQVIsQ0FBQSxRQUFBSCxNQUFBO0lBTkhXLEVBQUEsSUFBQyxNQUFNLENBQ0ksT0FLUixDQUxRLENBQUFILEVBS1QsQ0FBQyxDQUNTUixRQUFNLENBQU5BLE9BQUssQ0FBQyxHQUNoQjtJQUFBRyxDQUFBLE1BQUFILE1BQUE7SUFBQUcsQ0FBQSxNQUFBUSxFQUFBO0VBQUE7SUFBQUEsRUFBQSxHQUFBUixDQUFBO0VBQUE7RUFBQSxJQUFBUyxFQUFBO0VBQUEsSUFBQVQsQ0FBQSxRQUFBSCxNQUFBLElBQUFHLENBQUEsUUFBQVEsRUFBQTtJQWhCSkMsRUFBQSxJQUFDLE1BQU0sQ0FDQyxLQUFvRCxDQUFwRCxvREFBb0QsQ0FDaERaLFFBQU0sQ0FBTkEsT0FBSyxDQUFDLENBRWhCLENBQUFLLEVBR0ssQ0FDTCxDQUFBTSxFQVFDLENBQ0gsRUFqQkMsTUFBTSxDQWlCRTtJQUFBUixDQUFBLE1BQUFILE1BQUE7SUFBQUcsQ0FBQSxNQUFBUSxFQUFBO0lBQUFSLENBQUEsTUFBQVMsRUFBQTtFQUFBO0lBQUFBLEVBQUEsR0FBQVQsQ0FBQTtFQUFBO0VBQUEsT0FqQlRTLEVBaUJTO0FBQUEiLCJpZ25vcmVMaXN0IjpbXX0=
|
|
||||||
@@ -235,20 +235,60 @@ function normalizeSchemaForOpenAI(
|
|||||||
schema: Record<string, unknown>,
|
schema: Record<string, unknown>,
|
||||||
strict = true,
|
strict = true,
|
||||||
): Record<string, unknown> {
|
): Record<string, unknown> {
|
||||||
if (schema.type !== 'object' || !schema.properties) return schema
|
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
|
||||||
const properties = schema.properties as Record<string, unknown>
|
return (schema ?? {}) as Record<string, unknown>
|
||||||
const existingRequired = Array.isArray(schema.required) ? schema.required as string[] : []
|
|
||||||
// OpenAI strict mode requires every property to be listed in required[].
|
|
||||||
// Gemini rejects schemas where required[] contains keys absent from properties,
|
|
||||||
// so only promote keys that actually exist in properties.
|
|
||||||
if (strict) {
|
|
||||||
const allKeys = Object.keys(properties)
|
|
||||||
const required = Array.from(new Set([...existingRequired, ...allKeys]))
|
|
||||||
return { ...schema, required }
|
|
||||||
}
|
}
|
||||||
// For Gemini: keep only existing required keys that are present in properties
|
|
||||||
const required = existingRequired.filter(k => k in properties)
|
const record = { ...schema }
|
||||||
return { ...schema, required }
|
|
||||||
|
if (record.type === 'object' && record.properties) {
|
||||||
|
const properties = record.properties as Record<string, Record<string, unknown>>
|
||||||
|
const existingRequired = Array.isArray(record.required) ? record.required as string[] : []
|
||||||
|
|
||||||
|
// Recurse into each property
|
||||||
|
const normalizedProps: Record<string, unknown> = {}
|
||||||
|
for (const [key, value] of Object.entries(properties)) {
|
||||||
|
normalizedProps[key] = normalizeSchemaForOpenAI(
|
||||||
|
value as Record<string, unknown>,
|
||||||
|
strict,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
record.properties = normalizedProps
|
||||||
|
|
||||||
|
if (strict) {
|
||||||
|
// OpenAI strict mode requires every property to be listed in required[]
|
||||||
|
const allKeys = Object.keys(normalizedProps)
|
||||||
|
record.required = Array.from(new Set([...existingRequired, ...allKeys]))
|
||||||
|
// OpenAI strict mode requires additionalProperties: false on all object
|
||||||
|
// schemas — override unconditionally to ensure nested objects comply.
|
||||||
|
record.additionalProperties = false
|
||||||
|
} else {
|
||||||
|
// For Gemini: keep only existing required keys that are present in properties
|
||||||
|
record.required = existingRequired.filter(k => k in normalizedProps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recurse into array items
|
||||||
|
if ('items' in record) {
|
||||||
|
if (Array.isArray(record.items)) {
|
||||||
|
record.items = (record.items as unknown[]).map(
|
||||||
|
item => normalizeSchemaForOpenAI(item as Record<string, unknown>, strict),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
record.items = normalizeSchemaForOpenAI(record.items as Record<string, unknown>, strict)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recurse into combinators
|
||||||
|
for (const key of ['anyOf', 'oneOf', 'allOf'] as const) {
|
||||||
|
if (key in record && Array.isArray(record[key])) {
|
||||||
|
record[key] = (record[key] as unknown[]).map(
|
||||||
|
item => normalizeSchemaForOpenAI(item as Record<string, unknown>, strict),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return record
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertTools(
|
function convertTools(
|
||||||
@@ -374,15 +414,16 @@ async function* openaiStreamToAnthropic(
|
|||||||
const decoder = new TextDecoder()
|
const decoder = new TextDecoder()
|
||||||
let buffer = ''
|
let buffer = ''
|
||||||
|
|
||||||
while (true) {
|
try {
|
||||||
const { done, value } = await reader.read()
|
while (true) {
|
||||||
if (done) break
|
const { done, value } = await reader.read()
|
||||||
|
if (done) break
|
||||||
|
|
||||||
buffer += decoder.decode(value, { stream: true })
|
buffer += decoder.decode(value, { stream: true })
|
||||||
const lines = buffer.split('\n')
|
const lines = buffer.split('\n')
|
||||||
buffer = lines.pop() ?? ''
|
buffer = lines.pop() ?? ''
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const trimmed = line.trim()
|
const trimmed = line.trim()
|
||||||
if (!trimmed || trimmed === 'data: [DONE]') continue
|
if (!trimmed || trimmed === 'data: [DONE]') continue
|
||||||
if (!trimmed.startsWith('data: ')) continue
|
if (!trimmed.startsWith('data: ')) continue
|
||||||
@@ -528,6 +569,9 @@ async function* openaiStreamToAnthropic(
|
|||||||
hasEmittedFinalUsage = true
|
hasEmittedFinalUsage = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
reader.releaseLock()
|
||||||
}
|
}
|
||||||
|
|
||||||
yield { type: 'message_stop' }
|
yield { type: 'message_stop' }
|
||||||
|
|||||||
Reference in New Issue
Block a user