@sembl/provider-openai 0.1.0 → 0.2.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/dist/index.cjs +192 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +104 -0
- package/package.json +14 -6
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
DEFAULT_MAX_RETRIES: () => DEFAULT_MAX_RETRIES,
|
|
34
|
+
DEFAULT_TIMEOUT_MS: () => DEFAULT_TIMEOUT_MS,
|
|
35
|
+
OpenAIProvider: () => OpenAIProvider,
|
|
36
|
+
OpenAIProviderError: () => OpenAIProviderError,
|
|
37
|
+
toResponseFormat: () => toResponseFormat
|
|
38
|
+
});
|
|
39
|
+
module.exports = __toCommonJS(index_exports);
|
|
40
|
+
|
|
41
|
+
// src/openai-provider.ts
|
|
42
|
+
var import_openai2 = __toESM(require("openai"), 1);
|
|
43
|
+
|
|
44
|
+
// src/openai-config.ts
|
|
45
|
+
var DEFAULT_MAX_RETRIES = 2;
|
|
46
|
+
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
47
|
+
|
|
48
|
+
// src/errors.ts
|
|
49
|
+
var import_openai = require("openai");
|
|
50
|
+
var OpenAIProviderError = class extends Error {
|
|
51
|
+
/** What class of failure this is. */
|
|
52
|
+
kind;
|
|
53
|
+
/**
|
|
54
|
+
* Whether another attempt could plausibly succeed. The SDK has already
|
|
55
|
+
* retried retryable transport failures (see `maxRetries`); this says only
|
|
56
|
+
* that the failure was transient in nature, so a caller running a queue can
|
|
57
|
+
* re-enqueue the item rather than dead-letter it.
|
|
58
|
+
*/
|
|
59
|
+
retryable;
|
|
60
|
+
/** HTTP status, when the failure came back as an API error. */
|
|
61
|
+
status;
|
|
62
|
+
/** OpenAI's `finish_reason`, when the call returned a choice we rejected. */
|
|
63
|
+
finishReason;
|
|
64
|
+
constructor(message, options) {
|
|
65
|
+
super(message, { cause: options.cause });
|
|
66
|
+
this.name = "OpenAIProviderError";
|
|
67
|
+
this.kind = options.kind;
|
|
68
|
+
this.retryable = options.retryable;
|
|
69
|
+
this.status = options.status;
|
|
70
|
+
this.finishReason = options.finishReason;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
function toProviderError(error) {
|
|
74
|
+
if (error instanceof import_openai.APIError) {
|
|
75
|
+
const status = error.status;
|
|
76
|
+
const retryable = error instanceof import_openai.APIConnectionError || status === void 0 || status === 408 || status === 409 || status === 429 || status >= 500;
|
|
77
|
+
return new OpenAIProviderError(
|
|
78
|
+
`OpenAI request failed${status ? ` (${status})` : ""}: ${error.message}`,
|
|
79
|
+
{ kind: "api", retryable, status, cause: error }
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return new OpenAIProviderError(
|
|
83
|
+
`OpenAI request failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
84
|
+
{ kind: "api", retryable: false, cause: error }
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/schema-converter.ts
|
|
89
|
+
var import_core = require("@sembl/core");
|
|
90
|
+
function toResponseFormat(schema, bundle) {
|
|
91
|
+
const jsonSchema = (0, import_core.toOpenAIJsonSchema)(schema, bundle);
|
|
92
|
+
return {
|
|
93
|
+
type: "json_schema",
|
|
94
|
+
json_schema: {
|
|
95
|
+
name: schema.id,
|
|
96
|
+
strict: true,
|
|
97
|
+
schema: jsonSchema.schema
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/openai-provider.ts
|
|
103
|
+
var OpenAIProvider = class {
|
|
104
|
+
client;
|
|
105
|
+
config;
|
|
106
|
+
constructor(config) {
|
|
107
|
+
this.config = config;
|
|
108
|
+
this.client = new import_openai2.default({
|
|
109
|
+
apiKey: config.apiKey,
|
|
110
|
+
baseURL: config.baseURL,
|
|
111
|
+
maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
112
|
+
timeout: config.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
async complete(request) {
|
|
116
|
+
const responseFormat = toResponseFormat(request.schema, request.bundle);
|
|
117
|
+
let completion;
|
|
118
|
+
try {
|
|
119
|
+
completion = await this.client.chat.completions.create({
|
|
120
|
+
model: this.config.model,
|
|
121
|
+
temperature: this.config.temperature ?? 0,
|
|
122
|
+
max_tokens: this.config.maxTokens,
|
|
123
|
+
messages: [
|
|
124
|
+
{ role: "system", content: request.systemPrompt },
|
|
125
|
+
{ role: "user", content: request.userInput }
|
|
126
|
+
],
|
|
127
|
+
response_format: responseFormat
|
|
128
|
+
});
|
|
129
|
+
} catch (error) {
|
|
130
|
+
throw toProviderError(error);
|
|
131
|
+
}
|
|
132
|
+
const choice = completion.choices[0];
|
|
133
|
+
if (choice?.finish_reason === "length") {
|
|
134
|
+
const cap = this.config.maxTokens ? `the ${this.config.maxTokens}-token output cap` : "the model's default output cap";
|
|
135
|
+
throw new OpenAIProviderError(
|
|
136
|
+
`OpenAI hit ${cap} before completing the "${request.schema.id}" object. Raise maxTokens, or coerce into a smaller schema.`,
|
|
137
|
+
{ kind: "truncated", retryable: false, finishReason: "length" }
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
if (choice?.message?.refusal) {
|
|
141
|
+
throw new OpenAIProviderError(
|
|
142
|
+
`OpenAI declined to extract "${request.schema.id}": ${choice.message.refusal}`,
|
|
143
|
+
{
|
|
144
|
+
kind: "no_output",
|
|
145
|
+
retryable: false,
|
|
146
|
+
finishReason: choice.finish_reason
|
|
147
|
+
}
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
if (!choice?.message?.content) {
|
|
151
|
+
throw new OpenAIProviderError(
|
|
152
|
+
`OpenAI returned no content in response (finish_reason: ${choice?.finish_reason ?? "unknown"})`,
|
|
153
|
+
{
|
|
154
|
+
kind: "no_output",
|
|
155
|
+
retryable: false,
|
|
156
|
+
finishReason: choice?.finish_reason
|
|
157
|
+
}
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
let data;
|
|
161
|
+
try {
|
|
162
|
+
data = JSON.parse(choice.message.content);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
throw new OpenAIProviderError(
|
|
165
|
+
`OpenAI returned content that is not valid JSON for "${request.schema.id}"`,
|
|
166
|
+
{ kind: "no_output", retryable: false, cause: error }
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
data,
|
|
171
|
+
usage: completion.usage ? {
|
|
172
|
+
promptTokens: completion.usage.prompt_tokens,
|
|
173
|
+
completionTokens: completion.usage.completion_tokens,
|
|
174
|
+
totalTokens: completion.usage.total_tokens,
|
|
175
|
+
// OpenAI caches long prefixes automatically and reports only what
|
|
176
|
+
// it served from cache — there is no write to account for.
|
|
177
|
+
...completion.usage.prompt_tokens_details?.cached_tokens != null && {
|
|
178
|
+
cacheReadTokens: completion.usage.prompt_tokens_details.cached_tokens
|
|
179
|
+
}
|
|
180
|
+
} : void 0
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
185
|
+
0 && (module.exports = {
|
|
186
|
+
DEFAULT_MAX_RETRIES,
|
|
187
|
+
DEFAULT_TIMEOUT_MS,
|
|
188
|
+
OpenAIProvider,
|
|
189
|
+
OpenAIProviderError,
|
|
190
|
+
toResponseFormat
|
|
191
|
+
});
|
|
192
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/openai-provider.ts","../src/openai-config.ts","../src/errors.ts","../src/schema-converter.ts"],"sourcesContent":["export { OpenAIProvider } from \"./openai-provider.js\";\nexport type { OpenAIProviderConfig } from \"./openai-config.js\";\nexport { DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT_MS } from \"./openai-config.js\";\nexport { OpenAIProviderError } from \"./errors.js\";\nexport type { ProviderErrorKind } from \"./errors.js\";\nexport { toResponseFormat } from \"./schema-converter.js\";\n","import OpenAI from \"openai\";\nimport type { Provider, ProviderRequest, ProviderResponse } from \"@sembl/core\";\nimport type { OpenAIProviderConfig } from \"./openai-config.js\";\nimport { DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT_MS } from \"./openai-config.js\";\nimport { OpenAIProviderError, toProviderError } from \"./errors.js\";\nimport { toResponseFormat } from \"./schema-converter.js\";\n\n/**\n * OpenAI provider implementation using structured outputs (json_schema response_format).\n *\n * Retries and timeouts are the SDK's (exponential backoff, `retry-after`\n * aware); this class only chooses the numbers and translates whatever comes\n * back out into an {@link OpenAIProviderError}.\n */\nexport class OpenAIProvider implements Provider {\n private client: OpenAI;\n private config: OpenAIProviderConfig;\n\n constructor(config: OpenAIProviderConfig) {\n this.config = config;\n this.client = new OpenAI({\n apiKey: config.apiKey,\n baseURL: config.baseURL,\n maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,\n timeout: config.timeoutMs ?? DEFAULT_TIMEOUT_MS,\n });\n }\n\n async complete(request: ProviderRequest): Promise<ProviderResponse> {\n const responseFormat = toResponseFormat(request.schema, request.bundle);\n\n let completion;\n try {\n completion = await this.client.chat.completions.create({\n model: this.config.model,\n temperature: this.config.temperature ?? 0,\n max_tokens: this.config.maxTokens,\n messages: [\n { role: \"system\", content: request.systemPrompt },\n { role: \"user\", content: request.userInput },\n ],\n response_format: responseFormat,\n });\n } catch (error) {\n throw toProviderError(error);\n }\n\n const choice = completion.choices[0];\n\n // A truncated response is a budget problem, not a content one: the JSON is\n // cut mid-object, so it would only fail to parse a step later.\n if (choice?.finish_reason === \"length\") {\n const cap = this.config.maxTokens\n ? `the ${this.config.maxTokens}-token output cap`\n : \"the model's default output cap\";\n throw new OpenAIProviderError(\n `OpenAI hit ${cap} before completing the \"${request.schema.id}\" object. ` +\n \"Raise maxTokens, or coerce into a smaller schema.\",\n { kind: \"truncated\", retryable: false, finishReason: \"length\" },\n );\n }\n\n if (choice?.message?.refusal) {\n throw new OpenAIProviderError(\n `OpenAI declined to extract \"${request.schema.id}\": ${choice.message.refusal}`,\n {\n kind: \"no_output\",\n retryable: false,\n finishReason: choice.finish_reason,\n },\n );\n }\n\n if (!choice?.message?.content) {\n throw new OpenAIProviderError(\n `OpenAI returned no content in response (finish_reason: ${choice?.finish_reason ?? \"unknown\"})`,\n {\n kind: \"no_output\",\n retryable: false,\n finishReason: choice?.finish_reason,\n },\n );\n }\n\n let data: Record<string, unknown>;\n try {\n data = JSON.parse(choice.message.content) as Record<string, unknown>;\n } catch (error) {\n throw new OpenAIProviderError(\n `OpenAI returned content that is not valid JSON for \"${request.schema.id}\"`,\n { kind: \"no_output\", retryable: false, cause: error },\n );\n }\n\n return {\n data,\n usage: completion.usage\n ? {\n promptTokens: completion.usage.prompt_tokens,\n completionTokens: completion.usage.completion_tokens,\n totalTokens: completion.usage.total_tokens,\n // OpenAI caches long prefixes automatically and reports only what\n // it served from cache — there is no write to account for.\n ...(completion.usage.prompt_tokens_details?.cached_tokens !=\n null && {\n cacheReadTokens:\n completion.usage.prompt_tokens_details.cached_tokens,\n }),\n }\n : undefined,\n };\n }\n}\n","import type { ProviderConfig } from \"@sembl/core\";\n\n/**\n * Configuration specific to the OpenAI provider.\n */\nexport interface OpenAIProviderConfig extends ProviderConfig {\n /** OpenAI API key. Defaults to OPENAI_API_KEY env var. */\n apiKey?: string;\n /** Base URL for the OpenAI API. Defaults to OpenAI's production endpoint. */\n baseURL?: string;\n /**\n * How many times the SDK retries a failed call before giving up. The SDK\n * retries connection errors, 408/409/429 and 5xx with exponential backoff\n * and honours `retry-after`, so there is nothing to hand-roll here.\n *\n * Defaults to {@link DEFAULT_MAX_RETRIES}.\n */\n maxRetries?: number;\n /**\n * Timeout for a single attempt, in milliseconds. Retries each get their\n * own attempt, so the worst-case wall clock is roughly\n * `timeoutMs * (maxRetries + 1)` plus backoff.\n *\n * Defaults to {@link DEFAULT_TIMEOUT_MS}.\n */\n timeoutMs?: number;\n}\n\n/** Matches the SDK's own default; stated here so it survives an SDK change. */\nexport const DEFAULT_MAX_RETRIES = 2;\n\n/**\n * Two minutes per attempt. The SDK's own default is ten, which is a long time\n * for a backend import to sit on one record when the retry is cheap.\n */\nexport const DEFAULT_TIMEOUT_MS = 120_000;\n","import { APIConnectionError, APIError } from \"openai\";\n\n/**\n * Why a provider call failed, in the terms a caller can act on.\n *\n * A batch import wants to route these differently: `\"api\"` failures are worth\n * re-queueing, `\"truncated\"` needs a bigger output budget, and `\"no_output\"`\n * is a property of that one input's content — retrying it changes nothing.\n *\n * The same three kinds are used by `@sembl/provider-anthropic`, so a caller\n * that branches on `kind` keeps working when the provider is swapped.\n */\nexport type ProviderErrorKind = \"api\" | \"truncated\" | \"no_output\";\n\n/**\n * Error thrown by the OpenAI provider.\n *\n * Branch on `kind` rather than matching the message — messages stay\n * diagnostic and are free to change.\n */\nexport class OpenAIProviderError extends Error {\n /** What class of failure this is. */\n public readonly kind: ProviderErrorKind;\n /**\n * Whether another attempt could plausibly succeed. The SDK has already\n * retried retryable transport failures (see `maxRetries`); this says only\n * that the failure was transient in nature, so a caller running a queue can\n * re-enqueue the item rather than dead-letter it.\n */\n public readonly retryable: boolean;\n /** HTTP status, when the failure came back as an API error. */\n public readonly status?: number;\n /** OpenAI's `finish_reason`, when the call returned a choice we rejected. */\n public readonly finishReason?: string;\n\n constructor(\n message: string,\n options: {\n kind: ProviderErrorKind;\n retryable: boolean;\n status?: number;\n finishReason?: string;\n cause?: unknown;\n },\n ) {\n super(message, { cause: options.cause });\n this.name = \"OpenAIProviderError\";\n this.kind = options.kind;\n this.retryable = options.retryable;\n this.status = options.status;\n this.finishReason = options.finishReason;\n }\n}\n\n/**\n * Wrap an SDK-level failure as an `OpenAIProviderError`.\n *\n * Retryability is read off the SDK's own error classes rather than the\n * message: connection failures and timeouts are transient by construction,\n * and of the status codes only 408/409/429 and 5xx are worth another attempt —\n * the same set the SDK itself retries internally.\n */\nexport function toProviderError(error: unknown): OpenAIProviderError {\n if (error instanceof APIError) {\n const status = error.status;\n const retryable =\n error instanceof APIConnectionError ||\n status === undefined ||\n status === 408 ||\n status === 409 ||\n status === 429 ||\n status >= 500;\n\n return new OpenAIProviderError(\n `OpenAI request failed${status ? ` (${status})` : \"\"}: ${error.message}`,\n { kind: \"api\", retryable, status, cause: error },\n );\n }\n\n // Anything else (an AbortError, a bug in the transport) is surfaced with the\n // same shape so callers only need one catch.\n return new OpenAIProviderError(\n `OpenAI request failed: ${error instanceof Error ? error.message : String(error)}`,\n { kind: \"api\", retryable: false, cause: error },\n );\n}\n","import type { RuntimeSchema, SchemaBundle } from \"@sembl/core\";\nimport { toOpenAIJsonSchema } from \"@sembl/core\";\nimport type OpenAI from \"openai\";\n\ntype ResponseFormatJSONSchema =\n OpenAI.ChatCompletionCreateParams[\"response_format\"] & { type: \"json_schema\" };\n\n/**\n * Convert a RuntimeSchema to an OpenAI-compatible response_format parameter.\n * Applies OpenAI strict mode constraints:\n * - All properties in `required` array\n * - Optional fields use anyOf: [type, { type: \"null\" }]\n * - additionalProperties: false on every object\n * - Everything inlined (no $ref)\n */\nexport function toResponseFormat(\n schema: RuntimeSchema,\n bundle?: SchemaBundle,\n): ResponseFormatJSONSchema {\n const jsonSchema = toOpenAIJsonSchema(schema, bundle);\n\n return {\n type: \"json_schema\",\n json_schema: {\n name: schema.id,\n strict: true,\n schema: jsonSchema.schema as Record<string, unknown>,\n },\n } as ResponseFormatJSONSchema;\n}\n"],"mappings":"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","names":["import_openai","OpenAI"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { ProviderConfig, Provider, ProviderRequest, ProviderResponse, RuntimeSchema, SchemaBundle } from '@sembl/core';
|
|
2
|
+
import OpenAI from 'openai';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Configuration specific to the OpenAI provider.
|
|
6
|
+
*/
|
|
7
|
+
interface OpenAIProviderConfig extends ProviderConfig {
|
|
8
|
+
/** OpenAI API key. Defaults to OPENAI_API_KEY env var. */
|
|
9
|
+
apiKey?: string;
|
|
10
|
+
/** Base URL for the OpenAI API. Defaults to OpenAI's production endpoint. */
|
|
11
|
+
baseURL?: string;
|
|
12
|
+
/**
|
|
13
|
+
* How many times the SDK retries a failed call before giving up. The SDK
|
|
14
|
+
* retries connection errors, 408/409/429 and 5xx with exponential backoff
|
|
15
|
+
* and honours `retry-after`, so there is nothing to hand-roll here.
|
|
16
|
+
*
|
|
17
|
+
* Defaults to {@link DEFAULT_MAX_RETRIES}.
|
|
18
|
+
*/
|
|
19
|
+
maxRetries?: number;
|
|
20
|
+
/**
|
|
21
|
+
* Timeout for a single attempt, in milliseconds. Retries each get their
|
|
22
|
+
* own attempt, so the worst-case wall clock is roughly
|
|
23
|
+
* `timeoutMs * (maxRetries + 1)` plus backoff.
|
|
24
|
+
*
|
|
25
|
+
* Defaults to {@link DEFAULT_TIMEOUT_MS}.
|
|
26
|
+
*/
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
}
|
|
29
|
+
/** Matches the SDK's own default; stated here so it survives an SDK change. */
|
|
30
|
+
declare const DEFAULT_MAX_RETRIES = 2;
|
|
31
|
+
/**
|
|
32
|
+
* Two minutes per attempt. The SDK's own default is ten, which is a long time
|
|
33
|
+
* for a backend import to sit on one record when the retry is cheap.
|
|
34
|
+
*/
|
|
35
|
+
declare const DEFAULT_TIMEOUT_MS = 120000;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* OpenAI provider implementation using structured outputs (json_schema response_format).
|
|
39
|
+
*
|
|
40
|
+
* Retries and timeouts are the SDK's (exponential backoff, `retry-after`
|
|
41
|
+
* aware); this class only chooses the numbers and translates whatever comes
|
|
42
|
+
* back out into an {@link OpenAIProviderError}.
|
|
43
|
+
*/
|
|
44
|
+
declare class OpenAIProvider implements Provider {
|
|
45
|
+
private client;
|
|
46
|
+
private config;
|
|
47
|
+
constructor(config: OpenAIProviderConfig);
|
|
48
|
+
complete(request: ProviderRequest): Promise<ProviderResponse>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Why a provider call failed, in the terms a caller can act on.
|
|
53
|
+
*
|
|
54
|
+
* A batch import wants to route these differently: `"api"` failures are worth
|
|
55
|
+
* re-queueing, `"truncated"` needs a bigger output budget, and `"no_output"`
|
|
56
|
+
* is a property of that one input's content — retrying it changes nothing.
|
|
57
|
+
*
|
|
58
|
+
* The same three kinds are used by `@sembl/provider-anthropic`, so a caller
|
|
59
|
+
* that branches on `kind` keeps working when the provider is swapped.
|
|
60
|
+
*/
|
|
61
|
+
type ProviderErrorKind = "api" | "truncated" | "no_output";
|
|
62
|
+
/**
|
|
63
|
+
* Error thrown by the OpenAI provider.
|
|
64
|
+
*
|
|
65
|
+
* Branch on `kind` rather than matching the message — messages stay
|
|
66
|
+
* diagnostic and are free to change.
|
|
67
|
+
*/
|
|
68
|
+
declare class OpenAIProviderError extends Error {
|
|
69
|
+
/** What class of failure this is. */
|
|
70
|
+
readonly kind: ProviderErrorKind;
|
|
71
|
+
/**
|
|
72
|
+
* Whether another attempt could plausibly succeed. The SDK has already
|
|
73
|
+
* retried retryable transport failures (see `maxRetries`); this says only
|
|
74
|
+
* that the failure was transient in nature, so a caller running a queue can
|
|
75
|
+
* re-enqueue the item rather than dead-letter it.
|
|
76
|
+
*/
|
|
77
|
+
readonly retryable: boolean;
|
|
78
|
+
/** HTTP status, when the failure came back as an API error. */
|
|
79
|
+
readonly status?: number;
|
|
80
|
+
/** OpenAI's `finish_reason`, when the call returned a choice we rejected. */
|
|
81
|
+
readonly finishReason?: string;
|
|
82
|
+
constructor(message: string, options: {
|
|
83
|
+
kind: ProviderErrorKind;
|
|
84
|
+
retryable: boolean;
|
|
85
|
+
status?: number;
|
|
86
|
+
finishReason?: string;
|
|
87
|
+
cause?: unknown;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
type ResponseFormatJSONSchema = OpenAI.ChatCompletionCreateParams["response_format"] & {
|
|
92
|
+
type: "json_schema";
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Convert a RuntimeSchema to an OpenAI-compatible response_format parameter.
|
|
96
|
+
* Applies OpenAI strict mode constraints:
|
|
97
|
+
* - All properties in `required` array
|
|
98
|
+
* - Optional fields use anyOf: [type, { type: "null" }]
|
|
99
|
+
* - additionalProperties: false on every object
|
|
100
|
+
* - Everything inlined (no $ref)
|
|
101
|
+
*/
|
|
102
|
+
declare function toResponseFormat(schema: RuntimeSchema, bundle?: SchemaBundle): ResponseFormatJSONSchema;
|
|
103
|
+
|
|
104
|
+
export { DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT_MS, OpenAIProvider, type OpenAIProviderConfig, OpenAIProviderError, type ProviderErrorKind, toResponseFormat };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sembl/provider-openai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "OpenAI provider for SEMBL, using structured outputs.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"llm",
|
|
@@ -21,13 +21,21 @@
|
|
|
21
21
|
"directory": "packages/provider-openai"
|
|
22
22
|
},
|
|
23
23
|
"type": "module",
|
|
24
|
-
"main": "./dist/index.
|
|
24
|
+
"main": "./dist/index.cjs",
|
|
25
|
+
"module": "./dist/index.js",
|
|
25
26
|
"types": "./dist/index.d.ts",
|
|
26
27
|
"exports": {
|
|
27
28
|
".": {
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
"import": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"default": "./dist/index.js"
|
|
32
|
+
},
|
|
33
|
+
"require": {
|
|
34
|
+
"types": "./dist/index.d.cts",
|
|
35
|
+
"default": "./dist/index.cjs"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"./package.json": "./package.json"
|
|
31
39
|
},
|
|
32
40
|
"files": [
|
|
33
41
|
"dist",
|
|
@@ -43,7 +51,7 @@
|
|
|
43
51
|
"provenance": true
|
|
44
52
|
},
|
|
45
53
|
"dependencies": {
|
|
46
|
-
"@sembl/core": "0.
|
|
54
|
+
"@sembl/core": "0.2.0"
|
|
47
55
|
},
|
|
48
56
|
"devDependencies": {
|
|
49
57
|
"openai": "^4.0.0",
|