@aigne/openai 0.8.2 → 0.9.0

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,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.0](https://github.com/AIGNE-io/aigne-framework/compare/openai-v0.8.2...openai-v0.9.0) (2025-07-10)
4
+
5
+
6
+ ### Features
7
+
8
+ * **model:** reduce unnecessary LLM requests for structured output ([#241](https://github.com/AIGNE-io/aigne-framework/issues/241)) ([e28813c](https://github.com/AIGNE-io/aigne-framework/commit/e28813c021ed35c0251e198e2e007e2d746ab3d8))
9
+
10
+
11
+ ### Dependencies
12
+
13
+ * The following workspace dependencies were updated
14
+ * dependencies
15
+ * @aigne/core bumped to 1.33.0
16
+ * devDependencies
17
+ * @aigne/test-utils bumped to 0.5.5
18
+
3
19
  ## [0.8.2](https://github.com/AIGNE-io/aigne-framework/compare/openai-v0.8.1...openai-v0.8.2) (2025-07-09)
4
20
 
5
21
 
package/README.md CHANGED
@@ -6,8 +6,6 @@
6
6
  [![NPM Version](https://img.shields.io/npm/v/@aigne/openai)](https://www.npmjs.com/package/@aigne/openai)
7
7
  [![Elastic-2.0 licensed](https://img.shields.io/npm/l/@aigne/openai)](https://github.com/AIGNE-io/aigne-framework/blob/main/LICENSE.md)
8
8
 
9
- **English** | [中文](README.zh.md)
10
-
11
9
  AIGNE OpenAI SDK for integrating with OpenAI's GPT models and API services within the [AIGNE Framework](https://github.com/AIGNE-io/aigne-framework).
12
10
 
13
11
  ## Introduction
@@ -139,6 +139,7 @@ export declare class OpenAIChatModel extends ChatModel {
139
139
  * @returns The generated response
140
140
  */
141
141
  process(input: ChatModelInput): PromiseOrValue<AgentProcessResult<ChatModelOutput>>;
142
+ private ajv;
142
143
  private _process;
143
144
  private getParallelToolCalls;
144
145
  protected getRunMessages(input: ChatModelInput): Promise<ChatCompletionMessageParam[]>;
@@ -8,11 +8,13 @@ exports.contentsFromInputMessages = contentsFromInputMessages;
8
8
  exports.toolsFromInputTools = toolsFromInputTools;
9
9
  exports.jsonSchemaToOpenAIJsonSchema = jsonSchemaToOpenAIJsonSchema;
10
10
  const core_1 = require("@aigne/core");
11
- const json_schema_js_1 = require("@aigne/core/utils/json-schema.js");
11
+ const logger_js_1 = require("@aigne/core/utils/logger.js");
12
12
  const model_utils_js_1 = require("@aigne/core/utils/model-utils.js");
13
13
  const prompts_js_1 = require("@aigne/core/utils/prompts.js");
14
14
  const stream_utils_js_1 = require("@aigne/core/utils/stream-utils.js");
15
15
  const type_utils_js_1 = require("@aigne/core/utils/type-utils.js");
16
+ const ajv_1 = require("ajv");
17
+ const jaison_1 = __importDefault(require("jaison"));
16
18
  const nanoid_1 = require("nanoid");
17
19
  const openai_1 = __importDefault(require("openai"));
18
20
  const zod_1 = require("zod");
@@ -102,6 +104,7 @@ class OpenAIChatModel extends core_1.ChatModel {
102
104
  process(input) {
103
105
  return this._process(input);
104
106
  }
107
+ ajv = new ajv_1.Ajv();
105
108
  async _process(input) {
106
109
  const messages = await this.getRunMessages(input);
107
110
  const body = {
@@ -118,6 +121,11 @@ class OpenAIChatModel extends core_1.ChatModel {
118
121
  },
119
122
  stream: true,
120
123
  };
124
+ // For models that do not support tools use with JSON schema in same request,
125
+ // so we need to handle the case where tools are not used and responseFormat is json
126
+ if (!input.tools?.length && input.responseFormat?.type === "json_schema") {
127
+ return await this.requestStructuredOutput(body, input.responseFormat);
128
+ }
121
129
  const { jsonMode, responseFormat } = await this.getRunResponseFormat(input);
122
130
  const stream = (await this.client.chat.completions.create({
123
131
  ...body,
@@ -132,14 +140,18 @@ class OpenAIChatModel extends core_1.ChatModel {
132
140
  return await this.extractResultFromStream(stream, false, true);
133
141
  }
134
142
  const result = await this.extractResultFromStream(stream, jsonMode);
135
- if (!this.supportsToolsUseWithJsonSchema &&
136
- !result.toolCalls?.length &&
137
- input.responseFormat?.type === "json_schema" &&
138
- result.text) {
139
- const output = await this.requestStructuredOutput(body, input.responseFormat);
140
- return { ...output, usage: (0, model_utils_js_1.mergeUsage)(result.usage, output.usage) };
143
+ // Just return the result if it has tool calls
144
+ if (result.toolCalls?.length || result.json)
145
+ return result;
146
+ // Try to parse the text response as JSON
147
+ // If it matches the json_schema, return it as json
148
+ const json = safeParseJSON(result.text || "");
149
+ if (this.ajv.validate(input.responseFormat.jsonSchema.schema, json)) {
150
+ return { ...result, json, text: undefined };
141
151
  }
142
- return result;
152
+ logger_js_1.logger.warn(`${this.name}: Text response does not match JSON schema, trying to use tool to extract json `, { text: result.text });
153
+ const output = await this.requestStructuredOutput(body, input.responseFormat);
154
+ return { ...output, usage: (0, model_utils_js_1.mergeUsage)(result.usage, output.usage) };
143
155
  }
144
156
  getParallelToolCalls(input) {
145
157
  if (!this.supportsParallelToolCalls)
@@ -150,15 +162,14 @@ class OpenAIChatModel extends core_1.ChatModel {
150
162
  }
151
163
  async getRunMessages(input) {
152
164
  const messages = await contentsFromInputMessages(input.messages);
153
- if (!this.supportsToolsUseWithJsonSchema && input.tools?.length)
154
- return messages;
155
- if (this.supportsNativeStructuredOutputs)
156
- return messages;
157
165
  if (input.responseFormat?.type === "json_schema") {
158
- messages.unshift({
159
- role: "system",
160
- content: (0, prompts_js_1.getJsonOutputPrompt)(input.responseFormat.jsonSchema.schema),
161
- });
166
+ if (!this.supportsNativeStructuredOutputs ||
167
+ (!this.supportsToolsUseWithJsonSchema && input.tools?.length)) {
168
+ messages.unshift({
169
+ role: "system",
170
+ content: (0, prompts_js_1.getJsonOutputPrompt)(input.responseFormat.jsonSchema.schema),
171
+ });
172
+ }
162
173
  }
163
174
  return messages;
164
175
  }
@@ -269,7 +280,7 @@ class OpenAIChatModel extends core_1.ChatModel {
269
280
  controller.enqueue({
270
281
  delta: {
271
282
  json: {
272
- json: (0, json_schema_js_1.parseJSON)(text),
283
+ json: safeParseJSON(text),
273
284
  },
274
285
  },
275
286
  });
@@ -280,7 +291,7 @@ class OpenAIChatModel extends core_1.ChatModel {
280
291
  json: {
281
292
  toolCalls: toolCalls.map(({ args, ...c }) => ({
282
293
  ...c,
283
- function: { ...c.function, arguments: (0, json_schema_js_1.parseJSON)(args) },
294
+ function: { ...c.function, arguments: safeParseJSON(args) },
284
295
  })),
285
296
  },
286
297
  },
@@ -408,7 +419,7 @@ function handleCompleteToolCall(toolCalls, call) {
408
419
  type: "function",
409
420
  function: {
410
421
  name: call.function?.name || "",
411
- arguments: (0, json_schema_js_1.parseJSON)(call.function?.arguments || "{}"),
422
+ arguments: safeParseJSON(call.function?.arguments || "{}"),
412
423
  },
413
424
  args: call.function?.arguments || "",
414
425
  });
@@ -422,3 +433,13 @@ class CustomOpenAI extends openai_1.default {
422
433
  return super.makeStatusError(status, error, message, headers);
423
434
  }
424
435
  }
436
+ function safeParseJSON(text) {
437
+ if (!text)
438
+ return null;
439
+ try {
440
+ return (0, jaison_1.default)(text);
441
+ }
442
+ catch {
443
+ return null;
444
+ }
445
+ }
@@ -139,6 +139,7 @@ export declare class OpenAIChatModel extends ChatModel {
139
139
  * @returns The generated response
140
140
  */
141
141
  process(input: ChatModelInput): PromiseOrValue<AgentProcessResult<ChatModelOutput>>;
142
+ private ajv;
142
143
  private _process;
143
144
  private getParallelToolCalls;
144
145
  protected getRunMessages(input: ChatModelInput): Promise<ChatCompletionMessageParam[]>;
@@ -139,6 +139,7 @@ export declare class OpenAIChatModel extends ChatModel {
139
139
  * @returns The generated response
140
140
  */
141
141
  process(input: ChatModelInput): PromiseOrValue<AgentProcessResult<ChatModelOutput>>;
142
+ private ajv;
142
143
  private _process;
143
144
  private getParallelToolCalls;
144
145
  protected getRunMessages(input: ChatModelInput): Promise<ChatCompletionMessageParam[]>;
@@ -1,9 +1,11 @@
1
1
  import { ChatModel, } from "@aigne/core";
2
- import { parseJSON } from "@aigne/core/utils/json-schema.js";
2
+ import { logger } from "@aigne/core/utils/logger.js";
3
3
  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 { Ajv } from "ajv";
8
+ import jaison from "jaison";
7
9
  import { nanoid } from "nanoid";
8
10
  import OpenAI from "openai";
9
11
  import { z } from "zod";
@@ -93,6 +95,7 @@ export class OpenAIChatModel extends ChatModel {
93
95
  process(input) {
94
96
  return this._process(input);
95
97
  }
98
+ ajv = new Ajv();
96
99
  async _process(input) {
97
100
  const messages = await this.getRunMessages(input);
98
101
  const body = {
@@ -109,6 +112,11 @@ export class OpenAIChatModel extends ChatModel {
109
112
  },
110
113
  stream: true,
111
114
  };
115
+ // For models that do not support tools use with JSON schema in same request,
116
+ // so we need to handle the case where tools are not used and responseFormat is json
117
+ if (!input.tools?.length && input.responseFormat?.type === "json_schema") {
118
+ return await this.requestStructuredOutput(body, input.responseFormat);
119
+ }
112
120
  const { jsonMode, responseFormat } = await this.getRunResponseFormat(input);
113
121
  const stream = (await this.client.chat.completions.create({
114
122
  ...body,
@@ -123,14 +131,18 @@ export class OpenAIChatModel extends ChatModel {
123
131
  return await this.extractResultFromStream(stream, false, true);
124
132
  }
125
133
  const result = await this.extractResultFromStream(stream, jsonMode);
126
- if (!this.supportsToolsUseWithJsonSchema &&
127
- !result.toolCalls?.length &&
128
- input.responseFormat?.type === "json_schema" &&
129
- result.text) {
130
- const output = await this.requestStructuredOutput(body, input.responseFormat);
131
- return { ...output, usage: mergeUsage(result.usage, output.usage) };
134
+ // Just return the result if it has tool calls
135
+ if (result.toolCalls?.length || result.json)
136
+ return result;
137
+ // Try to parse the text response as JSON
138
+ // If it matches the json_schema, return it as json
139
+ const json = safeParseJSON(result.text || "");
140
+ if (this.ajv.validate(input.responseFormat.jsonSchema.schema, json)) {
141
+ return { ...result, json, text: undefined };
132
142
  }
133
- return result;
143
+ logger.warn(`${this.name}: Text response does not match JSON schema, trying to use tool to extract json `, { text: result.text });
144
+ const output = await this.requestStructuredOutput(body, input.responseFormat);
145
+ return { ...output, usage: mergeUsage(result.usage, output.usage) };
134
146
  }
135
147
  getParallelToolCalls(input) {
136
148
  if (!this.supportsParallelToolCalls)
@@ -141,15 +153,14 @@ export class OpenAIChatModel extends ChatModel {
141
153
  }
142
154
  async getRunMessages(input) {
143
155
  const messages = await contentsFromInputMessages(input.messages);
144
- if (!this.supportsToolsUseWithJsonSchema && input.tools?.length)
145
- return messages;
146
- if (this.supportsNativeStructuredOutputs)
147
- return messages;
148
156
  if (input.responseFormat?.type === "json_schema") {
149
- messages.unshift({
150
- role: "system",
151
- content: getJsonOutputPrompt(input.responseFormat.jsonSchema.schema),
152
- });
157
+ if (!this.supportsNativeStructuredOutputs ||
158
+ (!this.supportsToolsUseWithJsonSchema && input.tools?.length)) {
159
+ messages.unshift({
160
+ role: "system",
161
+ content: getJsonOutputPrompt(input.responseFormat.jsonSchema.schema),
162
+ });
163
+ }
153
164
  }
154
165
  return messages;
155
166
  }
@@ -260,7 +271,7 @@ export class OpenAIChatModel extends ChatModel {
260
271
  controller.enqueue({
261
272
  delta: {
262
273
  json: {
263
- json: parseJSON(text),
274
+ json: safeParseJSON(text),
264
275
  },
265
276
  },
266
277
  });
@@ -271,7 +282,7 @@ export class OpenAIChatModel extends ChatModel {
271
282
  json: {
272
283
  toolCalls: toolCalls.map(({ args, ...c }) => ({
273
284
  ...c,
274
- function: { ...c.function, arguments: parseJSON(args) },
285
+ function: { ...c.function, arguments: safeParseJSON(args) },
275
286
  })),
276
287
  },
277
288
  },
@@ -398,7 +409,7 @@ function handleCompleteToolCall(toolCalls, call) {
398
409
  type: "function",
399
410
  function: {
400
411
  name: call.function?.name || "",
401
- arguments: parseJSON(call.function?.arguments || "{}"),
412
+ arguments: safeParseJSON(call.function?.arguments || "{}"),
402
413
  },
403
414
  args: call.function?.arguments || "",
404
415
  });
@@ -412,3 +423,13 @@ class CustomOpenAI extends OpenAI {
412
423
  return super.makeStatusError(status, error, message, headers);
413
424
  }
414
425
  }
426
+ function safeParseJSON(text) {
427
+ if (!text)
428
+ return null;
429
+ try {
430
+ return jaison(text);
431
+ }
432
+ catch {
433
+ return null;
434
+ }
435
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aigne/openai",
3
- "version": "0.8.2",
3
+ "version": "0.9.0",
4
4
  "description": "AIGNE OpenAI SDK for integrating with OpenAI's GPT models and API services",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -32,10 +32,12 @@
32
32
  }
33
33
  },
34
34
  "dependencies": {
35
+ "ajv": "^8.17.1",
36
+ "jaison": "^2.0.2",
35
37
  "nanoid": "^5.1.5",
36
38
  "openai": "^5.8.2",
37
39
  "zod": "^3.25.67",
38
- "@aigne/core": "^1.32.2"
40
+ "@aigne/core": "^1.33.0"
39
41
  },
40
42
  "devDependencies": {
41
43
  "@types/bun": "^1.2.17",
@@ -43,7 +45,7 @@
43
45
  "npm-run-all": "^4.1.5",
44
46
  "rimraf": "^6.0.1",
45
47
  "typescript": "^5.8.3",
46
- "@aigne/test-utils": "^0.5.4"
48
+ "@aigne/test-utils": "^0.5.5"
47
49
  },
48
50
  "scripts": {
49
51
  "lint": "tsc --noEmit",
package/README.zh.md DELETED
@@ -1,114 +0,0 @@
1
- # @aigne/openai
2
-
3
- [![GitHub star chart](https://img.shields.io/github/stars/AIGNE-io/aigne-framework?style=flat-square)](https://star-history.com/#AIGNE-io/aigne-framework)
4
- [![Open Issues](https://img.shields.io/github/issues-raw/AIGNE-io/aigne-framework?style=flat-square)](https://github.com/AIGNE-io/aigne-framework/issues)
5
- [![codecov](https://codecov.io/gh/AIGNE-io/aigne-framework/graph/badge.svg?token=DO07834RQL)](https://codecov.io/gh/AIGNE-io/aigne-framework)
6
- [![NPM Version](https://img.shields.io/npm/v/@aigne/openai)](https://www.npmjs.com/package/@aigne/openai)
7
- [![Elastic-2.0 licensed](https://img.shields.io/npm/l/@aigne/openai)](https://github.com/AIGNE-io/aigne-framework/blob/main/LICENSE.md)
8
-
9
- [English](README.md) | **中文**
10
-
11
- AIGNE OpenAI SDK,用于在 [AIGNE 框架](https://github.com/AIGNE-io/aigne-framework) 中集成 OpenAI 的 GPT 模型和 API 服务。
12
-
13
- ## 简介
14
-
15
- `@aigne/openai` 提供了 AIGNE 框架与 OpenAI 强大的语言模型和 API 之间的无缝集成。该包使开发者能够在 AIGNE 应用程序中轻松利用 OpenAI 的 GPT 模型,同时提供框架内一致的接口,充分发挥 OpenAI 先进的 AI 能力。
16
-
17
- ## 特性
18
-
19
- * **OpenAI API 集成**:使用官方 SDK 直接连接到 OpenAI 的 API 服务
20
- * **聊天完成**:支持 OpenAI 的聊天完成 API 和所有可用模型
21
- * **函数调用**:内置支持 OpenAI 的函数调用功能
22
- * **流式响应**:支持流式响应,提供更高响应性的应用程序体验
23
- * **类型安全**:为所有 API 和模型提供全面的 TypeScript 类型定义
24
- * **一致接口**:兼容 AIGNE 框架的模型接口
25
- * **错误处理**:健壮的错误处理和重试机制
26
- * **完整配置**:丰富的配置选项用于微调行为
27
-
28
- ## 安装
29
-
30
- ### 使用 npm
31
-
32
- ```bash
33
- npm install @aigne/openai @aigne/core
34
- ```
35
-
36
- ### 使用 yarn
37
-
38
- ```bash
39
- yarn add @aigne/openai @aigne/core
40
- ```
41
-
42
- ### 使用 pnpm
43
-
44
- ```bash
45
- pnpm add @aigne/openai @aigne/core
46
- ```
47
-
48
- ## 基本用法
49
-
50
- ```typescript file="test/openai-chat-model.test.ts" region="example-openai-chat-model"
51
- import { OpenAIChatModel } from "@aigne/openai";
52
-
53
- const model = new OpenAIChatModel({
54
- // Provide API key directly or use environment variable OPENAI_API_KEY
55
- apiKey: "your-api-key", // Optional if set in env variables
56
- model: "gpt-4o", // Defaults to "gpt-4o-mini" if not specified
57
- modelOptions: {
58
- temperature: 0.7,
59
- },
60
- });
61
-
62
- const result = await model.invoke({
63
- messages: [{ role: "user", content: "Hello, who are you?" }],
64
- });
65
-
66
- console.log(result);
67
- /* Output:
68
- {
69
- text: "Hello! How can I assist you today?",
70
- model: "gpt-4o",
71
- usage: {
72
- inputTokens: 10,
73
- outputTokens: 9
74
- }
75
- }
76
- */
77
- ```
78
-
79
- ## 流式响应
80
-
81
- ```typescript file="test/openai-chat-model.test.ts" region="example-openai-chat-model-stream"
82
- import { isAgentResponseDelta } from "@aigne/core";
83
- import { OpenAIChatModel } from "@aigne/openai";
84
-
85
- const model = new OpenAIChatModel({
86
- apiKey: "your-api-key",
87
- model: "gpt-4o",
88
- });
89
-
90
- const stream = await model.invoke(
91
- {
92
- messages: [{ role: "user", content: "Hello, who are you?" }],
93
- },
94
- { streaming: true },
95
- );
96
-
97
- let fullText = "";
98
- const json = {};
99
-
100
- for await (const chunk of stream) {
101
- if (isAgentResponseDelta(chunk)) {
102
- const text = chunk.delta.text?.text;
103
- if (text) fullText += text;
104
- if (chunk.delta.json) Object.assign(json, chunk.delta.json);
105
- }
106
- }
107
-
108
- console.log(fullText); // Output: "Hello! How can I assist you today?"
109
- console.log(json); // { model: "gpt-4o", usage: { inputTokens: 10, outputTokens: 9 } }
110
- ```
111
-
112
- ## 许可证
113
-
114
- Elastic-2.0