Skip to content

Commit 7c4d082

Browse files
committed
fix(triage): normalize OpenAI response schemas
1 parent e2be937 commit 7c4d082

7 files changed

Lines changed: 67 additions & 7 deletions

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { z } from 'zod';
3+
import { toOpenAiJsonSchema } from './jsonSchema.js';
4+
5+
function findSchemaKey(value: unknown, key: string): boolean {
6+
if (Array.isArray(value)) {
7+
return value.some((item) => findSchemaKey(item, key));
8+
}
9+
10+
if (value == null || typeof value !== 'object') {
11+
return false;
12+
}
13+
14+
return Object.entries(value).some(
15+
([entryKey, entryValue]) =>
16+
entryKey === key || findSchemaKey(entryValue, key),
17+
);
18+
}
19+
20+
describe('toOpenAiJsonSchema', () => {
21+
it('rewrites Zod discriminated-union oneOf entries to anyOf', () => {
22+
const schema = z.object({
23+
result: z.discriminatedUnion('kind', [
24+
z.object({ kind: z.literal('existing'), id: z.string() }),
25+
z.object({ kind: z.literal('new'), type: z.string() }),
26+
]),
27+
});
28+
29+
const jsonSchema = toOpenAiJsonSchema(schema);
30+
31+
expect(findSchemaKey(jsonSchema, 'oneOf')).toBe(false);
32+
expect(findSchemaKey(jsonSchema, 'anyOf')).toBe(true);
33+
});
34+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { z } from 'zod';
2+
3+
type JsonObject = { [key: string]: unknown };
4+
5+
function replaceOneOfWithAnyOf(value: unknown): unknown {
6+
if (Array.isArray(value)) {
7+
return value.map(replaceOneOfWithAnyOf);
8+
}
9+
10+
if (value == null || typeof value !== 'object') {
11+
return value;
12+
}
13+
14+
const output: JsonObject = {};
15+
for (const [key, child] of Object.entries(value)) {
16+
output[key === 'oneOf' ? 'anyOf' : key] = replaceOneOfWithAnyOf(child);
17+
}
18+
return output;
19+
}
20+
21+
export function toOpenAiJsonSchema(schema: z.ZodType): JsonObject {
22+
return replaceOneOfWithAnyOf(z.toJSONSchema(schema)) as JsonObject;
23+
}

packages/triage/src/llm/functions/extractClaimsFromNewEvidence/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
} from '../../../helpers/estimateOpenAICost.js';
1313
import { assert } from '../../../util/assert.js';
1414
import { getOpenAiClient } from '../../client.js';
15+
import { toOpenAiJsonSchema } from '../../common/jsonSchema.js';
1516
import type { ToolRegistry } from '../../common/tool.js';
1617
import { normalizeClaimsForEvidence } from './normalizeClaimsForEvidence.js';
1718
import { buildSystemPrompt } from './prompt.js';
@@ -90,7 +91,7 @@ Timestamp: ${evidenceTs.toISO({ includeOffset: true })}
9091
type: 'json_schema',
9192
name: 'Response',
9293
strict: true,
93-
schema: z.toJSONSchema(ResponseSchema),
94+
schema: toOpenAiJsonSchema(ResponseSchema),
9495
},
9596
},
9697
tools: Object.values(toolRegistry).map((tool) => {

packages/triage/src/llm/functions/generateIssueTitleAndSlug/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { ResponseInputItem } from 'openai/resources/responses/responses.mjs
22
import { z } from 'zod';
33
import { assert } from '../../../util/assert.js';
44
import { getOpenAiClient } from '../../client.js';
5+
import { toOpenAiJsonSchema } from '../../common/jsonSchema.js';
56
import { buildSystemPrompt } from './prompt.js';
67

78
export const ResponseSchema = z.object({
@@ -46,7 +47,7 @@ Text: ${params.text}
4647
type: 'json_schema',
4748
name: 'Response',
4849
strict: true,
49-
schema: z.toJSONSchema(ResponseSchema),
50+
schema: toOpenAiJsonSchema(ResponseSchema),
5051
},
5152
},
5253
});

packages/triage/src/llm/functions/translate/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { TranslationsSchema } from '@mrtdown/core';
22
import type { ResponseInputItem } from 'openai/resources/responses/responses.js';
3-
import { z } from 'zod';
43
import {
54
estimateOpenAICostFromUsage,
65
normalizeOpenAIResponsesUsage,
76
} from '../../../helpers/estimateOpenAICost.js';
87
import { assert } from '../../../util/assert.js';
98
import { getOpenAiClient } from '../../client.js';
9+
import { toOpenAiJsonSchema } from '../../common/jsonSchema.js';
1010

1111
export async function translate(text: string) {
1212
const model = 'gpt-5-nano';
@@ -31,7 +31,7 @@ Keep the names in English as much as possible, do not provide any translations f
3131
type: 'json_schema',
3232
name: 'Translation',
3333
strict: true,
34-
schema: z.toJSONSchema(TranslationsSchema),
34+
schema: toOpenAiJsonSchema(TranslationsSchema),
3535
},
3636
},
3737
reasoning: {

packages/triage/src/llm/functions/triageNewEvidence/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
import z from 'zod';
99
import { assert } from '../../../util/assert.js';
1010
import { getOpenAiClient } from '../../client.js';
11+
import { toOpenAiJsonSchema } from '../../common/jsonSchema.js';
1112
import type { ToolRegistry } from '../../common/tool.js';
1213
import { buildSystemPrompt } from './prompt.js';
1314
import { FindIssuesByDateRangeTool } from './tools/FindIssuesByDateRangeTool.js';
@@ -82,7 +83,7 @@ Timestamp: ${evidenceTs.toISO({ includeOffset: true })}
8283
type: 'json_schema',
8384
name: 'Response',
8485
strict: true,
85-
schema: z.toJSONSchema(ResponseSchema),
86+
schema: toOpenAiJsonSchema(ResponseSchema),
8687
},
8788
},
8889
tools: Object.values(toolRegistry).map((tool) => {

packages/triage/src/util/ingestContent/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@ function createWriter(dataDir: string): MRTDownWriter {
3636
async function runLlmStep<T>(
3737
label: string,
3838
operation: () => Promise<T>,
39-
): Promise<T | null> {
39+
): Promise<T> {
4040
try {
4141
return await operation();
4242
} catch (error) {
4343
console.error(`[ingestContent] ${label} failed:`, error);
44-
return null;
44+
throw error;
4545
}
4646
}
4747

0 commit comments

Comments
 (0)