@aigne/openai 0.14.1 → 0.14.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.14.3](https://github.com/AIGNE-io/aigne-framework/compare/openai-v0.14.2...openai-v0.14.3) (2025-09-08)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * support optional field sturectured output for gemini ([#468](https://github.com/AIGNE-io/aigne-framework/issues/468)) ([70c6279](https://github.com/AIGNE-io/aigne-framework/commit/70c62795039a2862e3333f26707329489bf938de))
9
+
10
+
11
+ ### Dependencies
12
+
13
+ * The following workspace dependencies were updated
14
+ * dependencies
15
+ * @aigne/core bumped to 1.58.3
16
+ * devDependencies
17
+ * @aigne/test-utils bumped to 0.5.47
18
+
19
+ ## [0.14.2](https://github.com/AIGNE-io/aigne-framework/compare/openai-v0.14.1...openai-v0.14.2) (2025-09-05)
20
+
21
+
22
+ ### Bug Fixes
23
+
24
+ * **model:** transform local file to base64 before request llm ([#462](https://github.com/AIGNE-io/aigne-framework/issues/462)) ([58ef5d7](https://github.com/AIGNE-io/aigne-framework/commit/58ef5d77046c49f3c4eed15b7f0cc283cbbcd74a))
25
+
26
+
27
+ ### Dependencies
28
+
29
+ * The following workspace dependencies were updated
30
+ * dependencies
31
+ * @aigne/core bumped to 1.58.2
32
+ * devDependencies
33
+ * @aigne/test-utils bumped to 0.5.46
34
+
3
35
  ## [0.14.1](https://github.com/AIGNE-io/aigne-framework/compare/openai-v0.14.0...openai-v0.14.1) (2025-09-05)
4
36
 
5
37
 
@@ -151,6 +151,13 @@ export declare class OpenAIChatModel extends ChatModel {
151
151
  private getRunResponseFormat;
152
152
  private requestStructuredOutput;
153
153
  private extractResultFromStream;
154
+ /**
155
+ * Controls how optional fields are handled in JSON schema conversion
156
+ * - "anyOf": All fields are required but can be null (default)
157
+ * - "optional": Fields marked as optional in schema remain optional
158
+ */
159
+ protected optionalFieldMode?: "anyOf" | "optional";
160
+ protected jsonSchemaToOpenAIJsonSchema(schema: Record<string, unknown>): Record<string, unknown>;
154
161
  }
155
162
  /**
156
163
  * @hidden
@@ -162,7 +169,3 @@ export declare function contentsFromInputMessages(messages: ChatModelInputMessag
162
169
  export declare function toolsFromInputTools(tools?: ChatModelInputTool[], options?: {
163
170
  addTypeToEmptyParameters?: boolean;
164
171
  }): ChatCompletionTool[] | undefined;
165
- /**
166
- * @hidden
167
- */
168
- export declare function jsonSchemaToOpenAIJsonSchema(schema: Record<string, unknown>): Record<string, unknown>;
@@ -3,14 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.OpenAIChatModel = exports.openAIChatModelOptionsSchema = void 0;
4
4
  exports.contentsFromInputMessages = contentsFromInputMessages;
5
5
  exports.toolsFromInputTools = toolsFromInputTools;
6
- exports.jsonSchemaToOpenAIJsonSchema = jsonSchemaToOpenAIJsonSchema;
7
6
  const core_1 = require("@aigne/core");
8
7
  const logger_js_1 = require("@aigne/core/utils/logger.js");
9
8
  const model_utils_js_1 = require("@aigne/core/utils/model-utils.js");
10
9
  const prompts_js_1 = require("@aigne/core/utils/prompts.js");
11
10
  const stream_utils_js_1 = require("@aigne/core/utils/stream-utils.js");
12
11
  const type_utils_js_1 = require("@aigne/core/utils/type-utils.js");
13
- const index_js_1 = require("@aigne/platform-helpers/nodejs/index.js");
14
12
  const ajv_1 = require("ajv");
15
13
  const uuid_1 = require("uuid");
16
14
  const zod_1 = require("zod");
@@ -197,7 +195,7 @@ class OpenAIChatModel extends core_1.ChatModel {
197
195
  type: "json_schema",
198
196
  json_schema: {
199
197
  ...input.responseFormat.jsonSchema,
200
- schema: jsonSchemaToOpenAIJsonSchema(input.responseFormat.jsonSchema.schema),
198
+ schema: this.jsonSchemaToOpenAIJsonSchema(input.responseFormat.jsonSchema.schema),
201
199
  },
202
200
  },
203
201
  };
@@ -313,6 +311,40 @@ class OpenAIChatModel extends core_1.ChatModel {
313
311
  });
314
312
  return streaming ? result : await (0, stream_utils_js_1.agentResponseStreamToObject)(result);
315
313
  }
