mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
feat(kosong): support structured response formats (#1397)
This commit is contained in:
parent
150206a6f7
commit
6c9abe8cf7
12 changed files with 381 additions and 5 deletions
5
.changeset/structured-output-provider-format.md
Normal file
5
.changeset/structured-output-provider-format.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@moonshot-ai/kosong": patch
|
||||
---
|
||||
|
||||
Add provider-level structured response format support.
|
||||
|
|
@ -2,6 +2,24 @@ import type { Message, StreamedMessagePart, VideoURLPart } from './message';
|
|||
import type { Tool } from './tool';
|
||||
import type { TokenUsage } from './usage';
|
||||
|
||||
export type JsonSchemaObject = Record<string, unknown>;
|
||||
|
||||
export interface JsonObjectResponseFormat {
|
||||
readonly type: 'json_object';
|
||||
}
|
||||
|
||||
export interface JsonSchemaResponseFormat {
|
||||
readonly type: 'json_schema';
|
||||
readonly jsonSchema: {
|
||||
readonly name: string;
|
||||
readonly schema: JsonSchemaObject;
|
||||
readonly strict?: boolean | undefined;
|
||||
readonly description?: string | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
export type ResponseFormat = JsonObjectResponseFormat | JsonSchemaResponseFormat;
|
||||
|
||||
/**
|
||||
* Thinking effort passed to {@link ChatProvider.withThinking}.
|
||||
*
|
||||
|
|
@ -118,6 +136,11 @@ export interface GenerateOptions {
|
|||
* each request/retry so providers never retain mutable credential state.
|
||||
*/
|
||||
auth?: ProviderRequestAuth;
|
||||
/**
|
||||
* Optional model-output format constraint. Providers map this to their native
|
||||
* structured-output field when supported.
|
||||
*/
|
||||
responseFormat?: ResponseFormat;
|
||||
/**
|
||||
* Host-side instrumentation hook fired immediately before invoking the
|
||||
* provider adapter's generate call.
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import type {
|
|||
FinishReason,
|
||||
GenerateOptions,
|
||||
ProviderRequestAuth,
|
||||
ResponseFormat,
|
||||
StreamedMessage,
|
||||
ThinkingEffort,
|
||||
} from '#/provider';
|
||||
|
|
@ -142,6 +143,27 @@ const ANTHROPIC_TOOL_CALL_ID_POLICY: ToolCallIdPolicy = {
|
|||
maxLength: 64,
|
||||
};
|
||||
|
||||
function applyResponseFormat(
|
||||
kwargs: Record<string, unknown>,
|
||||
format: ResponseFormat | undefined,
|
||||
): void {
|
||||
if (format === undefined) return;
|
||||
if (format.type === 'json_object') {
|
||||
throw new ChatProviderError(
|
||||
'Anthropic provider requires a JSON schema for structured response output.',
|
||||
);
|
||||
}
|
||||
const outputConfig =
|
||||
kwargs['output_config'] !== undefined && kwargs['output_config'] !== null
|
||||
? { ...(kwargs['output_config'] as Record<string, unknown>) }
|
||||
: {};
|
||||
outputConfig['format'] = {
|
||||
type: 'json_schema',
|
||||
schema: format.jsonSchema.schema,
|
||||
};
|
||||
kwargs['output_config'] = outputConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-version default output ceilings sourced from Anthropic's Messages
|
||||
* API model cards (platform.claude.com/docs/en/about-claude/models/overview).
|
||||
|
|
@ -1114,6 +1136,7 @@ export class AnthropicChatProvider implements ChatProvider {
|
|||
if (this._generationKwargs.output_config !== undefined) {
|
||||
kwargs['output_config'] = this._generationKwargs.output_config;
|
||||
}
|
||||
applyResponseFormat(kwargs, options?.responseFormat);
|
||||
if (this._generationKwargs.contextManagement !== undefined) {
|
||||
kwargs['context_management'] = this._generationKwargs.contextManagement;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import type {
|
|||
FinishReason,
|
||||
GenerateOptions,
|
||||
ProviderRequestAuth,
|
||||
ResponseFormat,
|
||||
StreamedMessage,
|
||||
ThinkingEffort,
|
||||
} from '#/provider';
|
||||
|
|
@ -127,6 +128,19 @@ function toolToGoogleGenAI(tool: Tool): GoogleTool {
|
|||
],
|
||||
};
|
||||
}
|
||||
|
||||
function applyResponseFormat(
|
||||
config: Record<string, unknown>,
|
||||
format: ResponseFormat | undefined,
|
||||
): void {
|
||||
if (format === undefined) return;
|
||||
config['responseMimeType'] = 'application/json';
|
||||
delete config['responseSchema'];
|
||||
delete config['responseJsonSchema'];
|
||||
if (format.type === 'json_schema') {
|
||||
config['responseJsonSchema'] = format.jsonSchema.schema;
|
||||
}
|
||||
}
|
||||
interface GoogleContent {
|
||||
role: string;
|
||||
parts: GooglePart[];
|
||||
|
|
@ -819,6 +833,7 @@ export class GoogleGenAIChatProvider implements ChatProvider {
|
|||
systemInstruction: systemPrompt,
|
||||
...(tools.length > 0 ? { tools: tools.map((t) => toolToGoogleGenAI(t)) } : {}),
|
||||
};
|
||||
applyResponseFormat(config, options?.responseFormat);
|
||||
|
||||
try {
|
||||
const client = this._createClient(options?.auth);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import type {
|
|||
GenerateOptions,
|
||||
MaxCompletionTokensOptions,
|
||||
ProviderRequestAuth,
|
||||
ResponseFormat,
|
||||
StreamedMessage,
|
||||
ThinkingEffort,
|
||||
VideoUploadInput,
|
||||
|
|
@ -200,6 +201,21 @@ function convertTool(tool: Tool): OpenAIToolParam {
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
function responseFormatToOpenAI(format: ResponseFormat): Record<string, unknown> {
|
||||
if (format.type === 'json_object') {
|
||||
return { type: 'json_object' };
|
||||
}
|
||||
return {
|
||||
type: 'json_schema',
|
||||
json_schema: {
|
||||
name: format.jsonSchema.name,
|
||||
schema: format.jsonSchema.schema,
|
||||
strict: format.jsonSchema.strict,
|
||||
description: format.jsonSchema.description,
|
||||
},
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Extract usage from a streaming chunk. Moonshot may place usage in
|
||||
* `choices[0].usage` in addition to the top-level `usage` field.
|
||||
|
|
@ -505,6 +521,9 @@ export class KimiChatProvider implements ChatProvider {
|
|||
...requestKwargs,
|
||||
...(extraBody as Record<string, unknown> | undefined),
|
||||
};
|
||||
if (options?.responseFormat !== undefined) {
|
||||
createParams['response_format'] = responseFormatToOpenAI(options.responseFormat);
|
||||
}
|
||||
|
||||
if (tools.length > 0) {
|
||||
createParams['tools'] = tools.map((t) => convertTool(t));
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import type {
|
|||
GenerateOptions,
|
||||
MaxCompletionTokensOptions,
|
||||
ProviderRequestAuth,
|
||||
ResponseFormat,
|
||||
StreamedMessage,
|
||||
ThinkingEffort,
|
||||
} from '#/provider';
|
||||
|
|
@ -60,6 +61,21 @@ const OPENAI_CHAT_TOOL_CALL_ID_POLICY: ToolCallIdPolicy = {
|
|||
maxLength: 64,
|
||||
};
|
||||
|
||||
function responseFormatToOpenAI(format: ResponseFormat): Record<string, unknown> {
|
||||
if (format.type === 'json_object') {
|
||||
return { type: 'json_object' };
|
||||
}
|
||||
return {
|
||||
type: 'json_schema',
|
||||
json_schema: {
|
||||
name: format.jsonSchema.name,
|
||||
schema: format.jsonSchema.schema,
|
||||
strict: format.jsonSchema.strict,
|
||||
description: format.jsonSchema.description,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function extractReasoningContent(
|
||||
source: unknown,
|
||||
explicitKey: string | undefined,
|
||||
|
|
@ -571,6 +587,9 @@ export class OpenAILegacyChatProvider implements ChatProvider {
|
|||
stream: this._stream,
|
||||
...kwargs,
|
||||
};
|
||||
if (options?.responseFormat !== undefined) {
|
||||
createParams['response_format'] = responseFormatToOpenAI(options.responseFormat);
|
||||
}
|
||||
|
||||
if (tools.length > 0) {
|
||||
createParams['tools'] = tools.map((t) => toolToOpenAI(t));
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import type {
|
|||
FinishReason,
|
||||
GenerateOptions,
|
||||
ProviderRequestAuth,
|
||||
ResponseFormat,
|
||||
StreamedMessage,
|
||||
ThinkingEffort,
|
||||
} from '#/provider';
|
||||
|
|
@ -362,6 +363,22 @@ interface ResponseToolParam {
|
|||
parameters: Record<string, unknown>;
|
||||
strict: boolean;
|
||||
}
|
||||
|
||||
function responseFormatToResponsesText(format: ResponseFormat): Record<string, unknown> {
|
||||
if (format.type === 'json_object') {
|
||||
return { format: { type: 'json_object' } };
|
||||
}
|
||||
return {
|
||||
format: {
|
||||
type: 'json_schema',
|
||||
name: format.jsonSchema.name,
|
||||
schema: format.jsonSchema.schema,
|
||||
strict: format.jsonSchema.strict,
|
||||
description: format.jsonSchema.description,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// The Responses API has no input type for video, and only mp3/wav audio can
|
||||
// be inlined as input_file data. Degrade such parts to placeholder text so
|
||||
// the model still learns an attachment existed instead of silently losing it.
|
||||
|
|
@ -1083,6 +1100,12 @@ export class OpenAIResponsesChatProvider implements ChatProvider {
|
|||
if (systemPrompt) {
|
||||
createParams['instructions'] = systemPrompt;
|
||||
}
|
||||
if (options?.responseFormat !== undefined) {
|
||||
createParams['text'] = {
|
||||
...asRawObject(createParams['text']),
|
||||
...responseFormatToResponsesText(options.responseFormat),
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
!('responses' in client) ||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { ChatProviderError } from '#/errors';
|
||||
import type { ContentPart, Message, StreamedMessagePart, ToolCall } from '#/message';
|
||||
import { AnthropicChatProvider, resolveDefaultMaxTokens } from '#/providers/anthropic';
|
||||
import type { GenerateOptions } from '#/provider';
|
||||
import type { Tool } from '#/tool';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
|
|
@ -63,6 +64,7 @@ async function captureRequestBody(
|
|||
systemPrompt: string,
|
||||
tools: Tool[],
|
||||
history: Message[],
|
||||
options?: GenerateOptions,
|
||||
): Promise<Record<string, unknown>> {
|
||||
let capturedParams: Record<string, unknown> | undefined;
|
||||
let capturedOptions: Record<string, unknown> | undefined;
|
||||
|
|
@ -75,7 +77,7 @@ async function captureRequestBody(
|
|||
return Promise.resolve(makeAnthropicResponse());
|
||||
});
|
||||
|
||||
const stream = await provider.generate(systemPrompt, tools, history);
|
||||
const stream = await provider.generate(systemPrompt, tools, history, options);
|
||||
for await (const part of stream) {
|
||||
void part;
|
||||
}
|
||||
|
|
@ -405,6 +407,50 @@ describe('AnthropicChatProvider', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('maps json_schema response format to output_config.format', async () => {
|
||||
const provider = createProvider();
|
||||
const history: Message[] = [
|
||||
{ role: 'user', content: [{ type: 'text', text: 'Extract contact' }], toolCalls: [] },
|
||||
];
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: { name: { type: 'string' } },
|
||||
required: ['name'],
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
const body = await captureRequestBody(provider, '', [], history, {
|
||||
responseFormat: {
|
||||
type: 'json_schema',
|
||||
jsonSchema: {
|
||||
name: 'contact',
|
||||
schema,
|
||||
strict: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(body['output_config']).toEqual({
|
||||
format: {
|
||||
type: 'json_schema',
|
||||
schema,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects json_object response format because Anthropic requires a schema', async () => {
|
||||
const provider = createProvider();
|
||||
const history: Message[] = [
|
||||
{ role: 'user', content: [{ type: 'text', text: 'Extract contact' }], toolCalls: [] },
|
||||
];
|
||||
|
||||
await expect(
|
||||
provider.generate('', [], history, {
|
||||
responseFormat: { type: 'json_object' },
|
||||
}),
|
||||
).rejects.toThrow('Anthropic provider requires a JSON schema for structured response output.');
|
||||
});
|
||||
|
||||
it('multi-turn conversation', async () => {
|
||||
const provider = createProvider();
|
||||
const history: Message[] = [
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
GoogleGenAIStreamedMessage,
|
||||
messagesToGoogleGenAIContents,
|
||||
} from '#/providers/google-genai';
|
||||
import type { GenerateOptions } from '#/provider';
|
||||
import type { Tool } from '#/tool';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
|
|
@ -50,6 +51,7 @@ async function captureRequestBody(
|
|||
systemPrompt: string,
|
||||
tools: Tool[],
|
||||
history: Message[],
|
||||
options?: GenerateOptions,
|
||||
): Promise<Record<string, unknown>> {
|
||||
let capturedBody: Record<string, unknown> | undefined;
|
||||
|
||||
|
|
@ -69,7 +71,7 @@ async function captureRequestBody(
|
|||
return Promise.resolve(makeGenerateContentResponse());
|
||||
});
|
||||
|
||||
const stream = await provider.generate(systemPrompt, tools, history);
|
||||
const stream = await provider.generate(systemPrompt, tools, history, options);
|
||||
for await (const part of stream) {
|
||||
void part;
|
||||
}
|
||||
|
|
@ -131,6 +133,66 @@ describe('GoogleGenAIChatProvider', () => {
|
|||
expect(config['systemInstruction']).toBe('You are helpful.');
|
||||
});
|
||||
|
||||
it('maps json_schema response format to response config', async () => {
|
||||
const provider = createProvider();
|
||||
const history: Message[] = [
|
||||
{ role: 'user', content: [{ type: 'text', text: 'Extract contact' }], toolCalls: [] },
|
||||
];
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: { name: { type: 'string' } },
|
||||
required: ['name'],
|
||||
additionalProperties: false,
|
||||
};
|
||||
const body = await captureRequestBody(provider, '', [], history, {
|
||||
responseFormat: {
|
||||
type: 'json_schema',
|
||||
jsonSchema: {
|
||||
name: 'contact',
|
||||
schema,
|
||||
strict: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const config = body['config'] as Record<string, unknown>;
|
||||
expect(config['responseMimeType']).toBe('application/json');
|
||||
expect(config['responseJsonSchema']).toEqual(schema);
|
||||
});
|
||||
|
||||
it('replaces native responseSchema when applying json_schema response format', async () => {
|
||||
const provider = createProvider().withGenerationKwargs({
|
||||
responseSchema: {
|
||||
type: 'object',
|
||||
properties: { old: { type: 'string' } },
|
||||
},
|
||||
});
|
||||
const history: Message[] = [
|
||||
{ role: 'user', content: [{ type: 'text', text: 'Extract contact' }], toolCalls: [] },
|
||||
];
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: { name: { type: 'string' } },
|
||||
required: ['name'],
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
const body = await captureRequestBody(provider, '', [], history, {
|
||||
responseFormat: {
|
||||
type: 'json_schema',
|
||||
jsonSchema: {
|
||||
name: 'contact',
|
||||
schema,
|
||||
strict: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const config = body['config'] as Record<string, unknown>;
|
||||
expect(config['responseSchema']).toBeUndefined();
|
||||
expect(config['responseJsonSchema']).toEqual(schema);
|
||||
});
|
||||
|
||||
it('system messages in history are wrapped and emitted as user content', () => {
|
||||
// Regression: Google GenAI's Content.role only accepts "user" or
|
||||
// "model", so a `system` message sitting in the replay history (from
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { generate } from '#/generate';
|
|||
import type { ContentPart, Message, ToolCall } from '#/message';
|
||||
import { extractUsageFromChunk, KimiChatProvider } from '#/providers/kimi';
|
||||
import { extractUsage } from '#/providers/openai-common';
|
||||
import type { GenerateOptions } from '#/provider';
|
||||
import type { Tool } from '#/tool';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
|
|
@ -51,6 +52,7 @@ async function captureRequestBody(
|
|||
systemPrompt: string,
|
||||
tools: Tool[],
|
||||
history: Message[],
|
||||
options?: GenerateOptions,
|
||||
): Promise<Record<string, unknown>> {
|
||||
let capturedBody: Record<string, unknown> | undefined;
|
||||
|
||||
|
|
@ -61,7 +63,7 @@ async function captureRequestBody(
|
|||
return Promise.resolve(makeChatCompletionResponse('kimi-k2'));
|
||||
});
|
||||
|
||||
const stream = await provider.generate(systemPrompt, tools, history);
|
||||
const stream = await provider.generate(systemPrompt, tools, history, options);
|
||||
for await (const part of stream) {
|
||||
void part;
|
||||
}
|
||||
|
|
@ -619,6 +621,39 @@ describe('KimiChatProvider', () => {
|
|||
expect(body['max_tokens']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('maps json_schema response format to response_format', async () => {
|
||||
const provider = createProvider();
|
||||
const history: Message[] = [
|
||||
{ role: 'user', content: [{ type: 'text', text: 'Extract contact' }], toolCalls: [] },
|
||||
];
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: { name: { type: 'string' } },
|
||||
required: ['name'],
|
||||
additionalProperties: false,
|
||||
};
|
||||
const body = await captureRequestBody(provider, '', [], history, {
|
||||
responseFormat: {
|
||||
type: 'json_schema',
|
||||
jsonSchema: {
|
||||
name: 'contact',
|
||||
schema,
|
||||
strict: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(body['response_format']).toEqual({
|
||||
type: 'json_schema',
|
||||
json_schema: {
|
||||
name: 'contact',
|
||||
schema,
|
||||
strict: true,
|
||||
description: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('prefers max_completion_tokens when both fields are set', async () => {
|
||||
const provider = createProvider().withGenerationKwargs({
|
||||
max_completion_tokens: 2048,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { generate } from '#/generate';
|
||||
import type { ContentPart, Message, StreamedMessagePart, ToolCall } from '#/message';
|
||||
import { OpenAILegacyChatProvider } from '#/providers/openai-legacy';
|
||||
import type { GenerateOptions } from '#/provider';
|
||||
import type { Tool } from '#/tool';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
|
|
@ -42,6 +43,7 @@ async function captureRequestBody(
|
|||
systemPrompt: string,
|
||||
tools: Tool[],
|
||||
history: Message[],
|
||||
options?: GenerateOptions,
|
||||
): Promise<Record<string, unknown>> {
|
||||
let capturedBody: Record<string, unknown> | undefined;
|
||||
|
||||
|
|
@ -52,7 +54,7 @@ async function captureRequestBody(
|
|||
return Promise.resolve(makeChatCompletionResponse());
|
||||
});
|
||||
|
||||
const stream = await provider.generate(systemPrompt, tools, history);
|
||||
const stream = await provider.generate(systemPrompt, tools, history, options);
|
||||
for await (const part of stream) {
|
||||
void part;
|
||||
}
|
||||
|
|
@ -603,6 +605,39 @@ describe('OpenAILegacyChatProvider', () => {
|
|||
expect(body['max_tokens']).toBe(2048);
|
||||
});
|
||||
|
||||
it('maps json_schema response format to response_format', async () => {
|
||||
const provider = createProvider();
|
||||
const history: Message[] = [
|
||||
{ role: 'user', content: [{ type: 'text', text: 'Extract contact' }], toolCalls: [] },
|
||||
];
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: { name: { type: 'string' } },
|
||||
required: ['name'],
|
||||
additionalProperties: false,
|
||||
};
|
||||
const body = await captureRequestBody(provider, '', [], history, {
|
||||
responseFormat: {
|
||||
type: 'json_schema',
|
||||
jsonSchema: {
|
||||
name: 'contact',
|
||||
schema,
|
||||
strict: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(body['response_format']).toEqual({
|
||||
type: 'json_schema',
|
||||
json_schema: {
|
||||
name: 'contact',
|
||||
schema,
|
||||
strict: true,
|
||||
description: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('withMaxCompletionTokens sets max_tokens on the cloned provider', async () => {
|
||||
const original = createProvider();
|
||||
const provider = original.withMaxCompletionTokens(1024);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
OpenAIResponsesChatProvider,
|
||||
OpenAIResponsesStreamedMessage,
|
||||
} from '#/providers/openai-responses';
|
||||
import type { GenerateOptions } from '#/provider';
|
||||
import type { Tool } from '#/tool';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
|
|
@ -45,6 +46,7 @@ async function captureRequestBody(
|
|||
systemPrompt: string,
|
||||
tools: Tool[],
|
||||
history: Message[],
|
||||
options?: GenerateOptions,
|
||||
): Promise<Record<string, unknown>> {
|
||||
let capturedBody: Record<string, unknown> | undefined;
|
||||
|
||||
|
|
@ -57,7 +59,7 @@ async function captureRequestBody(
|
|||
return Promise.resolve(makeResponsesAPIResponse());
|
||||
});
|
||||
|
||||
const stream = await provider.generate(systemPrompt, tools, history);
|
||||
const stream = await provider.generate(systemPrompt, tools, history, options);
|
||||
for await (const part of stream) {
|
||||
void part;
|
||||
}
|
||||
|
|
@ -910,6 +912,75 @@ describe('OpenAIResponsesChatProvider', () => {
|
|||
expect(body['max_output_tokens']).toBe(1024);
|
||||
expect(provider.maxCompletionTokens).toBe(1024);
|
||||
});
|
||||
|
||||
it('maps json_schema response format to text.format', async () => {
|
||||
const provider = createProvider();
|
||||
const history: Message[] = [
|
||||
{ role: 'user', content: [{ type: 'text', text: 'Extract contact' }], toolCalls: [] },
|
||||
];
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: { name: { type: 'string' } },
|
||||
required: ['name'],
|
||||
additionalProperties: false,
|
||||
};
|
||||
const body = await captureRequestBody(provider, '', [], history, {
|
||||
responseFormat: {
|
||||
type: 'json_schema',
|
||||
jsonSchema: {
|
||||
name: 'contact',
|
||||
schema,
|
||||
strict: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(body['text']).toEqual({
|
||||
format: {
|
||||
type: 'json_schema',
|
||||
name: 'contact',
|
||||
schema,
|
||||
strict: true,
|
||||
description: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves existing Responses text options when applying response format', async () => {
|
||||
const provider = createProvider().withGenerationKwargs({
|
||||
text: { verbosity: 'low' },
|
||||
});
|
||||
const history: Message[] = [
|
||||
{ role: 'user', content: [{ type: 'text', text: 'Extract contact' }], toolCalls: [] },
|
||||
];
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: { name: { type: 'string' } },
|
||||
required: ['name'],
|
||||
additionalProperties: false,
|
||||
};
|
||||
const body = await captureRequestBody(provider, '', [], history, {
|
||||
responseFormat: {
|
||||
type: 'json_schema',
|
||||
jsonSchema: {
|
||||
name: 'contact',
|
||||
schema,
|
||||
strict: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(body['text']).toEqual({
|
||||
verbosity: 'low',
|
||||
format: {
|
||||
type: 'json_schema',
|
||||
name: 'contact',
|
||||
schema,
|
||||
strict: true,
|
||||
description: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('reasoning configuration', () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue