@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 +33 -0
- package/src/capabilities.ts +123 -0
- package/src/errors.ts +76 -0
- package/src/extension.ts +224 -0
- package/src/index.ts +25 -0
- package/src/responses.ts +386 -0
- package/src/sse.ts +35 -0
- package/src/stream.ts +360 -0
- package/src/thinking.ts +57 -0
- package/src/translate.ts +282 -0
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
|
+
}
|
package/src/extension.ts
ADDED
|
@@ -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
|
+
};
|