diff --git a/.changeset/fix-tuple-schema-coercion-v2.md b/.changeset/fix-tuple-schema-coercion-v2.md new file mode 100644 index 0000000000..accf940bd0 --- /dev/null +++ b/.changeset/fix-tuple-schema-coercion-v2.md @@ -0,0 +1,7 @@ +--- +'@tanstack/ai': patch +'@tanstack/ai-utils': patch +'@tanstack/openai-base': patch +--- + +Preserve positional tuple schemas and their null-widening metadata during strict output conversion. diff --git a/packages/ai-utils/src/transforms.ts b/packages/ai-utils/src/transforms.ts index c083952ad5..5b2e01589e 100644 --- a/packages/ai-utils/src/transforms.ts +++ b/packages/ai-utils/src/transforms.ts @@ -69,6 +69,7 @@ export type NullWideningMap = { widened?: boolean properties?: Record items?: NullWideningMap | Array + prefixItems?: Array } function walk(value: unknown, map: NullWideningMap | undefined): unknown { @@ -81,13 +82,13 @@ function walk(value: unknown, map: NullWideningMap | undefined): unknown { if (typeof value !== 'object' || !map) return value if (Array.isArray(value)) { - const { items } = map - if (!items) return value - // Tuple maps (`items: [a, b, …]`) describe each position separately; - // a single `items` map applies to every element. - return Array.isArray(items) - ? value.map((item, index) => walk(item, items[index])) - : value.map((item) => walk(item, items)) + const { items, prefixItems } = map + if (!items && !prefixItems) return value + return value.map((item, index) => { + const prefixMap = prefixItems?.[index] + const itemMap = prefixMap ?? (Array.isArray(items) ? items[index] : items) + return walk(item, itemMap) + }) } const { properties } = map diff --git a/packages/ai/src/activities/chat/tools/schema-converter.ts b/packages/ai/src/activities/chat/tools/schema-converter.ts index cda434bd1d..3ba7d234d8 100644 --- a/packages/ai/src/activities/chat/tools/schema-converter.ts +++ b/packages/ai/src/activities/chat/tools/schema-converter.ts @@ -145,20 +145,51 @@ function makeStructuredOutputCompatible( : nested.schema widenedHere = wasOptional childMap = nested.nullWidening - } else if (prop.type === 'array' && prop.items) { - const items = Array.isArray(prop.items) ? prop.items[0] : prop.items - const nestedItems = items - ? makeStructuredOutputCompatible(items, items.required || []) - : undefined + } else if (prop.type === 'array') { + const itemSchemas = prop.items + ? Array.isArray(prop.items) + ? prop.items + : [prop.items] + : [] + const nestedItems = itemSchemas.map((item) => + typeof item === 'object' && item !== null + ? makeStructuredOutputCompatible(item, item.required || []) + : undefined, + ) properties[propName] = { ...prop, - items: nestedItems ? nestedItems.schema : prop.items, + ...(prop.items + ? { + items: Array.isArray(prop.items) + ? nestedItems.map( + (nested, index) => nested?.schema ?? itemSchemas[index], + ) + : (nestedItems[0]?.schema ?? prop.items), + } + : {}), ...(wasOptional ? { type: ['array', 'null'] } : {}), } widenedHere = wasOptional - childMap = nestedItems?.nullWidening - ? { items: nestedItems.nullWidening } + const itemMaps = nestedItems.map((nested) => nested?.nullWidening ?? {}) + childMap = itemMaps.some((itemMap) => Object.keys(itemMap).length > 0) + ? { items: Array.isArray(prop.items) ? itemMaps : itemMaps[0] } : undefined + if (Array.isArray(prop.prefixItems)) { + const prefixItems = prop.prefixItems.map((item) => + typeof item === 'object' && item !== null + ? makeStructuredOutputCompatible(item, item.required || []) + : undefined, + ) + properties[propName].prefixItems = prefixItems.map( + (nested, index) => nested?.schema ?? prop.prefixItems[index], + ) + const prefixMaps = prefixItems.map( + (nested) => nested?.nullWidening ?? {}, + ) + if (prefixMaps.some((itemMap) => Object.keys(itemMap).length > 0)) { + childMap = { ...(childMap ?? {}), prefixItems: prefixMaps } + } + } } else if (wasOptional) { // Make optional fields nullable by adding null to the type. Mark // `widenedHere` only where we actually add `null`; a field already @@ -188,16 +219,42 @@ function makeStructuredOutputCompatible( if (Object.keys(propertyMaps).length > 0) map.properties = propertyMaps } - // Handle array types with object items - if (result.type === 'array' && result.items) { - const items = Array.isArray(result.items) ? result.items[0] : result.items - if (items) { - const nestedItems = makeStructuredOutputCompatible( - items, - items.required || [], + // Handle array item and prefix-item schemas recursively. + if (result.type === 'array') { + if (result.items) { + const itemSchemas = Array.isArray(result.items) + ? result.items + : [result.items] + const nestedItems = itemSchemas.map((item) => + typeof item === 'object' && item !== null + ? makeStructuredOutputCompatible(item, item.required || []) + : undefined, + ) + result.items = Array.isArray(result.items) + ? nestedItems.map( + (nested, index) => nested?.schema ?? itemSchemas[index], + ) + : (nestedItems[0]?.schema ?? result.items) + const itemMaps = nestedItems.map((nested) => nested?.nullWidening ?? {}) + if (itemMaps.some((itemMap) => Object.keys(itemMap).length > 0)) { + map.items = Array.isArray(result.items) ? itemMaps : itemMaps[0] + } + } + if (Array.isArray(result.prefixItems)) { + const prefixSchemas = result.prefixItems.map((item) => + typeof item === 'object' && item !== null + ? makeStructuredOutputCompatible(item, item.required || []) + : undefined, + ) + result.prefixItems = prefixSchemas.map( + (nested, index) => nested?.schema ?? result.prefixItems[index], ) - result.items = nestedItems.schema - if (nestedItems.nullWidening) map.items = nestedItems.nullWidening + const prefixMaps = prefixSchemas.map( + (nested) => nested?.nullWidening ?? {}, + ) + if (prefixMaps.some((itemMap) => Object.keys(itemMap).length > 0)) { + map.prefixItems = prefixMaps + } } } diff --git a/packages/ai/tests/chat-structured-output-null-normalization.test.ts b/packages/ai/tests/chat-structured-output-null-normalization.test.ts index 7d5a372bfd..7de6efee57 100644 --- a/packages/ai/tests/chat-structured-output-null-normalization.test.ts +++ b/packages/ai/tests/chat-structured-output-null-normalization.test.ts @@ -293,6 +293,20 @@ describe('convertSchemaForStructuredOutput → undoNullWidening round trip', () expect('meta' in result).toBe(false) }) + it('un-widens every item in a homogeneous array property', () => { + const outputSchema = z.object({ + items: z.array(z.object({ label: z.string().optional() })), + }) + + const { nullWideningMap } = convertSchemaForStructuredOutput(outputSchema) + expect( + undoNullWidening( + { items: [{ label: null }, { label: null }, { label: 'kept' }] }, + nullWideningMap, + ), + ).toEqual({ items: [{}, {}, { label: 'kept' }] }) + }) + it('keeps a genuine `.nullable()` null inside array items', () => { // The widener does NOT touch `note` (it's `.nullable()`, not `.optional()`), // so its null must survive even though it sits inside an array item — the diff --git a/packages/openai-base/src/utils/schema-converter.ts b/packages/openai-base/src/utils/schema-converter.ts index 7732b1ffac..4ed91d0780 100644 --- a/packages/openai-base/src/utils/schema-converter.ts +++ b/packages/openai-base/src/utils/schema-converter.ts @@ -104,6 +104,7 @@ export function makeStructuredOutputCompatibleWithMap( * emit these. * * - `oneOf` / `allOf` / `not` — combinator keywords strict mode rejects + * - `prefixItems` — tuple keyword unsupported by OpenAI strict outputs * - `$ref` / `$defs` / `definitions` — references and definition pools whose * object subschemas escape the `additionalProperties: false` normalization * strict mode requires @@ -115,6 +116,7 @@ const STRICT_UNSUPPORTED_KEYWORDS: ReadonlyArray = [ '$ref', '$defs', 'definitions', + 'prefixItems', ] /** @@ -331,15 +333,10 @@ function coerceStrictSchema( prop = nested.schema childMap = nested.nullWideningMap hasUntrackableAnyOfWidening ||= nested.hasUntrackableAnyOfWidening - } else if (isSchemaObject(prop) && prop.type === 'array' && prop.items) { - const nested = coerceStrictSchema(prop.items, prop.items.required || []) - prop = { - ...prop, - items: nested.schema, - } + } else if (isSchemaObject(prop) && prop.type === 'array') { + const nested = coerceStrictSchema(prop, []) + prop = nested.schema childMap = nested.nullWideningMap - ? { items: nested.nullWideningMap } - : undefined hasUntrackableAnyOfWidening ||= nested.hasUntrackableAnyOfWidening } else if (isSchemaObject(prop) && prop.anyOf) { const nested = coerceStrictSchema(prop, prop.required || []) @@ -411,12 +408,49 @@ function coerceStrictSchema( } if (result.type === 'array' && result.items) { - const nested = coerceStrictSchema(result.items, result.items.required || []) - result.items = nested.schema - if (nested.nullWideningMap) { - nullWideningMap.items = nested.nullWideningMap + if (Array.isArray(result.items)) { + const itemMaps: Array = [] + result.items = result.items.map((item) => { + if (!isSchemaObject(item)) { + itemMaps.push({}) + return item + } + const nested = coerceStrictSchema(item, item.required || []) + itemMaps.push(nested.nullWideningMap ?? {}) + hasUntrackableAnyOfWidening ||= nested.hasUntrackableAnyOfWidening + return nested.schema + }) + if (itemMaps.some((map) => Object.keys(map).length > 0)) { + nullWideningMap.items = itemMaps + } + } else { + const nested = coerceStrictSchema( + result.items, + result.items.required || [], + ) + result.items = nested.schema + if (nested.nullWideningMap) { + nullWideningMap.items = nested.nullWideningMap + } + hasUntrackableAnyOfWidening ||= nested.hasUntrackableAnyOfWidening + } + } + + if (Array.isArray(result.prefixItems)) { + const itemMaps: Array = [] + result.prefixItems = result.prefixItems.map((item) => { + if (!isSchemaObject(item)) { + itemMaps.push({}) + return item + } + const nested = coerceStrictSchema(item, item.required || []) + itemMaps.push(nested.nullWideningMap ?? {}) + hasUntrackableAnyOfWidening ||= nested.hasUntrackableAnyOfWidening + return nested.schema + }) + if (itemMaps.some((map) => Object.keys(map).length > 0)) { + nullWideningMap.prefixItems = itemMaps } - hasUntrackableAnyOfWidening ||= nested.hasUntrackableAnyOfWidening } if (result.anyOf && Array.isArray(result.anyOf)) { diff --git a/packages/openai-base/tests/schema-converter.test.ts b/packages/openai-base/tests/schema-converter.test.ts index 478bcfc4c0..c89fb96f18 100644 --- a/packages/openai-base/tests/schema-converter.test.ts +++ b/packages/openai-base/tests/schema-converter.test.ts @@ -433,7 +433,113 @@ describe('makeStructuredOutputCompatible', () => { }) }) +it('preserves positional tuple item schemas', () => { + const items = [ + { type: 'number', minimum: -180 }, + { type: 'number', minimum: -90 }, + ] + const schema = { type: 'array', items } + + const result: any = makeStructuredOutputCompatible(schema) + + expect(result.items).toEqual(items) +}) + +it('preserves tuple items nested in object properties', () => { + const schema = { + type: 'object', + properties: { + bbox: { type: 'array', items: [{ type: 'number' }, { type: 'number' }] }, + }, + required: ['bbox'], + } + + const result: any = makeStructuredOutputCompatible(schema) + + expect(result.properties.bbox.items).toEqual(schema.properties.bbox.items) +}) + +it('preserves boolean tuple schemas and positional metadata', () => { + const schema = { + type: 'array', + items: [ + false, + { + type: 'object', + properties: { item: { type: 'string' } }, + required: [], + }, + ], + prefixItems: [ + false, + { + type: 'object', + properties: { prefix: { type: 'string' } }, + required: [], + }, + ], + additionalItems: false, + } + + const { nullWideningMap } = makeStructuredOutputCompatibleWithMap(schema) + const result: any = makeStructuredOutputCompatible(schema) + + expect(result.items[0]).toBe(false) + expect(result.items[1].additionalProperties).toBe(false) + expect(result.prefixItems[0]).toBe(false) + expect(result.prefixItems[1].additionalProperties).toBe(false) + expect( + Array.isArray(nullWideningMap?.items) && nullWideningMap.items[1], + ).toEqual({ + properties: { item: { widened: true } }, + }) + expect(nullWideningMap?.prefixItems?.[1]).toEqual({ + properties: { prefix: { widened: true } }, + }) + expect(result.additionalItems).toBe(false) +}) + +it('preserves separate metadata for items and prefixItems', () => { + const schema = { + type: 'array', + items: [ + { + type: 'object', + properties: { item: { type: 'string' } }, + required: [], + }, + ], + prefixItems: [ + { + type: 'object', + properties: { prefix: { type: 'string' } }, + required: [], + }, + ], + } + + const { nullWideningMap } = makeStructuredOutputCompatibleWithMap(schema) + + expect(nullWideningMap?.items).toEqual([ + { properties: { item: { widened: true } } }, + ]) + expect(nullWideningMap?.prefixItems).toEqual([ + { properties: { prefix: { widened: true } } }, + ]) + const result: any = makeStructuredOutputCompatible(schema) + expect(result.prefixItems[0].additionalProperties).toBe(false) +}) + describe('isStrictModeCompatible', () => { + it('rejects prefixItems for OpenAI strict outputs', () => { + expect( + isStrictModeCompatible({ + type: 'array', + prefixItems: [{ type: 'string' }], + }), + ).toBe(false) + }) + it('returns true for a plain object schema in the strict subset', () => { expect( isStrictModeCompatible({ diff --git a/scripts/lovable-gateway.models.json b/scripts/lovable-gateway.models.json index 8a193376e4..79c6babbc9 100644 --- a/scripts/lovable-gateway.models.json +++ b/scripts/lovable-gateway.models.json @@ -12,15 +12,8 @@ "max_tokens": 65536, "knowledge": "2025-01", "modalities": { - "input": [ - "text", - "image", - "audio", - "video" - ], - "output": [ - "text" - ] + "input": ["text", "image", "audio", "video"], + "output": ["text"] }, "pricing": { "input": { @@ -85,15 +78,8 @@ "max_tokens": 65536, "knowledge": "2025-01", "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text", - "image" - ] + "input": ["text", "image", "video"], + "output": ["text", "image"] }, "pricing": { "input": { @@ -150,15 +136,8 @@ "max_tokens": 65536, "knowledge": "2025-01", "modalities": { - "input": [ - "text", - "image", - "audio", - "video" - ], - "output": [ - "text" - ] + "input": ["text", "image", "audio", "video"], + "output": ["text"] }, "pricing": { "input": { @@ -222,12 +201,8 @@ "context_window": 8192, "max_tokens": 16384, "modalities": { - "input": [ - "text" - ], - "output": [ - "audio" - ] + "input": ["text"], + "output": ["audio"] }, "pricing": { "input": { @@ -265,12 +240,8 @@ "max_tokens": 16384, "knowledge": "2025-01", "modalities": { - "input": [ - "text" - ], - "output": [ - "audio" - ] + "input": ["text"], + "output": ["audio"] }, "pricing": { "input": { @@ -308,15 +279,8 @@ "max_tokens": 65536, "knowledge": "2025-01", "modalities": { - "input": [ - "text", - "image", - "audio", - "video" - ], - "output": [ - "text" - ] + "input": ["text", "image", "audio", "video"], + "output": ["text"] }, "pricing": { "input": { @@ -406,12 +370,8 @@ "max_tokens": 16384, "knowledge": "2025-01", "modalities": { - "input": [ - "text" - ], - "output": [ - "audio" - ] + "input": ["text"], + "output": ["audio"] }, "pricing": { "input": { @@ -449,15 +409,8 @@ "max_tokens": 65000, "knowledge": "2025-01", "modalities": { - "input": [ - "text", - "image", - "audio", - "video" - ], - "output": [ - "text" - ] + "input": ["text", "image", "audio", "video"], + "output": ["text"] }, "pricing": { "input": { @@ -522,15 +475,8 @@ "max_tokens": 32768, "knowledge": "2025-01", "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text", - "image" - ] + "input": ["text", "image", "video"], + "output": ["text", "image"] }, "pricing": { "input": { @@ -595,15 +541,8 @@ "max_tokens": 32768, "knowledge": "2025-01", "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text", - "image" - ] + "input": ["text", "image", "video"], + "output": ["text", "image"] }, "pricing": { "input": { @@ -668,15 +607,8 @@ "max_tokens": 65000, "knowledge": "2025-01", "modalities": { - "input": [ - "text", - "image", - "audio", - "video" - ], - "output": [ - "text" - ] + "input": ["text", "image", "audio", "video"], + "output": ["text"] }, "pricing": { "input": { @@ -740,15 +672,8 @@ "context_window": 65536, "max_tokens": 4096, "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text", - "image" - ] + "input": ["text", "image", "video"], + "output": ["text", "image"] }, "pricing": { "input": { @@ -813,12 +738,8 @@ "max_tokens": 16384, "knowledge": "2025-01", "modalities": { - "input": [ - "text" - ], - "output": [ - "audio" - ] + "input": ["text"], + "output": ["audio"] }, "pricing": { "input": { @@ -856,15 +777,8 @@ "max_tokens": 64000, "knowledge": "2025-01", "modalities": { - "input": [ - "text", - "image", - "audio", - "video" - ], - "output": [ - "text" - ] + "input": ["text", "image", "audio", "video"], + "output": ["text"] }, "pricing": { "input": { @@ -954,15 +868,8 @@ "max_tokens": 64000, "knowledge": "2025-01", "modalities": { - "input": [ - "text", - "image", - "audio", - "video" - ], - "output": [ - "text" - ] + "input": ["text", "image", "audio", "video"], + "output": ["text"] }, "pricing": { "input": { @@ -1027,15 +934,8 @@ "max_tokens": 64000, "knowledge": "2026-03", "modalities": { - "input": [ - "text", - "image", - "audio", - "video" - ], - "output": [ - "text" - ] + "input": ["text", "image", "audio", "video"], + "output": ["text"] }, "pricing": { "input": { @@ -1100,12 +1000,8 @@ "max_tokens": 0, "knowledge": "2025-05", "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] + "input": ["text"], + "output": ["text"] }, "pricing": { "input": { @@ -1133,15 +1029,8 @@ "max_tokens": 0, "knowledge": "2025-11", "modalities": { - "input": [ - "text", - "image", - "audio", - "video" - ], - "output": [ - "text" - ] + "input": ["text", "image", "audio", "video"], + "output": ["text"] }, "pricing": { "input": { @@ -1192,13 +1081,8 @@ "context_window": 0, "max_tokens": 0, "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "video" - ] + "input": ["text", "image"], + "output": ["video"] }, "pricing": { "video_duration": { @@ -1226,13 +1110,8 @@ "context_window": 0, "max_tokens": 0, "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "video" - ] + "input": ["text", "image"], + "output": ["video"] }, "pricing": { "video_duration": { @@ -1260,13 +1139,8 @@ "context_window": 0, "max_tokens": 0, "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "video" - ] + "input": ["text", "image"], + "output": ["video"] }, "pricing": { "video_duration": { @@ -1291,13 +1165,8 @@ "context_window": 16000, "max_tokens": 2000, "modalities": { - "input": [ - "text", - "audio" - ], - "output": [ - "text" - ] + "input": ["text", "audio"], + "output": ["text"] }, "pricing": { "input": { @@ -1342,12 +1211,8 @@ "context_window": 2000, "max_tokens": 0, "modalities": { - "input": [ - "text" - ], - "output": [ - "audio" - ] + "input": ["text"], + "output": ["audio"] }, "pricing": { "input": { @@ -1384,13 +1249,8 @@ "context_window": 16000, "max_tokens": 2000, "modalities": { - "input": [ - "text", - "audio" - ], - "output": [ - "text" - ] + "input": ["text", "audio"], + "output": ["text"] }, "pricing": { "input": { @@ -1436,13 +1296,8 @@ "max_tokens": 128000, "knowledge": "2024-09-30", "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] + "input": ["text", "image"], + "output": ["text"] }, "pricing": { "input": { @@ -1526,13 +1381,8 @@ "max_tokens": 128000, "knowledge": "2024-05-30", "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] + "input": ["text", "image"], + "output": ["text"] }, "pricing": { "input": { @@ -1616,13 +1466,8 @@ "max_tokens": 128000, "knowledge": "2024-05-30", "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] + "input": ["text", "image"], + "output": ["text"] }, "pricing": { "input": { @@ -1671,13 +1516,8 @@ "max_tokens": 128000, "knowledge": "2024-10", "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] + "input": ["text", "image"], + "output": ["text"] }, "pricing": { "input": { @@ -1761,13 +1601,8 @@ "max_tokens": 128000, "knowledge": "2025-08-31", "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] + "input": ["text", "image"], + "output": ["text"] }, "pricing": { "input": { @@ -1866,13 +1701,8 @@ "max_tokens": 128000, "knowledge": "2025-08-31", "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] + "input": ["text", "image"], + "output": ["text"] }, "pricing": { "input": { @@ -1956,13 +1786,8 @@ "max_tokens": 128000, "knowledge": "2025-08-31", "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] + "input": ["text", "image"], + "output": ["text"] }, "pricing": { "input": { @@ -2011,13 +1836,8 @@ "max_tokens": 128000, "knowledge": "2025-08-31", "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] + "input": ["text", "image"], + "output": ["text"] }, "pricing": { "input": { @@ -2081,13 +1901,8 @@ "max_tokens": 128000, "knowledge": "2025-12-01", "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] + "input": ["text", "image"], + "output": ["text"] }, "pricing": { "input": { @@ -2186,13 +2001,8 @@ "max_tokens": 128000, "knowledge": "2025-12-01", "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] + "input": ["text", "image"], + "output": ["text"] }, "pricing": { "input": { @@ -2256,13 +2066,8 @@ "max_tokens": 128000, "knowledge": "2026-02-16", "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] + "input": ["text", "image"], + "output": ["text"] }, "pricing": { "input": { @@ -2361,13 +2166,8 @@ "max_tokens": 128000, "knowledge": "2026-02-16", "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] + "input": ["text", "image"], + "output": ["text"] }, "pricing": { "input": { @@ -2466,13 +2266,8 @@ "max_tokens": 128000, "knowledge": "2026-02-16", "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] + "input": ["text", "image"], + "output": ["text"] }, "pricing": { "input": { @@ -2570,13 +2365,8 @@ "context_window": 0, "max_tokens": 0, "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "image" - ] + "input": ["text", "image"], + "output": ["image"] }, "pricing": { "input": { @@ -2621,13 +2411,8 @@ "context_window": 0, "max_tokens": 0, "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "image" - ] + "input": ["text", "image"], + "output": ["image"] }, "pricing": { "input": { @@ -2672,12 +2457,8 @@ "context_window": 8191, "max_tokens": 0, "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] + "input": ["text"], + "output": ["text"] }, "pricing": { "input": { @@ -2704,12 +2485,8 @@ "context_window": 8191, "max_tokens": 0, "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] + "input": ["text"], + "output": ["text"] }, "pricing": { "input": {