314
+ /**
315
+ * Controls how optional fields are handled in JSON schema conversion
316
+ * - "anyOf": All fields are required but can be null (default)
317
+ * - "optional": Fields marked as optional in schema remain optional
318
+ */
319
+ optionalFieldMode = "anyOf";
320
+ jsonSchemaToOpenAIJsonSchema(schema) {
321
+ if (schema?.type === "object") {
322
+ const s = schema;
323
+ const required = this.optionalFieldMode === "anyOf" ? Object.keys(s.properties) : s.required;
324
+ return {
325
+ ...schema,
326
+ properties: Object.fromEntries(Object.entries(s.properties).map(([key, value]) => {
327
+ const valueSchema = this.jsonSchemaToOpenAIJsonSchema(value);
328
+ // NOTE: All fields must be required https://platform.openai.com/docs/guides/structured-outputs/all-fields-must-be-required
329
+ return [
330
+ key,
331
+ this.optionalFieldMode === "optional" || s.required?.includes(key)
332
+ ? valueSchema
333
+ : { anyOf: [valueSchema, { type: ["null"] }] },
334
+ ];
335
+ })),
336
+ required,
337
+ };
338
+ }
339
+ if (schema?.type === "array") {
340
+ const { items } = schema;
341
+ return {
342
+ ...schema,
343
+ items: this.jsonSchemaToOpenAIJsonSchema(items),
344
+ };
345
+ }
346
+ return schema;
347
+ }
316
348
  }
317
349
  exports.OpenAIChatModel = OpenAIChatModel;
318
350
  // Create role mapper for OpenAI (uses standard mapping)
@@ -338,13 +370,7 @@ async function contentsFromInputMessages(messages) {
338
370
  file: { file_data: c.data, filename: c.filename },
339
371
  };
340
372
  case "local": {
341
- return {
342
- type: "file",
343
- file: {
344
- file_data: await index_js_1.nodejs.fs.readFile(c.path, "base64"),
345
- filename: c.filename,
346
- },
347
- };
373
+ throw new Error(`Unsupported local file: ${c.path}, it should be converted to base64 at ChatModel`);
348
374
  }
349
375
  }
350
376
  }))).filter(type_utils_js_1.isNonNullable),
@@ -380,34 +406,6 @@ function toolsFromInputTools(tools, options) {
380
406
  })
381
407
  : undefined;
382
408
  }
