@fevex/openai 0.1.0-alpha.1
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 +73 -0
- package/dist/index.d.mts +33 -0
- package/dist/index.mjs +742 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# @fevex/openai
|
|
2
|
+
|
|
3
|
+
Official OpenAI `ModelGateway` adapter for Fevex.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @fevex/core @fevex/openai
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { createOpenAI } from '@fevex/openai';
|
|
11
|
+
|
|
12
|
+
const model = createOpenAI({
|
|
13
|
+
apiKey: process.env.OPENAI_API_KEY!,
|
|
14
|
+
organization: process.env.OPENAI_ORG_ID,
|
|
15
|
+
project: process.env.OPENAI_PROJECT_ID,
|
|
16
|
+
schemaPolicy: 'strict',
|
|
17
|
+
})('gpt-5.6');
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The adapter uses native `fetch` and implements only the Fevex model contract.
|
|
21
|
+
Runtime behavior such as tool execution, validation, events and retries belongs
|
|
22
|
+
to `@fevex/core`. Returned gateways include `metadata: { provider: "openai",
|
|
23
|
+
model: modelId }` for traces, eval cost calculators and OpenTelemetry export.
|
|
24
|
+
|
|
25
|
+
## Text, tools and continuation
|
|
26
|
+
|
|
27
|
+
Without `outputSchema`, OpenAI text is returned as the exact string supplied by
|
|
28
|
+
the Responses API, even when it looks like JSON. With `outputSchema`, the
|
|
29
|
+
adapter requires valid JSON before Fevex performs local schema validation.
|
|
30
|
+
Refusals, incomplete responses and malformed function calls fail explicitly.
|
|
31
|
+
|
|
32
|
+
During tool loops the adapter stores the original response items, including
|
|
33
|
+
[reasoning items required by Responses API](https://platform.openai.com/docs/api-reference/responses-streaming/response/refusal?lang=python),
|
|
34
|
+
in opaque `providerState`. Fevex returns that state to the next model step
|
|
35
|
+
automatically. Direct `ModelGateway` consumers must do the same; the state is
|
|
36
|
+
run-local and must not be inspected. The adapter's `stateCodec` lets Fevex
|
|
37
|
+
persist it only inside a private durable checkpoint.
|
|
38
|
+
|
|
39
|
+
Core reasoning efforts are forwarded to `reasoning.effort`;
|
|
40
|
+
`provider-default` leaves provider options untouched. Model-specific effort
|
|
41
|
+
support remains an OpenAI capability and an unsupported value is reported by
|
|
42
|
+
the API. Provider-only settings can be supplied through
|
|
43
|
+
`modelOptions.reasoning`.
|
|
44
|
+
|
|
45
|
+
The adapter always uses the Responses streaming API. It emits only
|
|
46
|
+
`response.output_text.delta` as visible output and builds the terminal result
|
|
47
|
+
from `response.completed`; reasoning and partial function arguments remain
|
|
48
|
+
private.
|
|
49
|
+
|
|
50
|
+
Tool names and `schemaName` must match `[A-Za-z0-9_-]{1,64}`. Fevex-owned
|
|
51
|
+
fields such as model, input, tools, tool choice, schema format, parallel calls
|
|
52
|
+
and output caps override conflicting `modelOptions`. Background provider jobs
|
|
53
|
+
and built-in tools remain outside this gateway.
|
|
54
|
+
|
|
55
|
+
## Schema policy
|
|
56
|
+
|
|
57
|
+
`schemaPolicy` defaults to `"strict"`. Before sending a request, the adapter
|
|
58
|
+
checks tool inputs and final outputs against OpenAI's documented
|
|
59
|
+
[Structured Outputs subset](https://developers.openai.com/api/docs/guides/structured-outputs).
|
|
60
|
+
Every schema must have an object root; every object must require all its
|
|
61
|
+
properties and set `additionalProperties: false`. Unsupported keywords and
|
|
62
|
+
documented size limits throw `OpenAIError` with
|
|
63
|
+
`code: "PROVIDER_SCHEMA_UNSUPPORTED"` before `fetch` runs.
|
|
64
|
+
|
|
65
|
+
Set `schemaPolicy: "best-effort"` to send tools with `strict: false`. Object
|
|
66
|
+
outputs use JSON mode and other JSON values use a schema instruction. Fevex
|
|
67
|
+
still validates the returned value locally, but OpenAI does not guarantee schema
|
|
68
|
+
adherence in this mode, so the caller may need to retry an invalid result.
|
|
69
|
+
|
|
70
|
+
| Policy | Tool input | Final output |
|
|
71
|
+
| --- | --- | --- |
|
|
72
|
+
| `strict` | OpenAI strict function schema | Structured Outputs |
|
|
73
|
+
| `best-effort` | Non-strict function schema | JSON mode or JSON instruction |
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ModelGateway, PROVIDER_REASONING_UNSUPPORTED, PROVIDER_SCHEMA_UNSUPPORTED } from "@fevex/core";
|
|
2
|
+
//#region src/config.d.ts
|
|
3
|
+
type OpenAISchemaPolicy = 'strict' | 'best-effort';
|
|
4
|
+
type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
5
|
+
interface OpenAIConfig {
|
|
6
|
+
apiKey: string;
|
|
7
|
+
baseURL?: string;
|
|
8
|
+
organization?: string;
|
|
9
|
+
project?: string;
|
|
10
|
+
schemaName?: string;
|
|
11
|
+
schemaPolicy?: OpenAISchemaPolicy;
|
|
12
|
+
fetch?: FetchLike;
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/openai-error.d.ts
|
|
16
|
+
type OpenAIErrorCode = typeof PROVIDER_SCHEMA_UNSUPPORTED | typeof PROVIDER_REASONING_UNSUPPORTED;
|
|
17
|
+
interface OpenAIErrorOptions {
|
|
18
|
+
status?: number;
|
|
19
|
+
requestId?: string;
|
|
20
|
+
code?: OpenAIErrorCode;
|
|
21
|
+
cause?: unknown;
|
|
22
|
+
}
|
|
23
|
+
declare class OpenAIError extends Error {
|
|
24
|
+
status?: number;
|
|
25
|
+
requestId?: string;
|
|
26
|
+
code?: OpenAIErrorCode;
|
|
27
|
+
constructor(message: string, options?: OpenAIErrorOptions);
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/index.d.ts
|
|
31
|
+
declare function createOpenAI(config: OpenAIConfig): (modelId: string) => ModelGateway;
|
|
32
|
+
//#endregion
|
|
33
|
+
export { type OpenAIConfig, OpenAIError, type OpenAISchemaPolicy, createOpenAI };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,742 @@
|
|
|
1
|
+
import { PROVIDER_SCHEMA_UNSUPPORTED } from "@fevex/core";
|
|
2
|
+
//#region src/config.ts
|
|
3
|
+
function resolveOpenAIConfig(config) {
|
|
4
|
+
if (!isRecord$3(config)) throw new TypeError("OpenAI config must be an object");
|
|
5
|
+
if (typeof config.apiKey !== "string" || !config.apiKey.trim()) throw new TypeError("OpenAI apiKey cannot be empty");
|
|
6
|
+
assertOptionalString(config.baseURL, "baseURL");
|
|
7
|
+
assertOptionalString(config.organization, "organization");
|
|
8
|
+
assertOptionalString(config.project, "project");
|
|
9
|
+
if (config.schemaName !== void 0 && !isProviderName$1(config.schemaName)) throw new TypeError("OpenAI schemaName must match [A-Za-z0-9_-]{1,64}");
|
|
10
|
+
if (config.schemaPolicy !== void 0 && config.schemaPolicy !== "strict" && config.schemaPolicy !== "best-effort") throw new TypeError("OpenAI schemaPolicy must be \"strict\" or \"best-effort\"");
|
|
11
|
+
const requestFetch = config.fetch ?? globalThis.fetch;
|
|
12
|
+
if (typeof requestFetch !== "function") throw new TypeError("OpenAI adapter requires fetch");
|
|
13
|
+
return {
|
|
14
|
+
apiKey: config.apiKey,
|
|
15
|
+
baseURL: config.baseURL,
|
|
16
|
+
organization: config.organization,
|
|
17
|
+
project: config.project,
|
|
18
|
+
schemaName: config.schemaName,
|
|
19
|
+
schemaPolicy: config.schemaPolicy ?? "strict",
|
|
20
|
+
fetch: requestFetch
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function assertOptionalString(value, name) {
|
|
24
|
+
if (value !== void 0 && (typeof value !== "string" || !value.trim())) throw new TypeError(`OpenAI ${name} must be a non-empty string`);
|
|
25
|
+
}
|
|
26
|
+
function isProviderName$1(value) {
|
|
27
|
+
return typeof value === "string" && /^[A-Za-z0-9_-]{1,64}$/.test(value);
|
|
28
|
+
}
|
|
29
|
+
function isRecord$3(value) {
|
|
30
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/openai-error.ts
|
|
34
|
+
var OpenAIError = class extends Error {
|
|
35
|
+
status;
|
|
36
|
+
requestId;
|
|
37
|
+
code;
|
|
38
|
+
constructor(message, options = {}) {
|
|
39
|
+
super(message, { cause: options.cause });
|
|
40
|
+
this.name = "OpenAIError";
|
|
41
|
+
this.status = options.status;
|
|
42
|
+
this.requestId = options.requestId;
|
|
43
|
+
this.code = options.code;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/internal/provider-state.ts
|
|
48
|
+
const stateBrand = Symbol("openai-provider-state");
|
|
49
|
+
function readOpenAIProviderState(value, modelId) {
|
|
50
|
+
if (value === void 0) return void 0;
|
|
51
|
+
if (!isOpenAIProviderState(value) || value.modelId !== modelId) throw new OpenAIError("OpenAI providerState is invalid for this model");
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
function appendOpenAIProviderState(previous, modelId, output, toolCallIds) {
|
|
55
|
+
return {
|
|
56
|
+
[stateBrand]: true,
|
|
57
|
+
modelId,
|
|
58
|
+
turns: [...previous?.turns ?? [], {
|
|
59
|
+
toolCallIds: [...toolCallIds],
|
|
60
|
+
output: [...output]
|
|
61
|
+
}]
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function serializeOpenAIProviderState(value, modelId) {
|
|
65
|
+
const state = readOpenAIProviderState(value, modelId);
|
|
66
|
+
if (!state) throw new OpenAIError("OpenAI providerState is required");
|
|
67
|
+
return JSON.parse(JSON.stringify({
|
|
68
|
+
modelId: state.modelId,
|
|
69
|
+
turns: state.turns
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
function restoreOpenAIProviderState(value, modelId) {
|
|
73
|
+
if (typeof value !== "object" || value === null || Array.isArray(value) || value.modelId !== modelId || !Array.isArray(value.turns)) throw new OpenAIError("OpenAI serialized providerState is invalid for this model");
|
|
74
|
+
const turns = value.turns.map((turn) => {
|
|
75
|
+
if (typeof turn !== "object" || turn === null || Array.isArray(turn) || !Array.isArray(turn.toolCallIds) || !turn.toolCallIds.every((id) => typeof id === "string" && id.length > 0) || !Array.isArray(turn.output)) throw new OpenAIError("OpenAI serialized providerState is invalid for this model");
|
|
76
|
+
return {
|
|
77
|
+
toolCallIds: [...turn.toolCallIds],
|
|
78
|
+
output: structuredClone(turn.output)
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
return {
|
|
82
|
+
[stateBrand]: true,
|
|
83
|
+
modelId,
|
|
84
|
+
turns
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function isOpenAIProviderState(value) {
|
|
88
|
+
if (typeof value !== "object" || value === null) return false;
|
|
89
|
+
if (value[stateBrand] !== true || typeof value.modelId !== "string" || !Array.isArray(value.turns)) return false;
|
|
90
|
+
return value.turns.every((turn) => typeof turn === "object" && turn !== null && Array.isArray(turn.toolCallIds) && turn.toolCallIds.every((id) => typeof id === "string" && id.length > 0) && Array.isArray(turn.output));
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region src/internal/provider-schema.ts
|
|
94
|
+
const supportedTypes = /* @__PURE__ */ new Set([
|
|
95
|
+
"string",
|
|
96
|
+
"number",
|
|
97
|
+
"boolean",
|
|
98
|
+
"integer",
|
|
99
|
+
"object",
|
|
100
|
+
"array",
|
|
101
|
+
"null"
|
|
102
|
+
]);
|
|
103
|
+
const supportedFormats = /* @__PURE__ */ new Set([
|
|
104
|
+
"date-time",
|
|
105
|
+
"time",
|
|
106
|
+
"date",
|
|
107
|
+
"duration",
|
|
108
|
+
"email",
|
|
109
|
+
"hostname",
|
|
110
|
+
"ipv4",
|
|
111
|
+
"ipv6",
|
|
112
|
+
"uuid"
|
|
113
|
+
]);
|
|
114
|
+
const supportedKeywords = /* @__PURE__ */ new Set([
|
|
115
|
+
"$schema",
|
|
116
|
+
"$id",
|
|
117
|
+
"$ref",
|
|
118
|
+
"$defs",
|
|
119
|
+
"title",
|
|
120
|
+
"description",
|
|
121
|
+
"type",
|
|
122
|
+
"properties",
|
|
123
|
+
"required",
|
|
124
|
+
"additionalProperties",
|
|
125
|
+
"items",
|
|
126
|
+
"enum",
|
|
127
|
+
"const",
|
|
128
|
+
"anyOf",
|
|
129
|
+
"format",
|
|
130
|
+
"pattern",
|
|
131
|
+
"minimum",
|
|
132
|
+
"maximum",
|
|
133
|
+
"exclusiveMinimum",
|
|
134
|
+
"exclusiveMaximum",
|
|
135
|
+
"multipleOf",
|
|
136
|
+
"minItems",
|
|
137
|
+
"maxItems"
|
|
138
|
+
]);
|
|
139
|
+
function findOpenAISchemaIssue(schema) {
|
|
140
|
+
if (schema.type !== "object" || schema.anyOf !== void 0) return "$: root must be an object and must not use anyOf";
|
|
141
|
+
const state = {
|
|
142
|
+
propertyCount: 0,
|
|
143
|
+
enumCount: 0,
|
|
144
|
+
stringLength: 0
|
|
145
|
+
};
|
|
146
|
+
const issue = visitSchema(schema, "$", 0, state);
|
|
147
|
+
if (issue) return issue;
|
|
148
|
+
if (state.propertyCount > 5e3) return "$: schema exceeds OpenAI limit of 5000 object properties";
|
|
149
|
+
if (state.enumCount > 1e3) return "$: schema exceeds OpenAI limit of 1000 enum values";
|
|
150
|
+
if (state.stringLength > 12e4) return "$: schema exceeds OpenAI limit of 120000 schema string characters";
|
|
151
|
+
}
|
|
152
|
+
function visitSchema(schema, path, objectDepth, state) {
|
|
153
|
+
if (!isRecord$2(schema)) return `${path}: schema must be an object`;
|
|
154
|
+
for (const keyword of Object.keys(schema)) if (!supportedKeywords.has(keyword)) return `${path}.${keyword}: keyword is not supported by OpenAI strict schemas`;
|
|
155
|
+
if (schema.$ref !== void 0 && typeof schema.$ref !== "string") return `${path}.$ref: must be a string`;
|
|
156
|
+
const typeIssue = validateType(schema.type, path);
|
|
157
|
+
if (typeIssue) return typeIssue;
|
|
158
|
+
const types = typeof schema.type === "string" ? [schema.type] : Array.isArray(schema.type) ? schema.type : [];
|
|
159
|
+
const isObject = types.includes("object");
|
|
160
|
+
const isArray = types.includes("array");
|
|
161
|
+
const isString = types.includes("string");
|
|
162
|
+
const isNumeric = types.includes("number") || types.includes("integer");
|
|
163
|
+
const nextObjectDepth = isObject ? objectDepth + 1 : objectDepth;
|
|
164
|
+
if (nextObjectDepth > 10) return `${path}: schema exceeds OpenAI limit of 10 object nesting levels`;
|
|
165
|
+
if (!isObject && hasAny(schema, [
|
|
166
|
+
"properties",
|
|
167
|
+
"required",
|
|
168
|
+
"additionalProperties"
|
|
169
|
+
])) return `${path}: object keywords require type "object"`;
|
|
170
|
+
if (!isArray && hasAny(schema, [
|
|
171
|
+
"items",
|
|
172
|
+
"minItems",
|
|
173
|
+
"maxItems"
|
|
174
|
+
])) return `${path}: array keywords require type "array"`;
|
|
175
|
+
if (!isString && hasAny(schema, ["pattern", "format"])) return `${path}: string keywords require type "string"`;
|
|
176
|
+
if (!isNumeric && hasAny(schema, [
|
|
177
|
+
"minimum",
|
|
178
|
+
"maximum",
|
|
179
|
+
"exclusiveMinimum",
|
|
180
|
+
"exclusiveMaximum",
|
|
181
|
+
"multipleOf"
|
|
182
|
+
])) return `${path}: numeric keywords require type "number" or "integer"`;
|
|
183
|
+
if (isObject) {
|
|
184
|
+
if (!isRecord$2(schema.properties)) return `${path}.properties: must be an object`;
|
|
185
|
+
if (schema.additionalProperties !== false) return `${path}.additionalProperties: must be false`;
|
|
186
|
+
const propertyNames = Object.keys(schema.properties);
|
|
187
|
+
const required = schema.required;
|
|
188
|
+
if (!Array.isArray(required) || required.some((name) => typeof name !== "string") || required.length !== propertyNames.length || propertyNames.some((name) => !required.includes(name))) return `${path}.required: must contain every property exactly once`;
|
|
189
|
+
state.propertyCount += propertyNames.length;
|
|
190
|
+
state.stringLength += propertyNames.reduce((total, name) => total + name.length, 0);
|
|
191
|
+
for (const name of propertyNames) {
|
|
192
|
+
const issue = visitSchema(schema.properties[name], `${path}.properties.${name}`, nextObjectDepth, state);
|
|
193
|
+
if (issue) return issue;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (isArray) {
|
|
197
|
+
if (!isRecord$2(schema.items)) return `${path}.items: must be a schema object`;
|
|
198
|
+
const issue = visitSchema(schema.items, `${path}.items`, objectDepth, state);
|
|
199
|
+
if (issue) return issue;
|
|
200
|
+
}
|
|
201
|
+
if (schema.anyOf !== void 0) {
|
|
202
|
+
if (!Array.isArray(schema.anyOf) || schema.anyOf.length === 0) return `${path}.anyOf: must be a non-empty array`;
|
|
203
|
+
for (let index = 0; index < schema.anyOf.length; index += 1) {
|
|
204
|
+
const issue = visitSchema(schema.anyOf[index], `${path}.anyOf[${index}]`, objectDepth, state);
|
|
205
|
+
if (issue) return issue;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const definitions = schema.$defs;
|
|
209
|
+
if (definitions !== void 0) {
|
|
210
|
+
if (!isRecord$2(definitions)) return `${path}.$defs: must be an object`;
|
|
211
|
+
const names = Object.keys(definitions);
|
|
212
|
+
state.stringLength += names.reduce((total, name) => total + name.length, 0);
|
|
213
|
+
for (const name of names) {
|
|
214
|
+
const issue = visitSchema(definitions[name], `${path}.$defs.${name}`, objectDepth, state);
|
|
215
|
+
if (issue) return issue;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (schema.enum !== void 0) {
|
|
219
|
+
if (!Array.isArray(schema.enum) || schema.enum.some((value) => !isJsonPrimitive(value))) return `${path}.enum: must contain only JSON primitive values`;
|
|
220
|
+
state.enumCount += schema.enum.length;
|
|
221
|
+
const enumStringLength = schema.enum.reduce((total, value) => total + (typeof value === "string" ? value.length : 0), 0);
|
|
222
|
+
state.stringLength += enumStringLength;
|
|
223
|
+
if (schema.enum.length > 250 && enumStringLength > 15e3) return `${path}.enum: exceeds OpenAI limit of 15000 characters for large enums`;
|
|
224
|
+
}
|
|
225
|
+
if (schema.const !== void 0 && !isJsonPrimitive(schema.const)) return `${path}.const: must be a JSON primitive`;
|
|
226
|
+
if (typeof schema.const === "string") state.stringLength += schema.const.length;
|
|
227
|
+
if (schema.pattern !== void 0 && typeof schema.pattern !== "string") return `${path}.pattern: must be a string`;
|
|
228
|
+
if (schema.format !== void 0 && (typeof schema.format !== "string" || !supportedFormats.has(schema.format))) return `${path}.format: format "${String(schema.format)}" is not supported by OpenAI`;
|
|
229
|
+
for (const keyword of [
|
|
230
|
+
"minimum",
|
|
231
|
+
"maximum",
|
|
232
|
+
"exclusiveMinimum",
|
|
233
|
+
"exclusiveMaximum",
|
|
234
|
+
"multipleOf"
|
|
235
|
+
]) if (schema[keyword] !== void 0 && (typeof schema[keyword] !== "number" || !Number.isFinite(schema[keyword]))) return `${path}.${keyword}: must be a number`;
|
|
236
|
+
for (const keyword of ["minItems", "maxItems"]) if (schema[keyword] !== void 0 && (typeof schema[keyword] !== "number" || !Number.isInteger(schema[keyword]) || schema[keyword] < 0)) return `${path}.${keyword}: must be a non-negative integer`;
|
|
237
|
+
}
|
|
238
|
+
function validateType(value, path) {
|
|
239
|
+
if (value === void 0) return void 0;
|
|
240
|
+
if (typeof value === "string") return supportedTypes.has(value) ? void 0 : `${path}.type: type "${value}" is not supported by OpenAI`;
|
|
241
|
+
if (Array.isArray(value) && value.length === 2 && value.filter((type) => type === "null").length === 1 && value.every((type) => typeof type === "string" && supportedTypes.has(type))) return;
|
|
242
|
+
return `${path}.type: must be a supported type or a nullable two-type union`;
|
|
243
|
+
}
|
|
244
|
+
function isRecord$2(value) {
|
|
245
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
246
|
+
}
|
|
247
|
+
function hasAny(schema, keywords) {
|
|
248
|
+
return keywords.some((keyword) => schema[keyword] !== void 0);
|
|
249
|
+
}
|
|
250
|
+
function isJsonPrimitive(value) {
|
|
251
|
+
return value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value);
|
|
252
|
+
}
|
|
253
|
+
//#endregion
|
|
254
|
+
//#region src/internal/response.ts
|
|
255
|
+
function parseOpenAIResponse(response, input, modelId) {
|
|
256
|
+
assertCompletedResponse(response);
|
|
257
|
+
if (!Array.isArray(response.output)) throw new OpenAIError("OpenAI response returned an invalid output array");
|
|
258
|
+
const providerOutput = response.output;
|
|
259
|
+
const toolCalls = parseToolCalls(providerOutput);
|
|
260
|
+
const output = parseOutput(providerOutput, input.outputSchema !== void 0);
|
|
261
|
+
const usage = parseUsage(response.usage);
|
|
262
|
+
const providerState = toolCalls?.length ? appendOpenAIProviderState(readOpenAIProviderState(input.providerState, modelId), modelId, providerOutput, toolCalls.map(({ id }) => id)) : void 0;
|
|
263
|
+
if (output === void 0 && !toolCalls?.length) throw new OpenAIError("OpenAI response returned no output");
|
|
264
|
+
return {
|
|
265
|
+
...output === void 0 ? {} : { output },
|
|
266
|
+
...toolCalls?.length ? { toolCalls } : {},
|
|
267
|
+
...usage ? { usage } : {},
|
|
268
|
+
...providerState ? { providerState } : {}
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function parseToolCalls(output) {
|
|
272
|
+
if (!Array.isArray(output)) return void 0;
|
|
273
|
+
const calls = [];
|
|
274
|
+
const callIds = /* @__PURE__ */ new Set();
|
|
275
|
+
for (const item of output) {
|
|
276
|
+
if (!isRecord$1(item) || item.type !== "function_call") continue;
|
|
277
|
+
if (typeof item.call_id !== "string" || !item.call_id.trim()) throw new OpenAIError("OpenAI tool call returned an invalid call_id");
|
|
278
|
+
if (typeof item.name !== "string" || !/^[A-Za-z0-9_-]{1,64}$/.test(item.name)) throw new OpenAIError("OpenAI tool call returned an invalid name");
|
|
279
|
+
if (callIds.has(item.call_id)) throw new OpenAIError(`OpenAI tool call id "${item.call_id}" is duplicated`);
|
|
280
|
+
callIds.add(item.call_id);
|
|
281
|
+
if (typeof item.arguments !== "string") throw new OpenAIError(`OpenAI tool call "${item.name}" returned invalid arguments`);
|
|
282
|
+
let parsedArguments;
|
|
283
|
+
try {
|
|
284
|
+
parsedArguments = JSON.parse(item.arguments);
|
|
285
|
+
} catch (error) {
|
|
286
|
+
throw new OpenAIError(`OpenAI tool call "${item.name}" returned invalid JSON arguments`, { cause: error });
|
|
287
|
+
}
|
|
288
|
+
if (!isRecord$1(parsedArguments)) throw new OpenAIError(`OpenAI tool call "${item.name}" arguments must be a JSON object`);
|
|
289
|
+
calls.push({
|
|
290
|
+
id: item.call_id,
|
|
291
|
+
name: item.name,
|
|
292
|
+
input: parsedArguments
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
return calls.length ? calls : void 0;
|
|
296
|
+
}
|
|
297
|
+
function parseOutput(providerOutput, structured) {
|
|
298
|
+
const texts = [];
|
|
299
|
+
for (const item of providerOutput) {
|
|
300
|
+
if (!isRecord$1(item) || item.type !== "message" || !Array.isArray(item.content)) continue;
|
|
301
|
+
for (const content of item.content) {
|
|
302
|
+
if (!isRecord$1(content)) continue;
|
|
303
|
+
if (content.type === "refusal" && typeof content.refusal === "string") throw new OpenAIError(`OpenAI refused the response: ${content.refusal}`);
|
|
304
|
+
if (content.type === "output_text" && typeof content.text === "string") texts.push(content.text);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const text = texts.length ? texts.join("") : void 0;
|
|
308
|
+
if (text === void 0) return void 0;
|
|
309
|
+
if (!structured) return text;
|
|
310
|
+
try {
|
|
311
|
+
return JSON.parse(text);
|
|
312
|
+
} catch (error) {
|
|
313
|
+
throw new OpenAIError("OpenAI structured output was not valid JSON", { cause: error });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function assertCompletedResponse(response) {
|
|
317
|
+
if (response.status === "completed") return;
|
|
318
|
+
if (typeof response.status !== "string") throw new OpenAIError("OpenAI response returned an invalid status");
|
|
319
|
+
let detail;
|
|
320
|
+
if (isRecord$1(response.error) && typeof response.error.message === "string") detail = response.error.message;
|
|
321
|
+
else if (isRecord$1(response.incomplete_details) && typeof response.incomplete_details.reason === "string") detail = response.incomplete_details.reason;
|
|
322
|
+
throw new OpenAIError(`OpenAI response was ${response.status}${detail ? `: ${detail}` : ""}`);
|
|
323
|
+
}
|
|
324
|
+
function parseUsage(usage) {
|
|
325
|
+
if (!usage) return void 0;
|
|
326
|
+
return {
|
|
327
|
+
...typeof usage.input_tokens === "number" ? { inputTokens: usage.input_tokens } : {},
|
|
328
|
+
...typeof usage.output_tokens === "number" ? { outputTokens: usage.output_tokens } : {},
|
|
329
|
+
...typeof usage.total_tokens === "number" ? { totalTokens: usage.total_tokens } : {}
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
function isRecord$1(value) {
|
|
333
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
334
|
+
}
|
|
335
|
+
//#endregion
|
|
336
|
+
//#region src/internal/sse.ts
|
|
337
|
+
async function* readSSE(body, signal) {
|
|
338
|
+
if (!body) throw new TypeError("SSE response body is missing");
|
|
339
|
+
const reader = body.getReader();
|
|
340
|
+
const decoder = new TextDecoder();
|
|
341
|
+
let buffer = "";
|
|
342
|
+
let finished = false;
|
|
343
|
+
const takeFrame = () => {
|
|
344
|
+
const match = /\r?\n\r?\n/.exec(buffer);
|
|
345
|
+
if (!match || match.index === void 0) return void 0;
|
|
346
|
+
const frame = buffer.slice(0, match.index);
|
|
347
|
+
buffer = buffer.slice(match.index + match[0].length);
|
|
348
|
+
return frame;
|
|
349
|
+
};
|
|
350
|
+
const data = (frame) => {
|
|
351
|
+
const lines = frame.split(/\r?\n/).filter((line) => line === "data" || line.startsWith("data:")).map((line) => line.slice(5).replace(/^ /, ""));
|
|
352
|
+
return lines.length ? lines.join("\n") : void 0;
|
|
353
|
+
};
|
|
354
|
+
try {
|
|
355
|
+
while (true) {
|
|
356
|
+
signal?.throwIfAborted();
|
|
357
|
+
const next = await readChunk(reader, signal);
|
|
358
|
+
if (next.done) {
|
|
359
|
+
finished = true;
|
|
360
|
+
buffer += decoder.decode();
|
|
361
|
+
if (buffer) {
|
|
362
|
+
const value = data(buffer);
|
|
363
|
+
if (value !== void 0) yield value;
|
|
364
|
+
}
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
buffer += decoder.decode(next.value, { stream: true });
|
|
368
|
+
let frame;
|
|
369
|
+
while ((frame = takeFrame()) !== void 0) {
|
|
370
|
+
const value = data(frame);
|
|
371
|
+
if (value !== void 0) yield value;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
} finally {
|
|
375
|
+
if (!finished) await reader.cancel().catch(() => {});
|
|
376
|
+
reader.releaseLock();
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
function readChunk(reader, signal) {
|
|
380
|
+
if (!signal) return reader.read();
|
|
381
|
+
signal.throwIfAborted();
|
|
382
|
+
return new Promise((resolve, reject) => {
|
|
383
|
+
const aborted = () => reject(signal.reason);
|
|
384
|
+
signal.addEventListener("abort", aborted, { once: true });
|
|
385
|
+
reader.read().then((value) => {
|
|
386
|
+
signal.removeEventListener("abort", aborted);
|
|
387
|
+
resolve(value);
|
|
388
|
+
}, (error) => {
|
|
389
|
+
signal.removeEventListener("abort", aborted);
|
|
390
|
+
reject(error);
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
//#endregion
|
|
395
|
+
//#region src/internal/responses-api.ts
|
|
396
|
+
const defaultBaseURL = "https://api.openai.com/v1";
|
|
397
|
+
async function* streamOpenAIResponse(config, modelId, input) {
|
|
398
|
+
input.signal?.throwIfAborted();
|
|
399
|
+
assertCompatibleInput(input);
|
|
400
|
+
if (config.schemaPolicy === "strict") assertStrictSchemas(input);
|
|
401
|
+
const body = buildRequestBody(config, modelId, input);
|
|
402
|
+
let response;
|
|
403
|
+
try {
|
|
404
|
+
response = await config.fetch(toResponsesURL(config.baseURL), {
|
|
405
|
+
method: "POST",
|
|
406
|
+
headers: buildHeaders(config),
|
|
407
|
+
body: JSON.stringify(body),
|
|
408
|
+
signal: input.signal
|
|
409
|
+
});
|
|
410
|
+
} catch (error) {
|
|
411
|
+
if (input.signal?.aborted) throw input.signal.reason;
|
|
412
|
+
throw new OpenAIError("OpenAI request failed", { cause: error });
|
|
413
|
+
}
|
|
414
|
+
input.signal?.throwIfAborted();
|
|
415
|
+
const requestId = response.headers.get("x-request-id") ?? void 0;
|
|
416
|
+
if (!response.ok) {
|
|
417
|
+
const data = await readResponseJson(response, requestId, input.signal);
|
|
418
|
+
throw new OpenAIError(toOpenAIErrorMessage(data, response.status), {
|
|
419
|
+
status: response.status,
|
|
420
|
+
requestId,
|
|
421
|
+
cause: data
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
let completed = false;
|
|
425
|
+
try {
|
|
426
|
+
for await (const data of readSSE(response.body, input.signal)) {
|
|
427
|
+
if (data === "[DONE]") continue;
|
|
428
|
+
let event;
|
|
429
|
+
try {
|
|
430
|
+
event = JSON.parse(data);
|
|
431
|
+
} catch (error) {
|
|
432
|
+
throw new OpenAIError("OpenAI stream event was not valid JSON", {
|
|
433
|
+
status: response.status,
|
|
434
|
+
requestId,
|
|
435
|
+
cause: error
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
if (!isRecord(event) || typeof event.type !== "string") throw new OpenAIError("OpenAI stream event was invalid", {
|
|
439
|
+
status: response.status,
|
|
440
|
+
requestId,
|
|
441
|
+
cause: event
|
|
442
|
+
});
|
|
443
|
+
if (event.type === "response.output_text.delta") {
|
|
444
|
+
if (typeof event.delta !== "string") throw new OpenAIError("OpenAI output delta was invalid", {
|
|
445
|
+
status: response.status,
|
|
446
|
+
requestId,
|
|
447
|
+
cause: event
|
|
448
|
+
});
|
|
449
|
+
if (event.delta) yield {
|
|
450
|
+
type: "output.delta",
|
|
451
|
+
delta: event.delta
|
|
452
|
+
};
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
if (event.type === "response.completed") {
|
|
456
|
+
if (completed || !isRecord(event.response)) throw new OpenAIError("OpenAI stream returned an invalid completion", {
|
|
457
|
+
status: response.status,
|
|
458
|
+
requestId,
|
|
459
|
+
cause: event
|
|
460
|
+
});
|
|
461
|
+
completed = true;
|
|
462
|
+
yield {
|
|
463
|
+
type: "completed",
|
|
464
|
+
result: parseOpenAIResponse(event.response, input, modelId)
|
|
465
|
+
};
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
if (event.type === "response.failed" || event.type === "response.incomplete") {
|
|
469
|
+
if (!isRecord(event.response)) throw new OpenAIError("OpenAI stream returned an invalid terminal response", {
|
|
470
|
+
status: response.status,
|
|
471
|
+
requestId,
|
|
472
|
+
cause: event
|
|
473
|
+
});
|
|
474
|
+
parseOpenAIResponse(event.response, input, modelId);
|
|
475
|
+
}
|
|
476
|
+
if (event.type === "error") throw new OpenAIError(toOpenAIStreamErrorMessage(event), {
|
|
477
|
+
status: response.status,
|
|
478
|
+
requestId,
|
|
479
|
+
cause: event
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
} catch (error) {
|
|
483
|
+
if (input.signal?.aborted) throw input.signal.reason;
|
|
484
|
+
if (error instanceof OpenAIError) throw error;
|
|
485
|
+
throw new OpenAIError("OpenAI stream failed", {
|
|
486
|
+
status: response.status,
|
|
487
|
+
requestId,
|
|
488
|
+
cause: error
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
if (!completed) throw new OpenAIError("OpenAI stream ended without response.completed", {
|
|
492
|
+
status: response.status,
|
|
493
|
+
requestId
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
function buildHeaders(config) {
|
|
497
|
+
return {
|
|
498
|
+
authorization: `Bearer ${config.apiKey}`,
|
|
499
|
+
"content-type": "application/json",
|
|
500
|
+
accept: "text/event-stream",
|
|
501
|
+
...config.organization ? { "openai-organization": config.organization } : {},
|
|
502
|
+
...config.project ? { "openai-project": config.project } : {}
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
function buildRequestBody(config, modelId, input) {
|
|
506
|
+
const strict = config.schemaPolicy === "strict";
|
|
507
|
+
const options = { ...input.modelOptions ?? {} };
|
|
508
|
+
const providerLimit = options.max_output_tokens;
|
|
509
|
+
const providerReasoning = options.reasoning;
|
|
510
|
+
const providerText = options.text;
|
|
511
|
+
for (const key of [
|
|
512
|
+
"model",
|
|
513
|
+
"input",
|
|
514
|
+
"tools",
|
|
515
|
+
"tool_choice",
|
|
516
|
+
"parallel_tool_calls",
|
|
517
|
+
"max_output_tokens",
|
|
518
|
+
"text",
|
|
519
|
+
"reasoning",
|
|
520
|
+
"stream",
|
|
521
|
+
"background",
|
|
522
|
+
"previous_response_id",
|
|
523
|
+
"conversation"
|
|
524
|
+
]) delete options[key];
|
|
525
|
+
const messages = input.outputSchema && !strict ? withSchemaInstruction(input.messages, input.outputSchema) : input.messages;
|
|
526
|
+
const maxOutputTokens = cappedTokenLimit(input.maxOutputTokens, providerLimit);
|
|
527
|
+
const text = toOpenAIText(config, input, providerText);
|
|
528
|
+
const reasoning = toOpenAIReasoning(input, providerReasoning);
|
|
529
|
+
return {
|
|
530
|
+
...options,
|
|
531
|
+
model: modelId,
|
|
532
|
+
stream: true,
|
|
533
|
+
input: toOpenAIInput(messages, readOpenAIProviderState(input.providerState, modelId)),
|
|
534
|
+
...input.tools?.length ? {
|
|
535
|
+
tools: input.tools.map((tool) => toOpenAITool(tool, strict)),
|
|
536
|
+
parallel_tool_calls: false
|
|
537
|
+
} : {},
|
|
538
|
+
...text === void 0 ? {} : { text },
|
|
539
|
+
...reasoning === void 0 ? {} : { reasoning },
|
|
540
|
+
...maxOutputTokens === void 0 ? {} : { max_output_tokens: maxOutputTokens }
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
function toResponsesURL(baseURL) {
|
|
544
|
+
return `${(baseURL ?? defaultBaseURL).replace(/\/$/, "")}/responses`;
|
|
545
|
+
}
|
|
546
|
+
function cappedTokenLimit(runtimeLimit, providerLimit) {
|
|
547
|
+
if (runtimeLimit === void 0) return providerLimit;
|
|
548
|
+
return typeof providerLimit === "number" && Number.isFinite(providerLimit) && providerLimit > 0 ? Math.min(runtimeLimit, providerLimit) : runtimeLimit;
|
|
549
|
+
}
|
|
550
|
+
async function readResponseJson(response, requestId, signal) {
|
|
551
|
+
const text = await response.text();
|
|
552
|
+
signal?.throwIfAborted();
|
|
553
|
+
if (!text) return {};
|
|
554
|
+
try {
|
|
555
|
+
return JSON.parse(text);
|
|
556
|
+
} catch (error) {
|
|
557
|
+
throw new OpenAIError("OpenAI response was not valid JSON", {
|
|
558
|
+
status: response.status,
|
|
559
|
+
requestId,
|
|
560
|
+
cause: error
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
function toOpenAIInput(messages, state) {
|
|
565
|
+
const input = [];
|
|
566
|
+
let turnIndex = 0;
|
|
567
|
+
for (const message of messages) {
|
|
568
|
+
if (message.role === "tool") {
|
|
569
|
+
input.push({
|
|
570
|
+
type: "function_call_output",
|
|
571
|
+
call_id: message.toolCallId ?? "",
|
|
572
|
+
output: message.content
|
|
573
|
+
});
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
if (message.role === "assistant" && message.toolCalls?.length) {
|
|
577
|
+
const turn = state?.turns[turnIndex];
|
|
578
|
+
if (!turn) throw new OpenAIError("OpenAI providerState is required for assistant tool-call history");
|
|
579
|
+
const ids = message.toolCalls.map(({ id }) => id);
|
|
580
|
+
if (!sameStrings(turn.toolCallIds, ids)) throw new OpenAIError("OpenAI providerState does not match assistant tool-call history");
|
|
581
|
+
input.push(...turn.output);
|
|
582
|
+
turnIndex += 1;
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
input.push({
|
|
586
|
+
role: message.role,
|
|
587
|
+
content: message.content
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
if (turnIndex !== (state?.turns.length ?? 0)) throw new OpenAIError("OpenAI providerState does not match assistant tool-call history");
|
|
591
|
+
return input;
|
|
592
|
+
}
|
|
593
|
+
function toOpenAIText(config, input, providerText) {
|
|
594
|
+
if (!input.outputSchema) return providerText;
|
|
595
|
+
const text = isRecord(providerText) ? providerText : {};
|
|
596
|
+
if (config.schemaPolicy === "strict") return {
|
|
597
|
+
...text,
|
|
598
|
+
format: {
|
|
599
|
+
type: "json_schema",
|
|
600
|
+
name: config.schemaName ?? "fevex_output",
|
|
601
|
+
strict: true,
|
|
602
|
+
schema: input.outputSchema
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
if (!isObjectRoot(input.outputSchema) && providerText === void 0) return;
|
|
606
|
+
return {
|
|
607
|
+
...text,
|
|
608
|
+
format: isObjectRoot(input.outputSchema) ? { type: "json_object" } : { type: "text" }
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
function toOpenAIReasoning(input, providerReasoning) {
|
|
612
|
+
if (!input.reasoning || input.reasoning === "provider-default") return providerReasoning;
|
|
613
|
+
return {
|
|
614
|
+
...isRecord(providerReasoning) ? providerReasoning : {},
|
|
615
|
+
effort: input.reasoning
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
function toOpenAITool(tool, strict) {
|
|
619
|
+
return {
|
|
620
|
+
type: "function",
|
|
621
|
+
name: tool.name,
|
|
622
|
+
...tool.description ? { description: tool.description } : {},
|
|
623
|
+
parameters: tool.inputSchema ?? emptyObjectSchema(strict),
|
|
624
|
+
strict
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
function assertStrictSchemas(input) {
|
|
628
|
+
for (const tool of input.tools ?? []) assertStrictSchema(tool.inputSchema ?? emptyObjectSchema(true), `tool-input "${tool.name}"`);
|
|
629
|
+
if (input.outputSchema) assertStrictSchema(input.outputSchema, "output");
|
|
630
|
+
}
|
|
631
|
+
function assertCompatibleInput(input) {
|
|
632
|
+
if (!isRecord(input)) throw new OpenAIError("OpenAI input must be an object");
|
|
633
|
+
if (!Array.isArray(input.messages) || input.messages.length === 0) throw new OpenAIError("OpenAI messages must be a non-empty array");
|
|
634
|
+
if (input.modelOptions !== void 0 && !isRecord(input.modelOptions)) throw new OpenAIError("OpenAI modelOptions must be an object");
|
|
635
|
+
if (input.maxOutputTokens !== void 0 && (!Number.isInteger(input.maxOutputTokens) || input.maxOutputTokens < 1)) throw new OpenAIError("OpenAI maxOutputTokens must be a positive integer");
|
|
636
|
+
if (input.reasoning !== void 0 && ![
|
|
637
|
+
"provider-default",
|
|
638
|
+
"none",
|
|
639
|
+
"minimal",
|
|
640
|
+
"low",
|
|
641
|
+
"medium",
|
|
642
|
+
"high"
|
|
643
|
+
].includes(input.reasoning)) throw new OpenAIError("OpenAI reasoning effort is invalid");
|
|
644
|
+
if (input.outputSchema !== void 0 && !isRecord(input.outputSchema)) throw new OpenAIError("OpenAI outputSchema must be an object");
|
|
645
|
+
if (input.tools !== void 0 && !Array.isArray(input.tools)) throw new OpenAIError("OpenAI tools must be an array");
|
|
646
|
+
if ((input.tools?.length ?? 0) > 128) throw new OpenAIError("OpenAI supports at most 128 tools");
|
|
647
|
+
const toolNames = /* @__PURE__ */ new Set();
|
|
648
|
+
for (const tool of input.tools ?? []) {
|
|
649
|
+
if (!isRecord(tool) || !isProviderName(tool.name)) throw new OpenAIError("OpenAI tool names must match [A-Za-z0-9_-]{1,64}");
|
|
650
|
+
if (toolNames.has(tool.name)) throw new OpenAIError(`OpenAI tool "${tool.name}" is duplicated`);
|
|
651
|
+
toolNames.add(tool.name);
|
|
652
|
+
}
|
|
653
|
+
const callIds = /* @__PURE__ */ new Set();
|
|
654
|
+
const completedCallIds = /* @__PURE__ */ new Set();
|
|
655
|
+
for (const message of input.messages) {
|
|
656
|
+
if (!isRecord(message) || ![
|
|
657
|
+
"system",
|
|
658
|
+
"user",
|
|
659
|
+
"assistant",
|
|
660
|
+
"tool"
|
|
661
|
+
].includes(String(message.role)) || typeof message.content !== "string") throw new OpenAIError("OpenAI message is invalid");
|
|
662
|
+
if (message.role === "assistant" && message.toolCalls !== void 0) {
|
|
663
|
+
if (!Array.isArray(message.toolCalls)) throw new OpenAIError("OpenAI assistant toolCalls must be an array");
|
|
664
|
+
for (const call of message.toolCalls) {
|
|
665
|
+
if (!isRecord(call) || typeof call.id !== "string" || !call.id.trim() || !isProviderName(call.name)) throw new OpenAIError("OpenAI assistant tool call is invalid");
|
|
666
|
+
if (callIds.has(call.id)) throw new OpenAIError(`OpenAI tool call id "${call.id}" is duplicated`);
|
|
667
|
+
callIds.add(call.id);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
if (message.role === "tool") {
|
|
671
|
+
if (typeof message.toolCallId !== "string" || !message.toolCallId.trim() || !callIds.has(message.toolCallId) || completedCallIds.has(message.toolCallId)) throw new OpenAIError("OpenAI tool result has an invalid toolCallId");
|
|
672
|
+
completedCallIds.add(message.toolCallId);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
if (completedCallIds.size !== callIds.size) throw new OpenAIError("OpenAI assistant tool call is missing a tool result");
|
|
676
|
+
}
|
|
677
|
+
function assertStrictSchema(schema, boundary) {
|
|
678
|
+
const issue = findOpenAISchemaIssue(schema);
|
|
679
|
+
if (!issue) return;
|
|
680
|
+
throw new OpenAIError(`OpenAI strict schema for ${boundary} is unsupported at ${issue}`, { code: PROVIDER_SCHEMA_UNSUPPORTED });
|
|
681
|
+
}
|
|
682
|
+
function emptyObjectSchema(strict) {
|
|
683
|
+
return strict ? {
|
|
684
|
+
type: "object",
|
|
685
|
+
properties: {},
|
|
686
|
+
required: [],
|
|
687
|
+
additionalProperties: false
|
|
688
|
+
} : {
|
|
689
|
+
type: "object",
|
|
690
|
+
properties: {}
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
function withSchemaInstruction(messages, schema) {
|
|
694
|
+
return [{
|
|
695
|
+
role: "system",
|
|
696
|
+
content: `Respond with a ${isObjectRoot(schema) ? "JSON object" : "JSON value"} matching this schema: ${JSON.stringify(schema)}`
|
|
697
|
+
}, ...messages];
|
|
698
|
+
}
|
|
699
|
+
function isObjectRoot(schema) {
|
|
700
|
+
return schema.type === "object" && schema.anyOf === void 0;
|
|
701
|
+
}
|
|
702
|
+
function isProviderName(value) {
|
|
703
|
+
return typeof value === "string" && /^[A-Za-z0-9_-]{1,64}$/.test(value);
|
|
704
|
+
}
|
|
705
|
+
function sameStrings(left, right) {
|
|
706
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
707
|
+
}
|
|
708
|
+
function toOpenAIErrorMessage(data, status) {
|
|
709
|
+
if (isRecord(data) && isRecord(data.error) && typeof data.error.message === "string") return data.error.message;
|
|
710
|
+
return `OpenAI request failed with status ${status}`;
|
|
711
|
+
}
|
|
712
|
+
function toOpenAIStreamErrorMessage(event) {
|
|
713
|
+
if (typeof event.message === "string") return event.message;
|
|
714
|
+
if (isRecord(event.error) && typeof event.error.message === "string") return event.error.message;
|
|
715
|
+
return "OpenAI stream failed";
|
|
716
|
+
}
|
|
717
|
+
function isRecord(value) {
|
|
718
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
719
|
+
}
|
|
720
|
+
//#endregion
|
|
721
|
+
//#region src/index.ts
|
|
722
|
+
function createOpenAI(config) {
|
|
723
|
+
const resolvedConfig = resolveOpenAIConfig(config);
|
|
724
|
+
return (modelId) => {
|
|
725
|
+
if (typeof modelId !== "string" || !modelId.trim()) throw new TypeError("OpenAI modelId cannot be empty");
|
|
726
|
+
return {
|
|
727
|
+
metadata: {
|
|
728
|
+
provider: "openai",
|
|
729
|
+
model: modelId
|
|
730
|
+
},
|
|
731
|
+
stateCodec: {
|
|
732
|
+
serialize: (state) => serializeOpenAIProviderState(state, modelId),
|
|
733
|
+
restore: (state) => restoreOpenAIProviderState(state, modelId)
|
|
734
|
+
},
|
|
735
|
+
stream(input) {
|
|
736
|
+
return streamOpenAIResponse(resolvedConfig, modelId, input);
|
|
737
|
+
}
|
|
738
|
+
};
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
//#endregion
|
|
742
|
+
export { OpenAIError, createOpenAI };
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fevex/openai",
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
|
+
"description": "OpenAI ModelGateway adapter for Fevex.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"main": "./dist/index.mjs",
|
|
8
|
+
"module": "./dist/index.mjs",
|
|
9
|
+
"types": "./dist/index.d.mts",
|
|
10
|
+
"files": ["dist"],
|
|
11
|
+
"sideEffects": false,
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/hemia-labs/fevex.git",
|
|
15
|
+
"directory": "packages/openai"
|
|
16
|
+
},
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/hemia-labs/fevex/issues"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/hemia-labs/fevex#readme",
|
|
21
|
+
"keywords": ["ai", "agents", "openai", "typescript", "fevex"],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=20"
|
|
24
|
+
},
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.mts",
|
|
28
|
+
"import": "./dist/index.mjs"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsdown src/index.ts --format esm --dts",
|
|
33
|
+
"prepack": "bun run build",
|
|
34
|
+
"test": "bun test src/index.test.ts",
|
|
35
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@fevex/core": "0.1.0-alpha.1"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"typescript": "^5.7.0"
|
|
45
|
+
}
|
|
46
|
+
}
|