plugin-ai-api 1.1.1 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -12
- package/dist/client/185.c47663fefaeb0e5b.js +10 -0
- package/dist/client/562.9012cfd1fa04303d.js +10 -0
- package/dist/client/685.b5b1e0a5b825d253.js +10 -0
- package/dist/client/index.js +1 -1
- package/dist/client-v2/185.b552dc91ec2371ba.js +10 -0
- package/dist/client-v2/562.db2984167250b1be.js +10 -0
- package/dist/client-v2/685.cf16e5b829e06f85.js +10 -0
- package/dist/client-v2/index.js +1 -1
- package/dist/externalVersion.js +8 -8
- package/dist/locale/en-US.json +175 -139
- package/dist/locale/vi-VN.json +40 -2
- package/dist/locale/zh-CN.json +40 -2
- package/dist/server/collections/ai-api-model-metadata.js +26 -0
- package/dist/server/collections/ai-api-response-records.js +101 -0
- package/dist/server/collections/ai-api-virtual-models.js +68 -0
- package/dist/server/middleware/response-record-resource.js +66 -0
- package/dist/server/middleware/role-permission.js +43 -18
- package/dist/server/migrations/20260901000000-remove-default-group-members.js +60 -0
- package/dist/server/migrations/20260902000000-seed-default-role-permissions.js +55 -0
- package/dist/server/migrations/20260903000000-seed-sample-response-records.js +170 -0
- package/dist/server/plugin.js +66 -16
- package/dist/server/routes/chat-completions.js +38 -6
- package/dist/server/routes/completions.js +16 -4
- package/dist/server/routes/embeddings.js +25 -6
- package/dist/server/routes/models.js +29 -0
- package/dist/server/routes/responses.js +530 -0
- package/dist/server/routes/router.js +65 -10
- package/dist/server/usage.js +25 -4
- package/dist/server/utils/direct-llm-context.js +1 -1
- package/dist/server/utils/resolve-service.js +24 -0
- package/dist/server/utils/response-store.js +138 -0
- package/dist/server/utils/responses-format.js +686 -0
- package/dist/server/utils/responses-stream.js +330 -0
- package/dist/server/utils/virtual-models.js +238 -0
- package/dist/server/validation.js +44 -2
- package/dist/swagger.js +137 -0
- package/package.json +34 -32
- package/src/__tests__/locale.test.ts +43 -0
- package/src/client/__tests__/settings-registration.test.tsx +1 -0
- package/src/client/plugin.tsx +9 -1
- package/src/client-v2/__tests__/settings-registration.test.tsx +1 -0
- package/src/client-v2/pages/ModelMetadataPage.tsx +44 -0
- package/src/client-v2/pages/ModelRoutingPage.tsx +238 -0
- package/src/client-v2/pages/UsageGroupsPage.tsx +75 -38
- package/src/client-v2/plugin.tsx +8 -0
- package/src/locale/en-US.json +175 -139
- package/src/locale/vi-VN.json +40 -2
- package/src/locale/zh-CN.json +40 -2
- package/src/server/__tests__/embeddings.test.ts +184 -0
- package/src/server/__tests__/models.test.ts +21 -1
- package/src/server/__tests__/response-record-resource.test.ts +50 -0
- package/src/server/__tests__/response-store-integration.test.ts +341 -0
- package/src/server/__tests__/response-store.test.ts +195 -0
- package/src/server/__tests__/responses-contract.test.ts +469 -0
- package/src/server/__tests__/responses-format.test.ts +299 -0
- package/src/server/__tests__/responses-router.test.ts +182 -0
- package/src/server/__tests__/responses-streaming.test.ts +368 -0
- package/src/server/__tests__/responses.test.ts +462 -0
- package/src/server/__tests__/role-permission.test.ts +139 -0
- package/src/server/__tests__/seed-role-permission.test.ts +88 -0
- package/src/server/__tests__/types/responses-sdk.types.test-d.ts +23 -0
- package/src/server/__tests__/usage-groups.test.ts +96 -0
- package/src/server/__tests__/usage-route.test.ts +1 -0
- package/src/server/__tests__/usage.test.ts +14 -0
- package/src/server/__tests__/validation.test.ts +66 -7
- package/src/server/__tests__/virtual-model-routing.test.ts +589 -0
- package/src/server/collections/ai-api-model-metadata.ts +26 -0
- package/src/server/collections/ai-api-response-records.ts +77 -0
- package/src/server/collections/ai-api-virtual-models.ts +58 -0
- package/src/server/middleware/response-record-resource.ts +44 -0
- package/src/server/middleware/role-permission.ts +69 -35
- package/src/server/migrations/20260901000000-remove-default-group-members.ts +56 -0
- package/src/server/migrations/20260902000000-seed-default-role-permissions.ts +46 -0
- package/src/server/migrations/20260903000000-seed-sample-response-records.ts +162 -0
- package/src/server/plugin.ts +84 -20
- package/src/server/resource/ai-api-config.ts +2 -1
- package/src/server/routes/agent-completions.ts +3 -0
- package/src/server/routes/chat-completions.ts +34 -10
- package/src/server/routes/completions.ts +16 -4
- package/src/server/routes/embeddings.ts +32 -10
- package/src/server/routes/models.ts +34 -0
- package/src/server/routes/responses.ts +640 -0
- package/src/server/routes/router.ts +81 -12
- package/src/server/services/__tests__/file-processor.test.ts +1 -0
- package/src/server/usage.ts +29 -2
- package/src/server/utils/app-observability.ts +1 -1
- package/src/server/utils/direct-llm-context.ts +2 -1
- package/src/server/utils/openai-format.ts +1 -0
- package/src/server/utils/resolve-service.ts +39 -1
- package/src/server/utils/response-store.ts +148 -0
- package/src/server/utils/responses-format.ts +974 -0
- package/src/server/utils/responses-stream.ts +384 -0
- package/src/server/utils/virtual-models.ts +320 -0
- package/src/server/validation.ts +49 -0
- package/src/swagger.ts +139 -0
- package/dist/client/562.44b16aad4718b4c7.js +0 -10
- package/dist/client/685.ae483e17b6b49c98.js +0 -10
- package/dist/client-v2/562.45d5c504433be38b.js +0 -10
- package/dist/client-v2/685.1030370b309b7d4b.js +0 -10
- package/dist/server/collections/ai-api-user-permissions.js +0 -67
- package/dist/server/collections/ai-api-user-quota-buckets.js +0 -54
- package/dist/server/collections/ai-api-user-quota-policies.js +0 -63
- package/dist/server/resource/ai-api-usage-groups.js +0 -168
- package/src/server/collections/ai-api-user-permissions.ts +0 -46
- package/src/server/collections/ai-api-user-quota-buckets.ts +0 -24
- package/src/server/collections/ai-api-user-quota-policies.ts +0 -33
- package/src/server/resource/ai-api-usage-groups.ts +0 -171
|
@@ -0,0 +1,974 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import crypto from 'crypto';
|
|
11
|
+
import type { OpenAIMessage } from './direct-llm-context';
|
|
12
|
+
import type { OpenAIToolCall, OpenAIUsage } from './openai-format';
|
|
13
|
+
|
|
14
|
+
export type ResponseStatus = 'completed' | 'failed' | 'in_progress' | 'cancelled' | 'queued' | 'incomplete';
|
|
15
|
+
export type ResponseItemStatus = 'in_progress' | 'completed' | 'incomplete';
|
|
16
|
+
export type ResponseServiceTier = 'auto' | 'default' | 'flex' | 'scale' | 'priority';
|
|
17
|
+
|
|
18
|
+
export interface ResponseOutputTextFileCitation {
|
|
19
|
+
type: 'file_citation';
|
|
20
|
+
file_id: string;
|
|
21
|
+
filename: string;
|
|
22
|
+
index: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ResponseOutputTextURLCitation {
|
|
26
|
+
type: 'url_citation';
|
|
27
|
+
end_index: number;
|
|
28
|
+
start_index: number;
|
|
29
|
+
title: string;
|
|
30
|
+
url: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ResponseOutputTextContainerFileCitation {
|
|
34
|
+
type: 'container_file_citation';
|
|
35
|
+
container_id: string;
|
|
36
|
+
end_index: number;
|
|
37
|
+
file_id: string;
|
|
38
|
+
filename: string;
|
|
39
|
+
start_index: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ResponseOutputTextFilePath {
|
|
43
|
+
type: 'file_path';
|
|
44
|
+
file_id: string;
|
|
45
|
+
index: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type ResponseOutputTextAnnotation =
|
|
49
|
+
| ResponseOutputTextFileCitation
|
|
50
|
+
| ResponseOutputTextURLCitation
|
|
51
|
+
| ResponseOutputTextContainerFileCitation
|
|
52
|
+
| ResponseOutputTextFilePath;
|
|
53
|
+
|
|
54
|
+
export interface ResponseOutputTextTopLogprob {
|
|
55
|
+
token: string;
|
|
56
|
+
bytes: number[];
|
|
57
|
+
logprob: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ResponseOutputTextLogprob {
|
|
61
|
+
token: string;
|
|
62
|
+
bytes: number[];
|
|
63
|
+
logprob: number;
|
|
64
|
+
top_logprobs: ResponseOutputTextTopLogprob[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ResponseOutputText {
|
|
68
|
+
type: 'output_text';
|
|
69
|
+
text: string;
|
|
70
|
+
annotations: ResponseOutputTextAnnotation[];
|
|
71
|
+
logprobs?: ResponseOutputTextLogprob[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface ResponseOutputMessage {
|
|
75
|
+
id: string;
|
|
76
|
+
type: 'message';
|
|
77
|
+
status: ResponseItemStatus;
|
|
78
|
+
role: 'assistant';
|
|
79
|
+
content: ResponseOutputText[];
|
|
80
|
+
phase?: 'commentary' | 'final_answer' | null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface ResponseFunctionToolCall {
|
|
84
|
+
id: string;
|
|
85
|
+
type: 'function_call';
|
|
86
|
+
status: ResponseItemStatus;
|
|
87
|
+
call_id: string;
|
|
88
|
+
name: string;
|
|
89
|
+
arguments: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface ResponseReasoningItem {
|
|
93
|
+
id: string;
|
|
94
|
+
type: 'reasoning';
|
|
95
|
+
status: ResponseItemStatus;
|
|
96
|
+
summary: Array<{ type: 'summary_text'; text: string }>;
|
|
97
|
+
content?: Array<{ type: 'reasoning_text'; text: string }>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export type ResponseOutputItem = ResponseOutputMessage | ResponseFunctionToolCall | ResponseReasoningItem;
|
|
101
|
+
|
|
102
|
+
export interface ResponseUsage {
|
|
103
|
+
input_tokens: number;
|
|
104
|
+
input_tokens_details: { cached_tokens: number };
|
|
105
|
+
output_tokens: number;
|
|
106
|
+
output_tokens_details: { reasoning_tokens: number };
|
|
107
|
+
total_tokens: number;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface ResponseObject {
|
|
111
|
+
id: string;
|
|
112
|
+
object: 'response';
|
|
113
|
+
created_at: number;
|
|
114
|
+
completed_at?: number | null;
|
|
115
|
+
status: ResponseStatus;
|
|
116
|
+
error: {
|
|
117
|
+
code:
|
|
118
|
+
| 'server_error'
|
|
119
|
+
| 'rate_limit_exceeded'
|
|
120
|
+
| 'invalid_prompt'
|
|
121
|
+
| 'vector_store_timeout'
|
|
122
|
+
| 'invalid_image'
|
|
123
|
+
| 'invalid_image_format'
|
|
124
|
+
| 'invalid_base64_image'
|
|
125
|
+
| 'invalid_image_url'
|
|
126
|
+
| 'image_too_large'
|
|
127
|
+
| 'image_too_small'
|
|
128
|
+
| 'image_parse_error'
|
|
129
|
+
| 'image_content_policy_violation'
|
|
130
|
+
| 'invalid_image_mode'
|
|
131
|
+
| 'image_file_too_large'
|
|
132
|
+
| 'unsupported_image_media_type'
|
|
133
|
+
| 'empty_image_file'
|
|
134
|
+
| 'failed_to_download_image'
|
|
135
|
+
| 'image_file_not_found';
|
|
136
|
+
message: string;
|
|
137
|
+
} | null;
|
|
138
|
+
incomplete_details: { reason: 'max_output_tokens' | 'content_filter' } | null;
|
|
139
|
+
instructions: string | null;
|
|
140
|
+
max_output_tokens?: number | null;
|
|
141
|
+
model: string;
|
|
142
|
+
output: ResponseOutputItem[];
|
|
143
|
+
output_text: string;
|
|
144
|
+
parallel_tool_calls: boolean;
|
|
145
|
+
previous_response_id?: string | null;
|
|
146
|
+
prompt_cache_key?: string;
|
|
147
|
+
prompt_cache_retention?: 'in_memory' | '24h' | null;
|
|
148
|
+
reasoning?: Record<string, unknown> | null;
|
|
149
|
+
safety_identifier?: string;
|
|
150
|
+
service_tier: ResponseServiceTier;
|
|
151
|
+
temperature: number | null;
|
|
152
|
+
text?: {
|
|
153
|
+
format?:
|
|
154
|
+
| { type: 'text' }
|
|
155
|
+
| { type: 'json_object' }
|
|
156
|
+
| { type: 'json_schema'; name: string; schema: Record<string, unknown>; description?: string; strict?: boolean | null };
|
|
157
|
+
verbosity?: 'low' | 'medium' | 'high';
|
|
158
|
+
} | undefined;
|
|
159
|
+
tool_choice:
|
|
160
|
+
| 'none'
|
|
161
|
+
| 'auto'
|
|
162
|
+
| 'required'
|
|
163
|
+
| { type: 'allowed_tools'; mode: 'auto' | 'required'; tools: Array<Record<string, unknown>> }
|
|
164
|
+
| { type: 'function'; name: string }
|
|
165
|
+
| { type: 'mcp'; server_label: string; name?: string | null }
|
|
166
|
+
| { type: 'custom'; name: string }
|
|
167
|
+
| { type: 'apply_patch' }
|
|
168
|
+
| { type: 'shell' }
|
|
169
|
+
| {
|
|
170
|
+
type:
|
|
171
|
+
| 'file_search'
|
|
172
|
+
| 'web_search_preview'
|
|
173
|
+
| 'computer'
|
|
174
|
+
| 'computer_use_preview'
|
|
175
|
+
| 'computer_use'
|
|
176
|
+
| 'web_search_preview_2025_03_11'
|
|
177
|
+
| 'image_generation'
|
|
178
|
+
| 'code_interpreter'
|
|
179
|
+
| 'mcp';
|
|
180
|
+
};
|
|
181
|
+
tools: Array<
|
|
182
|
+
| { type: 'function'; name: string; parameters: Record<string, unknown> | null; strict: boolean | null; description?: string }
|
|
183
|
+
| { type: 'file_search'; vector_store_ids: string[]; max_num_results?: number }
|
|
184
|
+
| {
|
|
185
|
+
type: 'web_search_preview';
|
|
186
|
+
user_location?: { type: 'approximate'; city?: string | null; country?: string | null; region?: string | null; timezone?: string | null } | null;
|
|
187
|
+
search_context_size?: 'low' | 'medium' | 'high';
|
|
188
|
+
}
|
|
189
|
+
| { type: 'computer_use_preview'; display_width: number; display_height: number; environment: 'windows' | 'mac' | 'linux' | 'ubuntu' | 'browser' }
|
|
190
|
+
| { type: 'code_interpreter'; container: string | { type: 'auto'; file_ids?: string[]; memory_limit?: '1g' | '4g' | '16g' | '64g' | null } }
|
|
191
|
+
| { type: 'mcp'; server_label: string; server_url?: string }
|
|
192
|
+
| {
|
|
193
|
+
type: 'image_generation';
|
|
194
|
+
size?: string;
|
|
195
|
+
quality?: 'auto' | 'low' | 'medium' | 'high';
|
|
196
|
+
output_format?: 'png' | 'webp' | 'jpeg';
|
|
197
|
+
action?: 'generate' | 'edit' | 'auto';
|
|
198
|
+
background?: 'transparent' | 'opaque' | 'auto';
|
|
199
|
+
}
|
|
200
|
+
| { type: 'custom'; name: string; description?: string; format?: { type: 'text' } | { type: 'grammar'; definition: string; syntax: 'lark' | 'regex' }; defer_loading?: boolean }
|
|
201
|
+
| { type: 'shell' }
|
|
202
|
+
| { type: 'apply_patch' }
|
|
203
|
+
>;
|
|
204
|
+
top_p: number | null;
|
|
205
|
+
truncation: 'auto' | 'disabled';
|
|
206
|
+
usage: ResponseUsage;
|
|
207
|
+
user?: string;
|
|
208
|
+
metadata: Record<string, string> | null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface ResponseResultParts {
|
|
212
|
+
content: string;
|
|
213
|
+
reasoningText?: string;
|
|
214
|
+
usage?: OpenAIUsage;
|
|
215
|
+
toolCalls?: OpenAIToolCall[];
|
|
216
|
+
finishReason?: string;
|
|
217
|
+
serviceTier?: ResponseServiceTier;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function generateResponseId(): string {
|
|
221
|
+
return `resp_${crypto.randomBytes(16).toString('hex')}`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function generateResponseItemId(prefix: 'msg' | 'fc' | 'rs' = 'msg'): string {
|
|
225
|
+
return `${prefix}_${crypto.randomBytes(16).toString('hex')}`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
229
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function responseImageToChatBlock(block: Record<string, unknown>): Record<string, unknown> | null {
|
|
233
|
+
if (typeof block.image_url !== 'string' || !block.image_url) return null;
|
|
234
|
+
return {
|
|
235
|
+
type: 'image_url',
|
|
236
|
+
image_url: {
|
|
237
|
+
url: block.image_url,
|
|
238
|
+
...(typeof block.detail === 'string' ? { detail: block.detail } : {}),
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function responseFileToChatBlock(block: Record<string, unknown>): Record<string, unknown> | null {
|
|
244
|
+
if (typeof block.file_data === 'string' && block.file_data) {
|
|
245
|
+
return {
|
|
246
|
+
type: 'file',
|
|
247
|
+
file: {
|
|
248
|
+
file_data: block.file_data,
|
|
249
|
+
...(typeof block.filename === 'string' ? { filename: block.filename } : {}),
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
if (typeof block.file_url === 'string' && block.file_url) {
|
|
254
|
+
return { type: 'file_url', file_url: { url: block.file_url } };
|
|
255
|
+
}
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function responseContentToChatContent(content: unknown): unknown {
|
|
260
|
+
if (typeof content === 'string') return content;
|
|
261
|
+
if (!Array.isArray(content)) return content;
|
|
262
|
+
|
|
263
|
+
return content.flatMap((block): unknown[] => {
|
|
264
|
+
if (typeof block === 'string') return [{ type: 'text', text: block }];
|
|
265
|
+
if (!isRecord(block)) return [];
|
|
266
|
+
if ((block.type === 'input_text' || block.type === 'output_text') && typeof block.text === 'string') {
|
|
267
|
+
return [{ type: 'text', text: block.text }];
|
|
268
|
+
}
|
|
269
|
+
if (block.type === 'input_image') {
|
|
270
|
+
const converted = responseImageToChatBlock(block);
|
|
271
|
+
return converted ? [converted] : [];
|
|
272
|
+
}
|
|
273
|
+
if (block.type === 'input_file') {
|
|
274
|
+
const converted = responseFileToChatBlock(block);
|
|
275
|
+
return converted ? [converted] : [];
|
|
276
|
+
}
|
|
277
|
+
if (block.type === 'text' || block.type === 'image_url' || block.type === 'file' || block.type === 'file_url') {
|
|
278
|
+
return [block];
|
|
279
|
+
}
|
|
280
|
+
return [];
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function messageItemToChatMessage(item: Record<string, unknown>): OpenAIMessage | null {
|
|
285
|
+
const role = typeof item.role === 'string' ? item.role : undefined;
|
|
286
|
+
if (!role) return null;
|
|
287
|
+
return {
|
|
288
|
+
role,
|
|
289
|
+
content: responseContentToChatContent(item.content),
|
|
290
|
+
...(typeof item.phase === 'string' ? { phase: item.phase } : {}),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function functionCallToChatMessage(item: Record<string, unknown>): OpenAIMessage | null {
|
|
295
|
+
if (typeof item.call_id !== 'string' || typeof item.name !== 'string') return null;
|
|
296
|
+
return {
|
|
297
|
+
role: 'assistant',
|
|
298
|
+
content: '',
|
|
299
|
+
tool_calls: [
|
|
300
|
+
{
|
|
301
|
+
id: item.call_id,
|
|
302
|
+
type: 'function',
|
|
303
|
+
function: {
|
|
304
|
+
name: item.name,
|
|
305
|
+
arguments: typeof item.arguments === 'string' ? item.arguments : '{}',
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
],
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function appendFunctionCall(messages: OpenAIMessage[], item: Record<string, unknown>): boolean {
|
|
313
|
+
const message = functionCallToChatMessage(item);
|
|
314
|
+
if (!message) return false;
|
|
315
|
+
const previous = messages.at(-1);
|
|
316
|
+
const previousAdditional = isRecord(previous?.additional_kwargs) ? (previous as OpenAIMessage).additional_kwargs as Record<string, unknown> : {} as Record<string, unknown>;
|
|
317
|
+
const hasReasoning =
|
|
318
|
+
typeof previousAdditional.reasoning_content === 'string' || isRecord(previousAdditional.reasoning);
|
|
319
|
+
if (
|
|
320
|
+
previous?.role !== 'assistant' ||
|
|
321
|
+
previous.content !== '' ||
|
|
322
|
+
((!Array.isArray(previous.tool_calls) || previous.tool_calls.length === 0) && !hasReasoning)
|
|
323
|
+
) {
|
|
324
|
+
messages.push(message);
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const previousCalls = Array.isArray(previous.tool_calls) ? previous.tool_calls : [];
|
|
329
|
+
const nextCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
330
|
+
previous.tool_calls = [...previousCalls, ...nextCalls];
|
|
331
|
+
previous.additional_kwargs = {
|
|
332
|
+
...(isRecord(previous.additional_kwargs) ? previous.additional_kwargs : {}),
|
|
333
|
+
tool_calls: previous.tool_calls,
|
|
334
|
+
};
|
|
335
|
+
const responseMetadata = isRecord(previous.response_metadata) ? previous.response_metadata : {};
|
|
336
|
+
const output = Array.isArray(responseMetadata.output) ? responseMetadata.output : [];
|
|
337
|
+
previous.response_metadata = { ...responseMetadata, output: [...output, item] };
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function reasoningToChatMessage(item: Record<string, unknown>): OpenAIMessage {
|
|
342
|
+
const summary = Array.isArray(item.summary)
|
|
343
|
+
? item.summary
|
|
344
|
+
.filter((part): part is Record<string, unknown> => isRecord(part) && typeof part.text === 'string')
|
|
345
|
+
.map((part) => part.text)
|
|
346
|
+
.join('\n')
|
|
347
|
+
: '';
|
|
348
|
+
const content = Array.isArray(item.content)
|
|
349
|
+
? item.content
|
|
350
|
+
.filter((part): part is Record<string, unknown> => isRecord(part) && typeof part.text === 'string')
|
|
351
|
+
.map((part) => part.text)
|
|
352
|
+
.join('\n')
|
|
353
|
+
: '';
|
|
354
|
+
return {
|
|
355
|
+
role: 'assistant',
|
|
356
|
+
content: '',
|
|
357
|
+
additional_kwargs: {
|
|
358
|
+
reasoning_content: content || summary,
|
|
359
|
+
reasoning: item,
|
|
360
|
+
},
|
|
361
|
+
response_metadata: { output: [item] },
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function appendReasoningItem(messages: OpenAIMessage[], item: Record<string, unknown>): void {
|
|
366
|
+
const previous = messages.at(-1);
|
|
367
|
+
if (previous?.role === 'assistant' && previous.content === '') {
|
|
368
|
+
const reasoning = reasoningToChatMessage(item);
|
|
369
|
+
previous.additional_kwargs = {
|
|
370
|
+
...(isRecord(previous.additional_kwargs) ? previous.additional_kwargs : {}),
|
|
371
|
+
...(isRecord(reasoning.additional_kwargs) ? reasoning.additional_kwargs : {}),
|
|
372
|
+
};
|
|
373
|
+
const responseMetadata = isRecord(previous.response_metadata) ? previous.response_metadata : {};
|
|
374
|
+
const output = Array.isArray(responseMetadata.output) ? responseMetadata.output : [];
|
|
375
|
+
previous.response_metadata = { ...responseMetadata, output: [...output, item] };
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
messages.push(reasoningToChatMessage(item));
|
|
379
|
+
}
|
|
380
|
+
function functionCallOutputToChatMessage(item: Record<string, unknown>): OpenAIMessage | null {
|
|
381
|
+
if (typeof item.call_id !== 'string') return null;
|
|
382
|
+
return {
|
|
383
|
+
role: 'tool',
|
|
384
|
+
content: responseContentToChatContent(item.output),
|
|
385
|
+
tool_call_id: item.call_id,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export function responsesInputToMessages(input: unknown, instructions?: unknown): OpenAIMessage[] {
|
|
390
|
+
const messages: OpenAIMessage[] = [];
|
|
391
|
+
if (typeof instructions === 'string' && instructions) {
|
|
392
|
+
messages.push({ role: 'developer', content: instructions });
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (typeof input === 'string') return [...messages, { role: 'user', content: input }];
|
|
396
|
+
if (!Array.isArray(input)) return messages;
|
|
397
|
+
|
|
398
|
+
for (const item of input) {
|
|
399
|
+
if (!isRecord(item)) continue;
|
|
400
|
+
if (item.type === 'function_call') {
|
|
401
|
+
appendFunctionCall(messages, item);
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
if (item.type === 'reasoning') {
|
|
405
|
+
appendReasoningItem(messages, item);
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
let message: OpenAIMessage | null = null;
|
|
409
|
+
if (item.type === 'function_call_output') message = functionCallOutputToChatMessage(item);
|
|
410
|
+
else if (item.type === 'message' || typeof item.role === 'string') message = messageItemToChatMessage(item);
|
|
411
|
+
if (message) messages.push(message);
|
|
412
|
+
}
|
|
413
|
+
return messages;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function responseFunctionToolToChatTool(tool: Record<string, unknown>): Record<string, unknown> | null {
|
|
417
|
+
if (tool.type !== 'function' || typeof tool.name !== 'string') return null;
|
|
418
|
+
return {
|
|
419
|
+
type: 'function',
|
|
420
|
+
function: {
|
|
421
|
+
name: tool.name,
|
|
422
|
+
...(typeof tool.description === 'string' ? { description: tool.description } : {}),
|
|
423
|
+
parameters: isRecord(tool.parameters) ? tool.parameters : {},
|
|
424
|
+
...(typeof tool.strict === 'boolean' ? { strict: tool.strict } : {}),
|
|
425
|
+
},
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export function responsesToolsToChatTools(tools: unknown): unknown[] | undefined {
|
|
430
|
+
if (!Array.isArray(tools)) return undefined;
|
|
431
|
+
return tools.flatMap((tool) => {
|
|
432
|
+
if (!isRecord(tool)) return [];
|
|
433
|
+
const converted = responseFunctionToolToChatTool(tool);
|
|
434
|
+
return converted ? [converted] : [];
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function responsesToolChoiceToChatToolChoice(toolChoice: unknown): unknown {
|
|
439
|
+
if (!isRecord(toolChoice) || toolChoice.type !== 'function' || typeof toolChoice.name !== 'string') {
|
|
440
|
+
return toolChoice;
|
|
441
|
+
}
|
|
442
|
+
return { type: 'function', function: { name: toolChoice.name } };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export function responsesBodyToChatBody(body: Record<string, unknown>): Record<string, unknown> {
|
|
446
|
+
const chatBody: Record<string, unknown> = {
|
|
447
|
+
model: body.model,
|
|
448
|
+
messages: responsesInputToMessages(body.input, body.instructions),
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
if (body.temperature !== undefined) chatBody.temperature = body.temperature;
|
|
452
|
+
if (body.top_p !== undefined) chatBody.top_p = body.top_p;
|
|
453
|
+
if (body.max_output_tokens !== undefined) chatBody.max_completion_tokens = body.max_output_tokens;
|
|
454
|
+
const tools = responsesToolsToChatTools(body.tools);
|
|
455
|
+
if (tools) chatBody.tools = tools;
|
|
456
|
+
if (body.tool_choice !== undefined) chatBody.tool_choice = responsesToolChoiceToChatToolChoice(body.tool_choice);
|
|
457
|
+
if (body.stream !== undefined) chatBody.stream = body.stream;
|
|
458
|
+
if (body.service_tier !== undefined && body.service_tier !== null) chatBody.service_tier = body.service_tier;
|
|
459
|
+
if (body.prompt_cache_key !== undefined) chatBody.promptCacheKey = body.prompt_cache_key;
|
|
460
|
+
if (body.prompt_cache_retention !== undefined && body.prompt_cache_retention !== null) {
|
|
461
|
+
chatBody.promptCacheRetention = body.prompt_cache_retention;
|
|
462
|
+
}
|
|
463
|
+
if (body.reasoning !== undefined && body.reasoning !== null) chatBody.reasoning = body.reasoning;
|
|
464
|
+
if (body.safety_identifier !== undefined) chatBody.safety_identifier = body.safety_identifier;
|
|
465
|
+
if (body.text !== undefined) {
|
|
466
|
+
if (isRecord(body.text)) {
|
|
467
|
+
if (body.text.format !== undefined) chatBody.response_format = responsesTextFormatToChatFormat(body.text.format);
|
|
468
|
+
if (body.text.verbosity !== undefined) chatBody.verbosity = body.text.verbosity;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (body.parallel_tool_calls !== undefined && body.parallel_tool_calls !== null) {
|
|
472
|
+
chatBody.parallel_tool_calls = body.parallel_tool_calls;
|
|
473
|
+
}
|
|
474
|
+
if (body.user !== undefined) chatBody.user = body.user;
|
|
475
|
+
return chatBody;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export function responseUsageFromOpenAIUsage(usage: OpenAIUsage | undefined, reasoningTokens = 0): ResponseUsage {
|
|
479
|
+
return {
|
|
480
|
+
input_tokens: usage?.prompt_tokens ?? 0,
|
|
481
|
+
input_tokens_details: { cached_tokens: usage?.prompt_cache_tokens ?? 0 },
|
|
482
|
+
output_tokens: usage?.completion_tokens ?? 0,
|
|
483
|
+
output_tokens_details: { reasoning_tokens: usage?.reasoning_tokens ?? reasoningTokens },
|
|
484
|
+
total_tokens: usage?.total_tokens ?? 0,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
export function createResponseOutputMessage(
|
|
489
|
+
text: string,
|
|
490
|
+
status: ResponseItemStatus = 'completed',
|
|
491
|
+
id = generateResponseItemId('msg'),
|
|
492
|
+
phase?: ResponseOutputMessage['phase'],
|
|
493
|
+
): ResponseOutputMessage {
|
|
494
|
+
return {
|
|
495
|
+
id,
|
|
496
|
+
type: 'message',
|
|
497
|
+
status,
|
|
498
|
+
role: 'assistant',
|
|
499
|
+
content: text ? [{ type: 'output_text', text, annotations: [] }] : [],
|
|
500
|
+
...(phase === undefined ? {} : { phase }),
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export function extractResponseOutputText(value: unknown): string {
|
|
505
|
+
if (!isRecord(value)) return '';
|
|
506
|
+
if (typeof value.content === 'string') return value.content;
|
|
507
|
+
if (!Array.isArray(value.content)) return '';
|
|
508
|
+
return value.content
|
|
509
|
+
.filter(
|
|
510
|
+
(block): block is Record<string, unknown> =>
|
|
511
|
+
isRecord(block) && (block.type === 'text' || block.type === 'output_text'),
|
|
512
|
+
)
|
|
513
|
+
.map((block) => (typeof block.text === 'string' ? block.text : ''))
|
|
514
|
+
.join('');
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function toolCallToResponseFunctionCall(call: OpenAIToolCall): ResponseFunctionToolCall {
|
|
518
|
+
return {
|
|
519
|
+
id: generateResponseItemId('fc'),
|
|
520
|
+
type: 'function_call',
|
|
521
|
+
status: 'completed',
|
|
522
|
+
call_id: call.id,
|
|
523
|
+
name: call.function.name,
|
|
524
|
+
arguments: call.function.arguments,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export function createResponseReasoningItem(text: string): ResponseReasoningItem {
|
|
529
|
+
return {
|
|
530
|
+
id: generateResponseItemId('rs'),
|
|
531
|
+
type: 'reasoning',
|
|
532
|
+
status: 'completed',
|
|
533
|
+
summary: [],
|
|
534
|
+
content: text ? [{ type: 'reasoning_text', text }] : [],
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function numberOrNull(value: unknown): number | null {
|
|
539
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function responseMetadata(value: unknown): Record<string, string> | null {
|
|
543
|
+
if (!isRecord(value)) return null;
|
|
544
|
+
const entries = Object.entries(value).filter((entry): entry is [string, string] => typeof entry[1] === 'string');
|
|
545
|
+
return Object.fromEntries(entries);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
export function chatResultToResponse(
|
|
549
|
+
options: ResponseResultParts & {
|
|
550
|
+
id?: string;
|
|
551
|
+
model: string;
|
|
552
|
+
requestBody: Record<string, unknown>;
|
|
553
|
+
status?: ResponseStatus;
|
|
554
|
+
},
|
|
555
|
+
): ResponseObject {
|
|
556
|
+
const {
|
|
557
|
+
id = generateResponseId(),
|
|
558
|
+
model,
|
|
559
|
+
content,
|
|
560
|
+
reasoningText,
|
|
561
|
+
usage,
|
|
562
|
+
toolCalls,
|
|
563
|
+
finishReason,
|
|
564
|
+
serviceTier,
|
|
565
|
+
requestBody,
|
|
566
|
+
status = responseIncompleteReason(finishReason) ? 'incomplete' : 'completed',
|
|
567
|
+
} = options;
|
|
568
|
+
const output: ResponseOutputItem[] = [];
|
|
569
|
+
if (reasoningText) output.push(createResponseReasoningItem(reasoningText));
|
|
570
|
+
if (content) output.push(createResponseOutputMessage(content));
|
|
571
|
+
if (toolCalls?.length) output.push(...toolCalls.map(toolCallToResponseFunctionCall));
|
|
572
|
+
const now = Math.floor(Date.now() / 1000);
|
|
573
|
+
|
|
574
|
+
return {
|
|
575
|
+
id,
|
|
576
|
+
object: 'response',
|
|
577
|
+
created_at: now,
|
|
578
|
+
completed_at: status === 'completed' ? now : null,
|
|
579
|
+
status,
|
|
580
|
+
error: null,
|
|
581
|
+
incomplete_details:
|
|
582
|
+
status === 'incomplete' ? { reason: responseIncompleteReason(finishReason) ?? 'max_output_tokens' } : null,
|
|
583
|
+
instructions: typeof requestBody.instructions === 'string' ? requestBody.instructions : null,
|
|
584
|
+
...(requestBody.max_output_tokens === undefined
|
|
585
|
+
? {}
|
|
586
|
+
: { max_output_tokens: numberOrNull(requestBody.max_output_tokens) }),
|
|
587
|
+
model,
|
|
588
|
+
output,
|
|
589
|
+
output_text: content,
|
|
590
|
+
parallel_tool_calls: requestBody.parallel_tool_calls !== false,
|
|
591
|
+
...(requestBody.previous_response_id === undefined
|
|
592
|
+
? {}
|
|
593
|
+
: {
|
|
594
|
+
previous_response_id:
|
|
595
|
+
typeof requestBody.previous_response_id === 'string' ? requestBody.previous_response_id : null,
|
|
596
|
+
}),
|
|
597
|
+
prompt_cache_key: typeof requestBody.prompt_cache_key === 'string' ? requestBody.prompt_cache_key : undefined,
|
|
598
|
+
prompt_cache_retention:
|
|
599
|
+
requestBody.prompt_cache_retention === 'in_memory' || requestBody.prompt_cache_retention === '24h'
|
|
600
|
+
? requestBody.prompt_cache_retention
|
|
601
|
+
: requestBody.prompt_cache_retention === null
|
|
602
|
+
? null
|
|
603
|
+
: undefined,
|
|
604
|
+
...(requestBody.reasoning === undefined
|
|
605
|
+
? {}
|
|
606
|
+
: { reasoning: isRecord(requestBody.reasoning) ? requestBody.reasoning : null }),
|
|
607
|
+
safety_identifier: typeof requestBody.safety_identifier === 'string' ? requestBody.safety_identifier : undefined,
|
|
608
|
+
service_tier: serviceTier ?? (isResponseServiceTier(requestBody.service_tier) ? requestBody.service_tier : 'auto'),
|
|
609
|
+
temperature: numberOrNull(requestBody.temperature),
|
|
610
|
+
...(requestBody.text === undefined ? {} : { text: isRecord(requestBody.text) ? (requestBody.text as ResponseObject['text']) : undefined }),
|
|
611
|
+
tool_choice: (requestBody.tool_choice as ResponseObject['tool_choice']) ?? 'auto',
|
|
612
|
+
tools: Array.isArray(requestBody.tools) ? requestBody.tools : [],
|
|
613
|
+
top_p: numberOrNull(requestBody.top_p),
|
|
614
|
+
truncation: requestBody.truncation === 'auto' ? 'auto' : 'disabled',
|
|
615
|
+
usage: responseUsageFromOpenAIUsage(usage),
|
|
616
|
+
user: typeof requestBody.user === 'string' ? requestBody.user : undefined,
|
|
617
|
+
metadata: responseMetadata(requestBody.metadata),
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
export function extractReasoningText(value: unknown): string | undefined {
|
|
622
|
+
if (!isRecord(value)) return undefined;
|
|
623
|
+
const candidates: unknown[] = [
|
|
624
|
+
value.reasoning_content,
|
|
625
|
+
value.reasoning,
|
|
626
|
+
isRecord(value.additional_kwargs) ? value.additional_kwargs.reasoning_content : undefined,
|
|
627
|
+
isRecord(value.additional_kwargs) ? value.additional_kwargs.reasoning : undefined,
|
|
628
|
+
isRecord(value.response_metadata) ? value.response_metadata.reasoning_content : undefined,
|
|
629
|
+
isRecord(value.response_metadata) ? value.response_metadata.reasoning : undefined,
|
|
630
|
+
];
|
|
631
|
+
for (const candidate of candidates) {
|
|
632
|
+
const text = reasoningValueToText(candidate);
|
|
633
|
+
if (text) return text;
|
|
634
|
+
}
|
|
635
|
+
if (Array.isArray(value.content)) {
|
|
636
|
+
const text = value.content
|
|
637
|
+
.filter(
|
|
638
|
+
(block): block is Record<string, unknown> =>
|
|
639
|
+
isRecord(block) && (block.type === 'reasoning' || block.type === 'reasoning_content'),
|
|
640
|
+
)
|
|
641
|
+
.map(reasoningValueToText)
|
|
642
|
+
.filter((part): part is string => Boolean(part))
|
|
643
|
+
.join('');
|
|
644
|
+
if (text) return text;
|
|
645
|
+
}
|
|
646
|
+
return undefined;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function reasoningValueToText(value: unknown): string | undefined {
|
|
650
|
+
if (typeof value === 'string') return value || undefined;
|
|
651
|
+
if (!isRecord(value)) return undefined;
|
|
652
|
+
if (typeof value.reasoning === 'string' && value.reasoning) return value.reasoning;
|
|
653
|
+
if (typeof value.text === 'string' && value.text) return value.text;
|
|
654
|
+
if (typeof value.content === 'string' && value.content) return value.content;
|
|
655
|
+
|
|
656
|
+
for (const key of ['content', 'summary']) {
|
|
657
|
+
const parts = value[key];
|
|
658
|
+
if (!Array.isArray(parts)) continue;
|
|
659
|
+
const text = parts
|
|
660
|
+
.filter(isRecord)
|
|
661
|
+
.map((part) => (typeof part.text === 'string' ? part.text : ''))
|
|
662
|
+
.filter(Boolean)
|
|
663
|
+
.join('');
|
|
664
|
+
if (text) return text;
|
|
665
|
+
}
|
|
666
|
+
return undefined;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
export function responseIncompleteReason(
|
|
670
|
+
finishReason: string | undefined,
|
|
671
|
+
): NonNullable<ResponseObject['incomplete_details']>['reason'] | null {
|
|
672
|
+
if (finishReason === 'length' || finishReason === 'max_tokens' || finishReason === 'max_output_tokens') {
|
|
673
|
+
return 'max_output_tokens';
|
|
674
|
+
}
|
|
675
|
+
if (finishReason === 'content_filter') return 'content_filter';
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/** Read the terminal reason LangChain preserves from an upstream Responses API object. */
|
|
680
|
+
export function extractResponseIncompleteReason(value: unknown): string | undefined {
|
|
681
|
+
if (!isRecord(value)) return undefined;
|
|
682
|
+
const metadataCandidates = [value.response_metadata, value.additional_kwargs, value];
|
|
683
|
+
for (const candidate of metadataCandidates) {
|
|
684
|
+
if (!isRecord(candidate)) continue;
|
|
685
|
+
if (candidate.status === 'incomplete' && candidate.incomplete_details === null) return 'max_output_tokens';
|
|
686
|
+
const details = candidate.incomplete_details;
|
|
687
|
+
if (!isRecord(details)) continue;
|
|
688
|
+
if (details.reason === 'max_output_tokens' || details.reason === 'content_filter') return details.reason;
|
|
689
|
+
}
|
|
690
|
+
return undefined;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
export function extractResponseServiceTier(value: unknown): ResponseServiceTier | undefined {
|
|
694
|
+
if (!isRecord(value)) return undefined;
|
|
695
|
+
const responseMetadata = isRecord(value.response_metadata) ? value.response_metadata : undefined;
|
|
696
|
+
const candidate = responseMetadata?.service_tier ?? responseMetadata?.serviceTier ?? value.service_tier;
|
|
697
|
+
return isResponseServiceTier(candidate) ? candidate : undefined;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
export function isResponseServiceTier(value: unknown): value is ResponseServiceTier {
|
|
701
|
+
return value === 'auto' || value === 'default' || value === 'flex' || value === 'scale' || value === 'priority';
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
export function findResponseParameterProblem(body: Record<string, unknown>): string | undefined {
|
|
705
|
+
if (body.instructions !== undefined && body.instructions !== null && typeof body.instructions !== 'string') {
|
|
706
|
+
return "'instructions' must be a string or null";
|
|
707
|
+
}
|
|
708
|
+
const numberFields: Array<{ name: string; min: number; max?: number; integer?: boolean }> = [
|
|
709
|
+
{ name: 'max_output_tokens', min: 1, integer: true },
|
|
710
|
+
{ name: 'temperature', min: 0, max: 2 },
|
|
711
|
+
{ name: 'top_p', min: 0, max: 1 },
|
|
712
|
+
];
|
|
713
|
+
for (const field of numberFields) {
|
|
714
|
+
const value = body[field.name];
|
|
715
|
+
if (value === undefined || value === null) continue;
|
|
716
|
+
if (
|
|
717
|
+
typeof value !== 'number' ||
|
|
718
|
+
!Number.isFinite(value) ||
|
|
719
|
+
value < field.min ||
|
|
720
|
+
(field.max !== undefined && value > field.max) ||
|
|
721
|
+
(field.integer && !Number.isSafeInteger(value))
|
|
722
|
+
) {
|
|
723
|
+
return `'${field.name}' must be ${field.integer ? 'an integer' : 'a number'} between ${field.min} and ${
|
|
724
|
+
field.max ?? 'the supported model limit'
|
|
725
|
+
}`;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
for (const name of ['parallel_tool_calls', 'store', 'stream']) {
|
|
729
|
+
if (body[name] !== undefined && body[name] !== null && typeof body[name] !== 'boolean') {
|
|
730
|
+
return `'${name}' must be a boolean or null`;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
for (const name of ['prompt_cache_key', 'safety_identifier', 'user']) {
|
|
734
|
+
if (body[name] !== undefined && typeof body[name] !== 'string') {
|
|
735
|
+
return `'${name}' must be a string`;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
if (typeof body.safety_identifier === 'string' && body.safety_identifier.length > 64) {
|
|
739
|
+
return "'safety_identifier' must be at most 64 characters";
|
|
740
|
+
}
|
|
741
|
+
if (
|
|
742
|
+
body.prompt_cache_retention !== undefined &&
|
|
743
|
+
body.prompt_cache_retention !== null &&
|
|
744
|
+
body.prompt_cache_retention !== 'in_memory' &&
|
|
745
|
+
body.prompt_cache_retention !== '24h'
|
|
746
|
+
) {
|
|
747
|
+
return "'prompt_cache_retention' must be 'in_memory', '24h', or null";
|
|
748
|
+
}
|
|
749
|
+
if (body.reasoning !== undefined && body.reasoning !== null) {
|
|
750
|
+
if (!isRecord(body.reasoning)) return "'reasoning' must be an object or null";
|
|
751
|
+
const effort = body.reasoning.effort;
|
|
752
|
+
if (
|
|
753
|
+
effort !== undefined &&
|
|
754
|
+
effort !== null &&
|
|
755
|
+
effort !== 'none' &&
|
|
756
|
+
effort !== 'minimal' &&
|
|
757
|
+
effort !== 'low' &&
|
|
758
|
+
effort !== 'medium' &&
|
|
759
|
+
effort !== 'high' &&
|
|
760
|
+
effort !== 'xhigh'
|
|
761
|
+
) {
|
|
762
|
+
return "'reasoning.effort' must be none, minimal, low, medium, high, xhigh, or null";
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
if (body.text !== undefined && body.text !== null) {
|
|
766
|
+
if (!isRecord(body.text)) return "'text' must be an object or null";
|
|
767
|
+
if (body.text.verbosity !== undefined && body.text.verbosity !== null) {
|
|
768
|
+
if (body.text.verbosity !== 'low' && body.text.verbosity !== 'medium' && body.text.verbosity !== 'high') {
|
|
769
|
+
return "'text.verbosity' must be low, medium, high, or null";
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
const format = body.text.format;
|
|
773
|
+
if (format !== undefined && format !== null) {
|
|
774
|
+
if (!isRecord(format)) return "'text.format' must be an object or null";
|
|
775
|
+
if (format.type !== 'text' && format.type !== 'json_object' && format.type !== 'json_schema') {
|
|
776
|
+
return "'text.format.type' must be text, json_object, or json_schema";
|
|
777
|
+
}
|
|
778
|
+
if (
|
|
779
|
+
format.type === 'json_schema' &&
|
|
780
|
+
(typeof format.name !== 'string' || !format.name || !isRecord(format.schema))
|
|
781
|
+
) {
|
|
782
|
+
return "'text.format' with type json_schema requires a non-empty name and an object schema";
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
if (body.previous_response_id !== undefined && body.previous_response_id !== null) {
|
|
787
|
+
if (typeof body.previous_response_id !== 'string' || !body.previous_response_id) {
|
|
788
|
+
return "'previous_response_id' must be a non-empty string or null";
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
return undefined;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
export function findUnsupportedResponseInput(input: unknown): string | undefined {
|
|
795
|
+
if (!Array.isArray(input)) return undefined;
|
|
796
|
+
for (const item of input) {
|
|
797
|
+
if (!isRecord(item) || !Array.isArray(item.content)) continue;
|
|
798
|
+
for (const block of item.content) {
|
|
799
|
+
if (!isRecord(block)) continue;
|
|
800
|
+
if (block.type === 'input_image' && typeof block.file_id === 'string') {
|
|
801
|
+
return 'input_image.file_id';
|
|
802
|
+
}
|
|
803
|
+
if (block.type === 'input_file' && typeof block.file_id === 'string') {
|
|
804
|
+
return 'input_file.file_id';
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return undefined;
|
|
809
|
+
}
|
|
810
|
+
export function findResponseToolProblem(tools: unknown): string | undefined {
|
|
811
|
+
if (tools === undefined) return undefined;
|
|
812
|
+
if (!Array.isArray(tools)) return "'tools' must be an array";
|
|
813
|
+
for (const [index, tool] of tools.entries()) {
|
|
814
|
+
if (!isRecord(tool)) return `tools[${index}] must be an object`;
|
|
815
|
+
if (tool.type !== 'function') return `tool type '${String(tool.type ?? 'unknown')}' is not supported`;
|
|
816
|
+
if (typeof tool.name !== 'string' || !tool.name) return `tools[${index}].name must be a non-empty string`;
|
|
817
|
+
if (tool.parameters !== undefined && tool.parameters !== null && !isRecord(tool.parameters)) {
|
|
818
|
+
return `tools[${index}].parameters must be an object or null`;
|
|
819
|
+
}
|
|
820
|
+
if (tool.strict !== undefined && tool.strict !== null && typeof tool.strict !== 'boolean') {
|
|
821
|
+
return `tools[${index}].strict must be a boolean or null`;
|
|
822
|
+
}
|
|
823
|
+
if (tool.description !== undefined && tool.description !== null && typeof tool.description !== 'string') {
|
|
824
|
+
return `tools[${index}].description must be a string or null`;
|
|
825
|
+
}
|
|
826
|
+
if (tool.defer_loading !== undefined) return `tools[${index}].defer_loading is not supported`;
|
|
827
|
+
}
|
|
828
|
+
return undefined;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
export function findResponseToolChoiceProblem(toolChoice: unknown): string | undefined {
|
|
832
|
+
if (toolChoice === undefined) return undefined;
|
|
833
|
+
if (toolChoice === 'auto' || toolChoice === 'none' || toolChoice === 'required') return undefined;
|
|
834
|
+
if (!isRecord(toolChoice)) return "'tool_choice' must be auto, none, required, or a function choice object";
|
|
835
|
+
if (toolChoice.type !== 'function' || typeof toolChoice.name !== 'string' || !toolChoice.name) {
|
|
836
|
+
return "'tool_choice' object must have type='function' and a non-empty name";
|
|
837
|
+
}
|
|
838
|
+
return undefined;
|
|
839
|
+
}
|
|
840
|
+
export function findResponseInputProblem(input: unknown): string | undefined {
|
|
841
|
+
if (typeof input === 'string') return input.length > 0 ? undefined : "'input' string must not be empty";
|
|
842
|
+
if (!Array.isArray(input)) return "'input' must be a string or an array";
|
|
843
|
+
if (input.length === 0) return "'input' array must not be empty";
|
|
844
|
+
for (const [index, item] of input.entries()) {
|
|
845
|
+
if (!isRecord(item)) return `input[${index}] must be an object`;
|
|
846
|
+
if (item.type === 'function_call') {
|
|
847
|
+
if (
|
|
848
|
+
typeof item.call_id !== 'string' ||
|
|
849
|
+
!item.call_id ||
|
|
850
|
+
typeof item.name !== 'string' ||
|
|
851
|
+
!item.name ||
|
|
852
|
+
typeof item.arguments !== 'string'
|
|
853
|
+
) {
|
|
854
|
+
return `input[${index}] function_call requires non-empty call_id and name strings plus string arguments`;
|
|
855
|
+
}
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
if (item.type === 'reasoning') {
|
|
859
|
+
if (typeof item.id !== 'string' || !item.id || !Array.isArray(item.summary)) {
|
|
860
|
+
return `input[${index}] reasoning requires a non-empty id and summary array`;
|
|
861
|
+
}
|
|
862
|
+
const invalidSummary = item.summary.some(
|
|
863
|
+
(part) => !isRecord(part) || part.type !== 'summary_text' || typeof part.text !== 'string',
|
|
864
|
+
);
|
|
865
|
+
if (invalidSummary) return `input[${index}].reasoning summary items must be summary_text objects`;
|
|
866
|
+
if (
|
|
867
|
+
item.content !== undefined &&
|
|
868
|
+
(!Array.isArray(item.content) ||
|
|
869
|
+
item.content.some(
|
|
870
|
+
(part) => !isRecord(part) || part.type !== 'reasoning_text' || typeof part.text !== 'string',
|
|
871
|
+
))
|
|
872
|
+
) {
|
|
873
|
+
return `input[${index}].reasoning content items must be reasoning_text objects`;
|
|
874
|
+
}
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
if (item.type === 'function_call_output') {
|
|
878
|
+
if (typeof item.call_id !== 'string' || !item.call_id || item.output === undefined) {
|
|
879
|
+
return `input[${index}] function_call_output requires call_id and output`;
|
|
880
|
+
}
|
|
881
|
+
if (
|
|
882
|
+
typeof item.output !== 'string' &&
|
|
883
|
+
(!Array.isArray(item.output) ||
|
|
884
|
+
item.output.some(
|
|
885
|
+
(part) =>
|
|
886
|
+
!isRecord(part) ||
|
|
887
|
+
(part.type !== 'input_text' && part.type !== 'input_image' && part.type !== 'input_file'),
|
|
888
|
+
))
|
|
889
|
+
) {
|
|
890
|
+
return `input[${index}] function_call_output output must be a string or input content array`;
|
|
891
|
+
}
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
if (item.type === 'message' || typeof item.role === 'string') {
|
|
895
|
+
if (item.role !== 'user' && item.role !== 'assistant' && item.role !== 'system' && item.role !== 'developer') {
|
|
896
|
+
return `input[${index}].role is not supported`;
|
|
897
|
+
}
|
|
898
|
+
if (typeof item.content !== 'string' && !Array.isArray(item.content)) {
|
|
899
|
+
return `input[${index}].content must be a string or an array`;
|
|
900
|
+
}
|
|
901
|
+
if (
|
|
902
|
+
item.phase !== undefined &&
|
|
903
|
+
item.phase !== null &&
|
|
904
|
+
item.phase !== 'commentary' &&
|
|
905
|
+
item.phase !== 'final_answer'
|
|
906
|
+
) {
|
|
907
|
+
return `input[${index}].phase must be commentary, final_answer, or null`;
|
|
908
|
+
}
|
|
909
|
+
if (Array.isArray(item.content)) {
|
|
910
|
+
for (const [contentIndex, block] of item.content.entries()) {
|
|
911
|
+
if (!isRecord(block) || typeof block.type !== 'string') {
|
|
912
|
+
return `input[${index}].content[${contentIndex}] must be a typed object`;
|
|
913
|
+
}
|
|
914
|
+
if ((block.type === 'input_text' || block.type === 'output_text') && typeof block.text !== 'string') {
|
|
915
|
+
return `input[${index}].content[${contentIndex}].text must be a string`;
|
|
916
|
+
}
|
|
917
|
+
if (
|
|
918
|
+
block.type === 'input_image' &&
|
|
919
|
+
typeof block.image_url !== 'string' &&
|
|
920
|
+
typeof block.file_id !== 'string'
|
|
921
|
+
) {
|
|
922
|
+
return `input[${index}].content[${contentIndex}] input_image requires image_url or file_id`;
|
|
923
|
+
}
|
|
924
|
+
if (
|
|
925
|
+
block.type === 'input_file' &&
|
|
926
|
+
typeof block.file_url !== 'string' &&
|
|
927
|
+
typeof block.file_data !== 'string' &&
|
|
928
|
+
typeof block.file_id !== 'string'
|
|
929
|
+
) {
|
|
930
|
+
return `input[${index}].content[${contentIndex}] input_file requires file_url, file_data, or file_id`;
|
|
931
|
+
}
|
|
932
|
+
if (
|
|
933
|
+
block.type !== 'input_text' &&
|
|
934
|
+
block.type !== 'output_text' &&
|
|
935
|
+
block.type !== 'input_image' &&
|
|
936
|
+
block.type !== 'input_file'
|
|
937
|
+
) {
|
|
938
|
+
return `input[${index}].content[${contentIndex}] type '${block.type}' is not supported`;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
return `input[${index}] type '${String(item.type ?? 'unknown')}' is not supported`;
|
|
945
|
+
}
|
|
946
|
+
return undefined;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
export function findUnsupportedResponseParameter(body: Record<string, unknown>): string | undefined {
|
|
950
|
+
const unsupported = [
|
|
951
|
+
'background',
|
|
952
|
+
'context_management',
|
|
953
|
+
'conversation',
|
|
954
|
+
'include',
|
|
955
|
+
'max_tool_calls',
|
|
956
|
+
'prompt',
|
|
957
|
+
'stream_options',
|
|
958
|
+
'top_logprobs',
|
|
959
|
+
];
|
|
960
|
+
return unsupported.find((name) => body[name] !== undefined);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function responsesTextFormatToChatFormat(format: unknown): unknown {
|
|
964
|
+
if (!isRecord(format) || format.type !== 'json_schema') return format;
|
|
965
|
+
return {
|
|
966
|
+
type: 'json_schema',
|
|
967
|
+
json_schema: {
|
|
968
|
+
name: format.name,
|
|
969
|
+
schema: format.schema,
|
|
970
|
+
...(typeof format.description === 'string' ? { description: format.description } : {}),
|
|
971
|
+
...(typeof format.strict === 'boolean' ? { strict: format.strict } : {}),
|
|
972
|
+
},
|
|
973
|
+
};
|
|
974
|
+
}
|