383
- /**
384
- * @hidden
385
- */
386
- function jsonSchemaToOpenAIJsonSchema(schema) {
387
- if (schema?.type === "object") {
388
- const { required, properties } = schema;
389
- return {
390
- ...schema,
391
- properties: Object.fromEntries(Object.entries(properties).map(([key, value]) => {
392
- const valueSchema = jsonSchemaToOpenAIJsonSchema(value);
393
- // NOTE: All fields must be required https://platform.openai.com/docs/guides/structured-outputs/all-fields-must-be-required
394
- return [
395
- key,
396
- required?.includes(key) ? valueSchema : { anyOf: [valueSchema, { type: ["null"] }] },
397
- ];
398
- })),
399
- required: Object.keys(properties),
400
- };
401
- }
402
- if (schema?.type === "array") {
403
- const { items } = schema;
404
- return {
405
- ...schema,
406
- items: jsonSchemaToOpenAIJsonSchema(items),
407
- };
408
- }
409
- return schema;
410
- }
411
409
  function handleToolCallDelta(toolCalls, call) {
412
410
  toolCalls[call.index] ??= {
413
411
  id: call.id || (0, uuid_1.v7)(),
@@ -151,6 +151,13 @@ export declare class OpenAIChatModel extends ChatModel {
151
151
  private getRunResponseFormat;
152
152
  private requestStructuredOutput;
153
153
  private extractResultFromStream;
154
+ /**
155
+ * Controls how optional fields are handled in JSON schema conversion
156
+ * - "anyOf": All fields are required but can be null (default)
157
+ * - "optional": Fields marked as optional in schema remain optional
158
+ */
159
+ protected optionalFieldMode?: "anyOf" | "optional";
160
+ protected jsonSchemaToOpenAIJsonSchema(schema: Record<string, unknown>): Record<string, unknown>;
154
161
  }
155
162
  /**
156
163
  * @hidden
@@ -162,7 +169,3 @@ export declare function contentsFromInputMessages(messages: ChatModelInputMessag
162
169
  export declare function toolsFromInputTools(tools?: ChatModelInputTool[], options?: {
163
170
  addTypeToEmptyParameters?: boolean;
164
171
  }): ChatCompletionTool[] | undefined;
165
- /**
166
- * @hidden
167
- */
168
- export declare function jsonSchemaToOpenAIJsonSchema(schema: Record<string, unknown>): Record<string, unknown>;
@@ -151,6 +151,13 @@ export declare class OpenAIChatModel extends ChatModel {
151
151
  private getRunResponseFormat;
152
152
  private requestStructuredOutput;
153
153
  private extractResultFromStream;
154
+ /**
155
+ * Controls how optional fields are handled in JSON schema conversion
156
+ * - "anyOf": All fields are required but can be null (default)
157
+ * - "optional": Fields marked as optional in schema remain optional
158
+ */
159
+ protected optionalFieldMode?: "anyOf" | "optional";
160
+ protected jsonSchemaToOpenAIJsonSchema(schema: Record<string, unknown>): Record<string, unknown>;
154
161
  }
155
162
  /**
156
163
  * @hidden
@@ -162,7 +169,3 @@ export declare function contentsFromInputMessages(messages: ChatModelInputMessag
162
169
  export declare function toolsFromInputTools(tools?: ChatModelInputTool[], options?: {
163
170
  addTypeToEmptyParameters?: boolean;
164
171
  }): ChatCompletionTool[] | undefined;
165
- /**
166
- * @hidden
167
- */
168
- export declare function jsonSchemaToOpenAIJsonSchema(schema: Record<string, unknown>): Record<string, unknown>;
@@ -4,7 +4,6 @@ import { mergeUsage } from "@aigne/core/utils/model-utils.js";
4
4
  import { getJsonOutputPrompt } from "@aigne/core/utils/prompts.js";
5
5
  import { agentResponseStreamToObject } from "@aigne/core/utils/stream-utils.js";
6
6
  import { checkArguments, isNonNullable, } from "@aigne/core/utils/type-utils.js";
7
- import { nodejs } from "@aigne/platform-helpers/nodejs/index.js";
8
7
  import { Ajv } from "ajv";
9
8
  import { v7 } from "uuid";
10
9
  import { z } from "zod";
@@ -191,7 +190,7 @@ export class OpenAIChatModel extends ChatModel {
191
190
  type: "json_schema",
192
191
  json_schema: {
193
192
  ...input.responseFormat.jsonSchema,
194
- schema: jsonSchemaToOpenAIJsonSchema(input.responseFormat.jsonSchema.schema),
193
+ schema: this.jsonSchemaToOpenAIJsonSchema(input.responseFormat.jsonSchema.schema),
195
194
  },
196
195
  },
197
196
  };
@@ -307,6 +306,40 @@ export class OpenAIChatModel extends ChatModel {
307
306
  });
308
307
  return streaming ? result : await agentResponseStreamToObject(result);
309
308
  }
