@dex-ai/openai 0.1.8

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/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@dex-ai/openai",
3
+ "version": "0.1.8",
4
+ "description": "OpenAI Chat Completions provider for @dex-ai/sdk.",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./src/index.ts",
9
+ "default": "./src/index.ts"
10
+ }
11
+ },
12
+ "files": [
13
+ "src"
14
+ ],
15
+ "scripts": {
16
+ "typecheck": "tsc --noEmit"
17
+ },
18
+ "dependencies": {
19
+ "@dex-ai/sdk": "^0.1.1",
20
+ "zod-to-json-schema": "^3.24.0"
21
+ },
22
+ "devDependencies": {
23
+ "zod": "^3.23.8"
24
+ },
25
+ "peerDependencies": {
26
+ "zod": "^3.23.0"
27
+ },
28
+ "sideEffects": false,
29
+ "publishConfig": {
30
+ "access": "public",
31
+ "registry": "https://registry.npmjs.org/"
32
+ }
33
+ }
@@ -0,0 +1,123 @@
1
+ /** OpenAI model capability metadata not exposed by /v1/models. */
2
+
3
+ export interface OpenAIModelCapabilities {
4
+ readonly contextWindow?: number;
5
+ }
6
+
7
+ /**
8
+ * The OpenAI Models API currently returns model identity/ownership fields, not
9
+ * context length. Keep a best-effort provider-side map for native OpenAI models.
10
+ *
11
+ * Context windows below were generated by intersecting OpenAI /v1/models with
12
+ * OpenRouter's openai/* model metadata, then adding prefix rules for dated
13
+ * OpenAI aliases that share the same family limits.
14
+ */
15
+ const EXACT_MODEL_CAPABILITIES: Record<string, OpenAIModelCapabilities> = {
16
+ "gpt-3.5-turbo": { contextWindow: 16_385 },
17
+ "gpt-3.5-turbo-0613": { contextWindow: 4_095 },
18
+ "gpt-3.5-turbo-16k": { contextWindow: 16_385 },
19
+ "gpt-3.5-turbo-instruct": { contextWindow: 4_095 },
20
+ "gpt-4": { contextWindow: 8_191 },
21
+ "gpt-4-0314": { contextWindow: 8_191 },
22
+ "gpt-4-1106-preview": { contextWindow: 128_000 },
23
+ "gpt-4-turbo": { contextWindow: 128_000 },
24
+ "gpt-4-turbo-preview": { contextWindow: 128_000 },
25
+ "gpt-4.1": { contextWindow: 1_047_576 },
26
+ "gpt-4.1-mini": { contextWindow: 1_047_576 },
27
+ "gpt-4.1-nano": { contextWindow: 1_047_576 },
28
+ "gpt-4o": { contextWindow: 128_000 },
29
+ "gpt-4o-2024-05-13": { contextWindow: 128_000 },
30
+ "gpt-4o-2024-08-06": { contextWindow: 128_000 },
31
+ "gpt-4o-2024-11-20": { contextWindow: 128_000 },
32
+ "gpt-4o-audio-preview": { contextWindow: 128_000 },
33
+ "gpt-4o-mini": { contextWindow: 128_000 },
34
+ "gpt-4o-mini-2024-07-18": { contextWindow: 128_000 },
35
+ "gpt-4o-mini-search-preview": { contextWindow: 128_000 },
36
+ "gpt-4o-search-preview": { contextWindow: 128_000 },
37
+ "gpt-5": { contextWindow: 400_000 },
38
+ "gpt-5-codex": { contextWindow: 400_000 },
39
+ "gpt-5-mini": { contextWindow: 400_000 },
40
+ "gpt-5-nano": { contextWindow: 400_000 },
41
+ "gpt-5-pro": { contextWindow: 400_000 },
42
+ "gpt-5.1": { contextWindow: 400_000 },
43
+ "gpt-5.1-codex": { contextWindow: 400_000 },
44
+ "gpt-5.1-codex-max": { contextWindow: 400_000 },
45
+ "gpt-5.1-codex-mini": { contextWindow: 400_000 },
46
+ "gpt-5.2": { contextWindow: 400_000 },
47
+ "gpt-5.2-codex": { contextWindow: 400_000 },
48
+ "gpt-5.2-pro": { contextWindow: 400_000 },
49
+ "gpt-5.3-codex": { contextWindow: 400_000 },
50
+ "gpt-5.4": { contextWindow: 1_050_000 },
51
+ "gpt-5.4-mini": { contextWindow: 400_000 },
52
+ "gpt-5.4-nano": { contextWindow: 400_000 },
53
+ "gpt-5.4-pro": { contextWindow: 1_050_000 },
54
+ "gpt-5.5": { contextWindow: 1_050_000 },
55
+ "gpt-5.5-pro": { contextWindow: 1_050_000 },
56
+ "gpt-audio": { contextWindow: 128_000 },
57
+ "gpt-audio-mini": { contextWindow: 128_000 },
58
+ "o1": { contextWindow: 200_000 },
59
+ "o1-mini": { contextWindow: 128_000 },
60
+ "o1-pro": { contextWindow: 200_000 },
61
+ "o3": { contextWindow: 200_000 },
62
+ "o3-mini": { contextWindow: 200_000 },
63
+ "o4-mini": { contextWindow: 200_000 },
64
+ };
65
+
66
+ const PREFIX_MODEL_CAPABILITIES: ReadonlyArray<
67
+ readonly [prefix: string, capabilities: OpenAIModelCapabilities]
68
+ > = [
69
+ ["gpt-3.5-turbo-instruct-", { contextWindow: 4_095 }],
70
+ ["gpt-3.5-turbo-", { contextWindow: 16_385 }],
71
+ ["gpt-4-turbo-", { contextWindow: 128_000 }],
72
+ ["gpt-4.1-mini-", { contextWindow: 1_047_576 }],
73
+ ["gpt-4.1-nano-", { contextWindow: 1_047_576 }],
74
+ ["gpt-4.1-", { contextWindow: 1_047_576 }],
75
+ ["gpt-4o-mini-search-preview-", { contextWindow: 128_000 }],
76
+ ["gpt-4o-search-preview-", { contextWindow: 128_000 }],
77
+ ["gpt-4o-mini-", { contextWindow: 128_000 }],
78
+ ["gpt-4o-", { contextWindow: 128_000 }],
79
+ ["gpt-4-", { contextWindow: 8_191 }],
80
+ ["gpt-5.5-pro-", { contextWindow: 1_050_000 }],
81
+ ["gpt-5.5-", { contextWindow: 1_050_000 }],
82
+ ["gpt-5.4-pro-", { contextWindow: 1_050_000 }],
83
+ ["gpt-5.4-mini-", { contextWindow: 400_000 }],
84
+ ["gpt-5.4-nano-", { contextWindow: 400_000 }],
85
+ ["gpt-5.4-", { contextWindow: 1_050_000 }],
86
+ ["gpt-5.3-codex-", { contextWindow: 400_000 }],
87
+ ["gpt-5.3-chat-", { contextWindow: 128_000 }],
88
+ ["gpt-5.2-pro-", { contextWindow: 400_000 }],
89
+ ["gpt-5.2-codex-", { contextWindow: 400_000 }],
90
+ ["gpt-5.2-chat-", { contextWindow: 128_000 }],
91
+ ["gpt-5.2-", { contextWindow: 400_000 }],
92
+ ["gpt-5.1-codex-", { contextWindow: 400_000 }],
93
+ ["gpt-5.1-chat-", { contextWindow: 128_000 }],
94
+ ["gpt-5.1-", { contextWindow: 400_000 }],
95
+ ["gpt-5-pro-", { contextWindow: 400_000 }],
96
+ ["gpt-5-mini-", { contextWindow: 400_000 }],
97
+ ["gpt-5-nano-", { contextWindow: 400_000 }],
98
+ ["gpt-5-codex-", { contextWindow: 400_000 }],
99
+ ["gpt-5-chat-", { contextWindow: 128_000 }],
100
+ ["gpt-5-", { contextWindow: 400_000 }],
101
+ ["gpt-audio-mini-", { contextWindow: 128_000 }],
102
+ ["gpt-audio-", { contextWindow: 128_000 }],
103
+ ["o1-mini-", { contextWindow: 128_000 }],
104
+ ["o1-pro-", { contextWindow: 200_000 }],
105
+ ["o1-", { contextWindow: 200_000 }],
106
+ ["o3-mini-", { contextWindow: 200_000 }],
107
+ ["o3-pro-", { contextWindow: 200_000 }],
108
+ ["o3-", { contextWindow: 200_000 }],
109
+ ["o4-mini-", { contextWindow: 200_000 }],
110
+ ];
111
+
112
+ export function getOpenAIModelCapabilities(
113
+ modelId: string,
114
+ ): OpenAIModelCapabilities {
115
+ const exact = EXACT_MODEL_CAPABILITIES[modelId];
116
+ if (exact) return exact;
117
+
118
+ for (const [prefix, capabilities] of PREFIX_MODEL_CAPABILITIES) {
119
+ if (modelId.startsWith(prefix)) return capabilities;
120
+ }
121
+
122
+ return {};
123
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,76 @@
1
+ /** OpenAI-specific error class. */
2
+
3
+ export class OpenAIError extends Error {
4
+ readonly status: number;
5
+ readonly code: string | null;
6
+ readonly type: string | null;
7
+
8
+ constructor(
9
+ status: number,
10
+ code: string | null,
11
+ type: string | null,
12
+ message: string,
13
+ ) {
14
+ super(message);
15
+ this.name = "OpenAIError";
16
+ this.status = status;
17
+ this.code = code;
18
+ this.type = type;
19
+ }
20
+
21
+ static async fromResponse(res: Response): Promise<OpenAIError> {
22
+ let body: string;
23
+ try {
24
+ body = await res.text();
25
+ } catch {
26
+ body = "";
27
+ }
28
+
29
+ let code: string | null = null;
30
+ let type: string | null = null;
31
+ let message = `${res.status} ${res.statusText}`;
32
+
33
+ try {
34
+ const json = JSON.parse(body) as {
35
+ error?: {
36
+ message?: string;
37
+ code?: string | number;
38
+ type?: string;
39
+ metadata?: {
40
+ raw?: string;
41
+ provider_name?: string;
42
+ };
43
+ };
44
+ };
45
+ if (json.error) {
46
+ message = json.error.message ?? message;
47
+ code = json.error.code != null ? String(json.error.code) : null;
48
+ type = json.error.type ?? null;
49
+
50
+ // OpenRouter wraps upstream provider errors in metadata.raw —
51
+ // extract the inner message for a more helpful error.
52
+ if (json.error.metadata?.raw) {
53
+ try {
54
+ const inner = JSON.parse(json.error.metadata.raw) as {
55
+ error?: { message?: string; code?: string; type?: string };
56
+ };
57
+ if (inner.error?.message) {
58
+ const provider = json.error.metadata.provider_name;
59
+ message = provider
60
+ ? `${inner.error.message} (provider: ${provider})`
61
+ : inner.error.message;
62
+ }
63
+ if (inner.error?.code) code = inner.error.code;
64
+ if (inner.error?.type) type = inner.error.type;
65
+ } catch {
66
+ // metadata.raw wasn't valid JSON — use outer message
67
+ }
68
+ }
69
+ }
70
+ } catch {
71
+ if (body) message += ` — ${body.slice(0, 200)}`;
72
+ }
73
+
74
+ return new OpenAIError(res.status, code, type, message);
75
+ }
76
+ }
@@ -0,0 +1,224 @@
1
+ /**
2
+ * OpenAI Extension — provides models via Chat Completions or Responses API.
3
+ */
4
+
5
+ import type {
6
+ Extension,
7
+ Model,
8
+ StreamPart,
9
+ ModelRequest,
10
+ ThinkingLevel,
11
+ } from "@dex-ai/sdk";
12
+ import { createChatStream } from "./stream";
13
+ import { createResponsesStream } from "./responses";
14
+ import { getOpenAIModelCapabilities } from "./capabilities";
15
+
16
+ /* ------------------------------------------------------------------ */
17
+ /* Options */
18
+ /* ------------------------------------------------------------------ */
19
+
20
+ export interface OpenAIExtensionOptions {
21
+ /** API key. Falls back to OPENAI_API_KEY. */
22
+ readonly apiKey?: string;
23
+ /** Base URL (without trailing slash). Falls back to OPENAI_BASE_URL → https://api.openai.com/v1 */
24
+ readonly baseUrl?: string;
25
+ /** Extension name (used as provider name in Agent.create). Default: "openai". */
26
+ readonly name?: string;
27
+ /** Model IDs to register statically. */
28
+ readonly models?: ReadonlyArray<string | ModelConfig>;
29
+ /**
30
+ * Models endpoint path appended to baseUrl for discovery.
31
+ * Default: "/models". Set to null to disable discovery.
32
+ */
33
+ readonly modelsPath?: string | null;
34
+ /** Emit raw-chunk parts during streaming. Default: false. */
35
+ readonly rawChunks?: boolean;
36
+ /**
37
+ * API mode:
38
+ * - "chat-completions" (default): POST /v1/chat/completions
39
+ * - "responses": POST /v1/responses (enables reasoning-delta streaming)
40
+ */
41
+ readonly mode?: "chat-completions" | "responses";
42
+ /** Additional headers sent with every request. */
43
+ readonly headers?: Record<string, string>;
44
+ /** Fetch override (for retry wrappers, testing). */
45
+ readonly fetch?: (url: string, init: RequestInit) => Promise<Response>;
46
+ /** Stream idle timeout in ms. If no SSE data arrives within this window, the stream aborts. Default: 120000 (2 min). */
47
+ readonly streamIdleTimeoutMs?: number;
48
+ }
49
+
50
+ export interface ModelConfig {
51
+ readonly id: string;
52
+ readonly name?: string;
53
+ readonly contextWindow?: number;
54
+ readonly maxTokens?: number;
55
+ readonly reasoning?: boolean;
56
+ readonly thinkingLevels?: ReadonlyArray<ThinkingLevel>;
57
+ readonly input?: ReadonlyArray<"text" | "image">;
58
+ }
59
+
60
+ /* ------------------------------------------------------------------ */
61
+ /* Helpers */
62
+ /* ------------------------------------------------------------------ */
63
+
64
+ function resolveBaseUrl(opts: OpenAIExtensionOptions): string {
65
+ const raw =
66
+ opts.baseUrl || process.env.OPENAI_BASE_URL || "https://api.openai.com/v1";
67
+ return raw.replace(/\/+$/, "");
68
+ }
69
+
70
+ function resolveApiKey(opts: OpenAIExtensionOptions): string {
71
+ const key = opts.apiKey ?? process.env.OPENAI_API_KEY;
72
+ if (!key) {
73
+ throw new Error(
74
+ "@dex-ai/openai: no API key found. Set apiKey or OPENAI_API_KEY.",
75
+ );
76
+ }
77
+ return key;
78
+ }
79
+
80
+ /* ------------------------------------------------------------------ */
81
+ /* Extension factory */
82
+ /* ------------------------------------------------------------------ */
83
+
84
+ export function openaiExtension(opts: OpenAIExtensionOptions = {}): Extension {
85
+ const extName = opts.name ?? "openai";
86
+ const baseUrl = resolveBaseUrl(opts);
87
+ const apiKey = resolveApiKey(opts);
88
+ const rawChunks = opts.rawChunks ?? false;
89
+ const mode = opts.mode ?? "chat-completions";
90
+ const doFetch =
91
+ opts.fetch ?? ((url: string, init: RequestInit) => fetch(url, init));
92
+
93
+ const streamOpts = {
94
+ baseUrl,
95
+ apiKey,
96
+ modelId: "", // overridden per model
97
+ providerName: extName,
98
+ rawChunks,
99
+ doFetch,
100
+ ...(opts.headers !== undefined ? { headers: opts.headers } : {}),
101
+ ...(opts.streamIdleTimeoutMs !== undefined
102
+ ? { streamIdleTimeoutMs: opts.streamIdleTimeoutMs }
103
+ : {}),
104
+ };
105
+
106
+ function createStreamFn(
107
+ modelId: string,
108
+ modelMaxTokens?: number,
109
+ ): (req: ModelRequest) => AsyncIterable<StreamPart> {
110
+ const perModel = { ...streamOpts, modelId };
111
+ if (mode === "responses") {
112
+ return createResponsesStream(perModel, modelMaxTokens);
113
+ }
114
+ return createChatStream(perModel, modelMaxTokens);
115
+ }
116
+
117
+ // Static models from config
118
+ const staticModels: Model[] = (opts.models ?? []).map((m) => {
119
+ const cfg: ModelConfig = typeof m === "string" ? { id: m } : m;
120
+ const capabilities = getOpenAIModelCapabilities(cfg.id);
121
+ const contextWindow = cfg.contextWindow ?? capabilities.contextWindow;
122
+ return {
123
+ id: cfg.id,
124
+ ...(cfg.name !== undefined ? { name: cfg.name } : {}),
125
+ ...(contextWindow !== undefined ? { contextWindow } : {}),
126
+ ...(cfg.maxTokens !== undefined ? { maxTokens: cfg.maxTokens } : {}),
127
+ ...(cfg.reasoning !== undefined ? { reasoning: cfg.reasoning } : {}),
128
+ ...(cfg.thinkingLevels !== undefined
129
+ ? { thinkingLevels: cfg.thinkingLevels }
130
+ : {}),
131
+ ...(cfg.input !== undefined ? { input: cfg.input } : {}),
132
+ stream: createStreamFn(cfg.id, cfg.maxTokens),
133
+ };
134
+ });
135
+
136
+ const allModels: Model[] = [...staticModels];
137
+
138
+ return {
139
+ name: extName,
140
+ get models() {
141
+ return allModels;
142
+ },
143
+
144
+ async init() {
145
+ const modelsPath =
146
+ opts.modelsPath !== null ? (opts.modelsPath ?? "/models") : null;
147
+ if (!modelsPath) return;
148
+
149
+ try {
150
+ const modelsUrl = modelsPath.startsWith("http")
151
+ ? modelsPath
152
+ : `${baseUrl}${modelsPath}`;
153
+
154
+ const res = await doFetch(modelsUrl, {
155
+ method: "GET",
156
+ headers: {
157
+ Authorization: `Bearer ${apiKey}`,
158
+ ...opts.headers,
159
+ },
160
+ });
161
+ if (!res.ok) return; // silently skip if discovery fails
162
+
163
+ const json = (await res.json()) as any;
164
+ const modelList: Array<{
165
+ id: string;
166
+ name?: string;
167
+ max_tokens?: number;
168
+ context_window?: number;
169
+ context_length?: number;
170
+ reasoning?: boolean;
171
+ input_modalities?: string[];
172
+ }> = json.data ?? json.models ?? (Array.isArray(json) ? json : []);
173
+
174
+ // Merge discovered metadata into static models + add new ones
175
+ const staticIds = new Set(staticModels.map((m) => m.id));
176
+ for (const m of modelList) {
177
+ if (!m.id) continue;
178
+ // Support OpenAI-compatible APIs that expose context metadata,
179
+ // then fall back to provider-side native OpenAI capability data.
180
+ const ctxWindow =
181
+ m.context_window ??
182
+ m.context_length ??
183
+ getOpenAIModelCapabilities(m.id).contextWindow;
184
+
185
+ if (staticIds.has(m.id)) {
186
+ // Enrich existing static model with discovered metadata
187
+ const idx = allModels.findIndex((am) => am.id === m.id);
188
+ if (idx !== -1) {
189
+ const existing = allModels[idx]!;
190
+ allModels[idx] = {
191
+ ...existing,
192
+ ...(ctxWindow && !existing.contextWindow
193
+ ? { contextWindow: ctxWindow }
194
+ : {}),
195
+ ...(m.max_tokens && !existing.maxTokens
196
+ ? { maxTokens: m.max_tokens }
197
+ : {}),
198
+ ...(m.reasoning !== undefined &&
199
+ existing.reasoning === undefined
200
+ ? { reasoning: m.reasoning }
201
+ : {}),
202
+ };
203
+ }
204
+ continue;
205
+ }
206
+
207
+ allModels.push({
208
+ id: m.id,
209
+ ...(m.name ? { name: m.name } : {}),
210
+ ...(ctxWindow ? { contextWindow: ctxWindow } : {}),
211
+ ...(m.max_tokens ? { maxTokens: m.max_tokens } : {}),
212
+ ...(m.reasoning !== undefined ? { reasoning: m.reasoning } : {}),
213
+ ...(m.input_modalities
214
+ ? { input: m.input_modalities as ("text" | "image")[] }
215
+ : {}),
216
+ stream: createStreamFn(m.id, m.max_tokens),
217
+ });
218
+ }
219
+ } catch {
220
+ // Discovery failed — proceed with static models only
221
+ }
222
+ },
223
+ };
224
+ }
package/src/index.ts ADDED
@@ -0,0 +1,25 @@
1
+ export { openaiExtension } from "./extension";
2
+ export type { OpenAIExtensionOptions, ModelConfig } from "./extension";
3
+
4
+ import type { ProviderDescriptor } from "@dex-ai/sdk";
5
+ import { openaiExtension } from "./extension";
6
+
7
+ export const descriptor: ProviderDescriptor = {
8
+ type: "openai",
9
+ label: "OpenAI",
10
+ fields: [
11
+ { key: "apiKey", label: "API Key", required: true, secret: true, envVar: "OPENAI_API_KEY", hint: "sk-... (or press Enter to use OPENAI_API_KEY env var)" },
12
+ { key: "baseUrl", label: "Base URL", required: false, default: "https://api.openai.com/v1", hint: "Custom endpoint for Azure, vLLM, LiteLLM, etc." },
13
+ { key: "models", label: "Models", required: false, hint: "Comma-separated model IDs, or leave blank to auto-discover" },
14
+ ],
15
+ create(config) {
16
+ const models = config.models as string[] | undefined;
17
+ return openaiExtension({
18
+ ...(config.name ? { name: config.name as string } : {}),
19
+ ...(config.apiKey ? { apiKey: config.apiKey as string } : {}),
20
+ ...(config.baseUrl ? { baseUrl: config.baseUrl as string } : {}),
21
+ ...(models ? { models } : {}),
22
+ ...(models?.length ? { modelsPath: null } : {}),
23
+ });
24
+ },
25
+ };