309
+ /**
310
+ * Controls how optional fields are handled in JSON schema conversion
311
+ * - "anyOf": All fields are required but can be null (default)
312
+ * - "optional": Fields marked as optional in schema remain optional
313
+ */
314
+ optionalFieldMode = "anyOf";
315
+ jsonSchemaToOpenAIJsonSchema(schema) {
316
+ if (schema?.type === "object") {
317
+ const s = schema;
318
+ const required = this.optionalFieldMode === "anyOf" ? Object.keys(s.properties) : s.required;
319
+ return {
320
+ ...schema,
321
+ properties: Object.fromEntries(Object.entries(s.properties).map(([key, value]) => {
322
+ const valueSchema = this.jsonSchemaToOpenAIJsonSchema(value);
323
+ // NOTE: All fields must be required https://platform.openai.com/docs/guides/structured-outputs/all-fields-must-be-required
324
+ return [
325
+ key,
326
+ this.optionalFieldMode === "optional" || s.required?.includes(key)
327
+ ? valueSchema
328
+ : { anyOf: [valueSchema, { type: ["null"] }] },
329
+ ];
330
+ })),
331
+ required,
332
+ };
333
+ }
334
+ if (schema?.type === "array") {
335
+ const { items } = schema;
336
+ return {
337
+ ...schema,
338
+ items: this.jsonSchemaToOpenAIJsonSchema(items),
339
+ };
340
+ }
341
+ return schema;
342
+ }
310
343
  }
311
344
  // Create role mapper for OpenAI (uses standard mapping)
312
345
  const mapRole = createRoleMapper(STANDARD_ROLE_MAP);
@@ -331,13 +364,7 @@ export async function contentsFromInputMessages(messages) {
331
364
  file: { file_data: c.data, filename: c.filename },
332
365
  };
333
366
  case "local": {
334
- return {
335
- type: "file",
336
- file: {
337
- file_data: await nodejs.fs.readFile(c.path, "base64"),
338
- filename: c.filename,
339
- },
340
- };
367
+ throw new Error(`Unsupported local file: ${c.path}, it should be converted to base64 at ChatModel`);
341
368
  }
342
369
  }
343
370
  }))).filter(isNonNullable),
@@ -373,34 +400,6 @@ export function toolsFromInputTools(tools, options) {
373
400
  })
374
401
  : undefined;
375
402
  }
376
- /**
377
- * @hidden
378
- */
379
- export function jsonSchemaToOpenAIJsonSchema(schema) {
380
- if (schema?.type === "object") {
381
- const { required, properties } = schema;
382
- return {
383
- ...schema,
384
- properties: Object.fromEntries(Object.entries(properties).map(([key, value]) => {
385
- const valueSchema = jsonSchemaToOpenAIJsonSchema(value);
386
- // NOTE: All fields must be required https://platform.openai.com/docs/guides/structured-outputs/all-fields-must-be-required
387
- return [
388
- key,
389
- required?.includes(key) ? valueSchema : { anyOf: [valueSchema, { type: ["null"] }] },
390
- ];
391
- })),
392
- required: Object.keys(properties),
393
- };
394
- }
395
- if (schema?.type === "array") {
396
- const { items } = schema;
397
- return {
398
- ...schema,
399
- items: jsonSchemaToOpenAIJsonSchema(items),
400
- };
401
- }
402
- return schema;
403
- }
404
403
  function handleToolCallDelta(toolCalls, call) {
405
404
  toolCalls[call.index] ??= {
406
405
  id: call.id || v7(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aigne/openai",
3
- "version": "0.14.1",
3
+ "version": "0.14.3",
4
4
  "description": "AIGNE OpenAI SDK for integrating with OpenAI's GPT models and API services",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -39,7 +39,7 @@
39
39
  "openai": "^5.8.3",
40
40
  "uuid": "^11.1.0",
41
41
  "zod": "^3.25.67",
42
- "@aigne/core": "^1.58.1",
42
+ "@aigne/core": "^1.58.3",
43
43
  "@aigne/platform-helpers": "^0.6.2"
44
44
  },
45
45
  "devDependencies": {
@@ -48,7 +48,7 @@
48
48
  "npm-run-all": "^4.1.5",
49
49
  "rimraf": "^6.0.1",
50
50
  "typescript": "^5.8.3",
51
- "@aigne/test-utils": "^0.5.45"
51
+ "@aigne/test-utils": "^0.5.47"
52
52
  },
53
53
  "scripts": {
54
54
  "lint": "tsc --noEmit",