@moxxy/plugin-provider-openai 0.1.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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"translate.d.ts","sourceRoot":"","sources":["../src/translate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAG3D,MAAM,MAAM,qBAAqB,GAC7B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,SAAS,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,GACjD;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC;AAErE,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,QAAQ,GAAG,MAAM,GAAG,WAAW,GAAG,MAAM,CAAC;IAC/C,OAAO,CAAC,EAAE,MAAM,GAAG,aAAa,CAAC,qBAAqB,CAAC,GAAG,IAAI,CAAC;IAC/D,UAAU,CAAC,EAAE,KAAK,CAAC;QACjB,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,UAAU,CAAC;QACjB,QAAQ,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,SAAS,EAAE,MAAM,CAAA;SAAE,CAAC;KAC/C,CAAC,CAAC;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE;QACR,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,UAAU,EAAE,OAAO,CAAC;KACrB,CAAC;CACH;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,aAAa,CAAC,eAAe,CAAC,GAAG,iBAAiB,EAAE,CA8E9F;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,GAAG,aAAa,EAAE,CAS5E"}
@@ -0,0 +1,92 @@
1
+ import { zodToJsonSchema } from '@moxxy/sdk';
2
+ export function toOpenAIMessages(messages) {
3
+ const out = [];
4
+ for (const msg of messages) {
5
+ if (msg.role === 'system') {
6
+ const text = msg.content.find((c) => c.type === 'text')?.text ?? '';
7
+ if (text)
8
+ out.push({ role: 'system', content: text });
9
+ continue;
10
+ }
11
+ if (msg.role === 'user') {
12
+ const hasRichPart = msg.content.some((c) => c.type === 'image' || c.type === 'document');
13
+ if (hasRichPart) {
14
+ // Vision/document user message: emit content as a parts array so
15
+ // base64 images and documents (PDFs) ride alongside text. Non-vision
16
+ // / non-document OpenAI models will 400 on this shape — callers gate
17
+ // by `supportsImages` / `supportsDocuments` on the model descriptor
18
+ // before attaching.
19
+ const parts = [];
20
+ for (const c of msg.content) {
21
+ if (c.type === 'text') {
22
+ parts.push({ type: 'text', text: c.text });
23
+ }
24
+ else if (c.type === 'image') {
25
+ parts.push({
26
+ type: 'image_url',
27
+ image_url: { url: `data:${c.mediaType};base64,${c.data}` },
28
+ });
29
+ }
30
+ else if (c.type === 'document') {
31
+ parts.push({
32
+ type: 'file',
33
+ file: {
34
+ ...(c.name ? { filename: c.name } : {}),
35
+ file_data: `data:${c.mediaType};base64,${c.data}`,
36
+ },
37
+ });
38
+ }
39
+ }
40
+ out.push({ role: 'user', content: parts });
41
+ }
42
+ else {
43
+ const text = msg.content
44
+ .filter((c) => c.type === 'text')
45
+ .map((c) => c.text)
46
+ .join('\n');
47
+ out.push({ role: 'user', content: text });
48
+ }
49
+ continue;
50
+ }
51
+ if (msg.role === 'assistant') {
52
+ const text = msg.content
53
+ .filter((c) => c.type === 'text')
54
+ .map((c) => c.text)
55
+ .join('');
56
+ const toolUses = msg.content.filter((c) => c.type === 'tool_use');
57
+ const message = { role: 'assistant', content: text || null };
58
+ if (toolUses.length > 0) {
59
+ message.tool_calls = toolUses.map((u) => ({
60
+ id: u.id,
61
+ type: 'function',
62
+ function: { name: u.name, arguments: JSON.stringify(u.input ?? {}) },
63
+ }));
64
+ }
65
+ out.push(message);
66
+ continue;
67
+ }
68
+ if (msg.role === 'tool_result') {
69
+ for (const block of msg.content) {
70
+ if (block.type === 'tool_result') {
71
+ out.push({
72
+ role: 'tool',
73
+ tool_call_id: block.toolUseId,
74
+ content: block.content,
75
+ });
76
+ }
77
+ }
78
+ }
79
+ }
80
+ return out;
81
+ }
82
+ export function toOpenAITools(tools) {
83
+ return tools.map((t) => ({
84
+ type: 'function',
85
+ function: {
86
+ name: t.name,
87
+ description: t.description,
88
+ parameters: (t.inputJsonSchema ?? zodToJsonSchema(t.inputSchema)),
89
+ },
90
+ }));
91
+ }
92
+ //# sourceMappingURL=translate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"translate.js","sourceRoot":"","sources":["../src/translate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ export type ValidationResult = {
2
+ ok: true;
3
+ } | {
4
+ ok: false;
5
+ message: string;
6
+ };
7
+ export interface ValidateKeyDeps {
8
+ /** Inject the OpenAI SDK constructor for tests. */
9
+ readonly client?: (apiKey: string) => {
10
+ models: {
11
+ list: () => Promise<unknown>;
12
+ };
13
+ };
14
+ /**
15
+ * Endpoint to probe. Omit for OpenAI itself; pass a vendor base URL to
16
+ * validate an OpenAI-compatible provider (groq, deepseek, openrouter, …)
17
+ * against its own `/v1/models`. Lets `@moxxy/plugin-provider-admin` reuse
18
+ * this validator instead of duplicating the OpenAI-compatible probe.
19
+ */
20
+ readonly baseURL?: string;
21
+ }
22
+ /**
23
+ * "Is this key accepted by the endpoint?" Lists models — free, no inference
24
+ * cost. Validates OpenAI by default, or an OpenAI-compatible vendor when a
25
+ * `baseURL` is supplied.
26
+ */
27
+ export declare function validateKey(key: string, deps?: ValidateKeyDeps): Promise<ValidationResult>;
28
+ //# sourceMappingURL=validate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,gBAAgB,GAAG;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7E,MAAM,WAAW,eAAe;IAC9B,mDAAmD;IACnD,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK;QACpC,MAAM,EAAE;YAAE,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;SAAE,CAAC;KAC1C,CAAC;IACF;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,eAAoB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAYpG"}
@@ -0,0 +1,24 @@
1
+ import OpenAI from 'openai';
2
+ /**
3
+ * "Is this key accepted by the endpoint?" Lists models — free, no inference
4
+ * cost. Validates OpenAI by default, or an OpenAI-compatible vendor when a
5
+ * `baseURL` is supplied.
6
+ */
7
+ export async function validateKey(key, deps = {}) {
8
+ if (!key || key.trim().length < 8) {
9
+ return { ok: false, message: 'key looks too short' };
10
+ }
11
+ const make = deps.client ?? ((k) => defaultMaker(k, deps.baseURL));
12
+ try {
13
+ const client = make(key);
14
+ await client.models.list();
15
+ return { ok: true };
16
+ }
17
+ catch (err) {
18
+ return { ok: false, message: err instanceof Error ? err.message : String(err) };
19
+ }
20
+ }
21
+ function defaultMaker(apiKey, baseURL) {
22
+ return new OpenAI({ apiKey, ...(baseURL ? { baseURL } : {}) });
23
+ }
24
+ //# sourceMappingURL=validate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate.js","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAkB5B;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,GAAW,EAAE,OAAwB,EAAE;IACvE,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAClC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,qBAAqB,EAAE,CAAC;IACvD,CAAC;IACD,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,IAAI,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC,YAAY,CAAC,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;IAC3E,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;QACzB,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QAC3B,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;IACtB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;IAClF,CAAC;AACH,CAAC;AAED,SAAS,YAAY,CAAC,MAAc,EAAE,OAAgB;IACpD,OAAO,IAAI,MAAM,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAE5D,CAAC;AACJ,CAAC"}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@moxxy/plugin-provider-openai",
3
+ "version": "0.1.0",
4
+ "description": "OpenAI (and OpenAI-compatible: z.ai, xAI, Google, Ollama/local) LLMProvider plugin for moxxy. Streams Chat Completions with tool_calls; translates moxxy ProviderRequest/Event ↔ the OpenAI wire shape. Register it into an @moxxy/core Session to route turns to OpenAI-compatible models.",
5
+ "keywords": [
6
+ "moxxy",
7
+ "agent",
8
+ "provider",
9
+ "openai",
10
+ "llm",
11
+ "tool-calls"
12
+ ],
13
+ "homepage": "https://moxxy.ai",
14
+ "bugs": {
15
+ "url": "https://github.com/moxxy-ai/moxxy/issues"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/moxxy-ai/moxxy.git",
20
+ "directory": "packages/plugin-provider-openai"
21
+ },
22
+ "author": "Michal Makowski <michal.makowski97@gmail.com>",
23
+ "license": "MIT",
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "type": "module",
28
+ "main": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "import": "./dist/index.js"
34
+ }
35
+ },
36
+ "files": [
37
+ "dist",
38
+ "src"
39
+ ],
40
+ "moxxy": {
41
+ "plugin": {
42
+ "entry": "./dist/index.js",
43
+ "kind": "provider"
44
+ }
45
+ },
46
+ "dependencies": {
47
+ "openai": "^4.80.0",
48
+ "@moxxy/sdk": "0.15.0"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "^22.10.0",
52
+ "typescript": "^5.7.3",
53
+ "vitest": "^2.1.8",
54
+ "zod": "^3.24.0",
55
+ "@moxxy/tsconfig": "0.0.0",
56
+ "@moxxy/vitest-preset": "0.0.0"
57
+ },
58
+ "scripts": {
59
+ "build": "tsc -p tsconfig.json",
60
+ "typecheck": "tsc -p tsconfig.json --noEmit",
61
+ "test": "vitest run",
62
+ "clean": "rm -rf dist .turbo"
63
+ }
64
+ }
@@ -0,0 +1,127 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { ModelDescriptor } from '@moxxy/sdk';
3
+ import {
4
+ defineOpenAICompatProvider,
5
+ pickOpenAICompatConfig,
6
+ } from './compat.js';
7
+
8
+ const TEST_MODELS: ReadonlyArray<ModelDescriptor> = [
9
+ { id: 'vendor-flagship', contextWindow: 1_000_000, supportsTools: true, supportsStreaming: true },
10
+ { id: 'vendor-mini', contextWindow: 128_000, supportsTools: true, supportsStreaming: true },
11
+ ];
12
+
13
+ describe('pickOpenAICompatConfig', () => {
14
+ it('keeps only string apiKey/baseURL/defaultModel and drops everything else', () => {
15
+ const picked = pickOpenAICompatConfig({
16
+ apiKey: 'k',
17
+ baseURL: 'https://vendor/v1',
18
+ defaultModel: 'vendor-mini',
19
+ extraneous: 'ignored',
20
+ });
21
+ expect(picked).toEqual({ apiKey: 'k', baseURL: 'https://vendor/v1', defaultModel: 'vendor-mini' });
22
+ });
23
+
24
+ it('drops wrong-typed fields (falls back to undefined) instead of smuggling them through', () => {
25
+ const picked = pickOpenAICompatConfig({
26
+ apiKey: 'k',
27
+ baseURL: { not: 'a string' },
28
+ defaultModel: ['nope'],
29
+ });
30
+ expect(picked).toEqual({ apiKey: 'k', baseURL: undefined, defaultModel: undefined });
31
+ });
32
+ });
33
+
34
+ describe('defineOpenAICompatProvider', () => {
35
+ const def = defineOpenAICompatProvider({
36
+ name: 'vendor',
37
+ baseURL: 'https://vendor.example/v1',
38
+ defaultModel: 'vendor-flagship',
39
+ models: TEST_MODELS,
40
+ auth: { kind: 'apiKey', hint: 'vendor key' },
41
+ });
42
+
43
+ it('stamps the vendor slug onto the def and the built client (not openai)', () => {
44
+ expect(def.name).toBe('vendor');
45
+ const client = def.createClient({ apiKey: 'test-key' });
46
+ expect(client.name).toBe('vendor');
47
+ });
48
+
49
+ it('forces the vendor catalog onto the def and the built client', () => {
50
+ expect(def.models).toEqual(TEST_MODELS);
51
+ const client = def.createClient({ apiKey: 'test-key' });
52
+ expect(client.models).toEqual(TEST_MODELS);
53
+ });
54
+
55
+ it('returns a fresh copy of the catalog (not the caller-owned array)', () => {
56
+ expect(def.models).not.toBe(TEST_MODELS);
57
+ });
58
+
59
+ it('narrows the untyped registry config — wrong-typed fields do not crash createClient', () => {
60
+ const messy: Record<string, unknown> = {
61
+ apiKey: 'test-key',
62
+ baseURL: { not: 'a string' },
63
+ defaultModel: ['nope'],
64
+ extraneous: 'ignored',
65
+ };
66
+ const client = def.createClient(messy);
67
+ expect(client.name).toBe('vendor');
68
+ expect(client.models).toEqual(TEST_MODELS);
69
+ expect(typeof client.stream).toBe('function');
70
+ });
71
+
72
+ it('passes the auth descriptor straight through', () => {
73
+ expect(def.auth).toEqual({ kind: 'apiKey', hint: 'vendor key' });
74
+ });
75
+
76
+ it('omits auth when none is supplied', () => {
77
+ const noAuth = defineOpenAICompatProvider({
78
+ name: 'vendor',
79
+ baseURL: 'https://vendor.example/v1',
80
+ defaultModel: 'vendor-flagship',
81
+ models: TEST_MODELS,
82
+ });
83
+ expect(noAuth.auth).toBeUndefined();
84
+ });
85
+
86
+ it('wires validateKey by default and short-circuits a too-short key without a network call', async () => {
87
+ expect(def.validateKey).toBeDefined();
88
+ // The real validateOpenAICompatKey rejects keys < 8 chars before any probe,
89
+ // proving the def is wired to the shared validator (no network needed).
90
+ await expect(def.validateKey?.('')).resolves.toEqual({ ok: false, message: 'key looks too short' });
91
+ });
92
+
93
+ it('omits validateKey entirely when validate: false (local servers do not authenticate)', () => {
94
+ const local = defineOpenAICompatProvider({
95
+ name: 'local',
96
+ baseURL: 'http://localhost:11434/v1',
97
+ defaultModel: 'llama3.3',
98
+ models: TEST_MODELS,
99
+ validate: false,
100
+ });
101
+ expect(local.validateKey).toBeUndefined();
102
+ });
103
+
104
+ it('uses resolveApiKey / resolveBaseURL hooks for per-call credential fallback', () => {
105
+ const seen: Array<{ apiKey?: string; baseURL?: string }> = [];
106
+ const def2 = defineOpenAICompatProvider({
107
+ name: 'local',
108
+ baseURL: 'http://localhost:11434/v1',
109
+ defaultModel: 'llama3.3',
110
+ models: TEST_MODELS,
111
+ validate: false,
112
+ resolveApiKey: (cfg) => {
113
+ seen.push({ apiKey: cfg.apiKey });
114
+ return cfg.apiKey ?? 'placeholder';
115
+ },
116
+ resolveBaseURL: (cfg) => {
117
+ seen.push({ baseURL: cfg.baseURL });
118
+ return cfg.baseURL ?? 'http://fallback/v1';
119
+ },
120
+ });
121
+ const client = def2.createClient({});
122
+ expect(client.name).toBe('local');
123
+ // Both hooks ran (per-call) against the narrowed config.
124
+ expect(seen).toContainEqual({ apiKey: undefined });
125
+ expect(seen).toContainEqual({ baseURL: undefined });
126
+ });
127
+ });
package/src/compat.ts ADDED
@@ -0,0 +1,105 @@
1
+ import { defineProvider, type ModelDescriptor, type ProviderAuthDescriptor, type ProviderDef } from '@moxxy/sdk';
2
+ import { OpenAIProvider, type OpenAIProviderConfig } from './provider.js';
3
+ import { validateKey as validateOpenAICompatKey } from './validate.js';
4
+
5
+ /**
6
+ * The slice of the registry's untyped `Record<string, unknown>` config that an
7
+ * OpenAI-compatible vendor actually forwards. The provider registry hands
8
+ * `createClient` a `Record<string, unknown>` (resolved credentials + any
9
+ * persisted provider config); only these three optional strings are ever
10
+ * meaningful to the underlying {@link OpenAIProvider}, so we narrow to them.
11
+ */
12
+ export interface OpenAICompatConfig {
13
+ readonly apiKey?: string;
14
+ readonly baseURL?: string;
15
+ readonly defaultModel?: string;
16
+ }
17
+
18
+ /**
19
+ * Narrow the registry's untyped config down to the handful of optional string
20
+ * fields an OpenAI-compatible vendor forwards (`apiKey`/`baseURL`/`defaultModel`).
21
+ * A blanket `config as OpenAIProviderConfig` cast would silently smuggle any
22
+ * wrong-typed field straight through to the client; this pick keeps only
23
+ * known-good strings so a bad value falls back to the vendor defaults instead.
24
+ */
25
+ export function pickOpenAICompatConfig(config: Record<string, unknown>): OpenAICompatConfig {
26
+ const str = (v: unknown): string | undefined => (typeof v === 'string' ? v : undefined);
27
+ return {
28
+ apiKey: str(config.apiKey),
29
+ baseURL: str(config.baseURL),
30
+ defaultModel: str(config.defaultModel),
31
+ };
32
+ }
33
+
34
+ export interface DefineOpenAICompatProviderSpec {
35
+ /**
36
+ * Vendor slug (e.g. `xai`, `zai`, `google`, `local`). Stamped onto the
37
+ * underlying client so usage stats, provider_request/response events and
38
+ * error context attribute to the vendor, NOT `openai`.
39
+ */
40
+ readonly name: string;
41
+ /** Vendor base URL. Config may override it (narrow string pick); else this default is used. */
42
+ readonly baseURL: string;
43
+ /** Default model when a request didn't pin one. Config may override it (narrow string pick). */
44
+ readonly defaultModel: string;
45
+ /** The vendor's model catalog, forced onto the client (so context-window/capability lookups hit the vendor's models). */
46
+ readonly models: ReadonlyArray<ModelDescriptor>;
47
+ /** Auth descriptor surfaced to the setup wizard / `moxxy login`. */
48
+ readonly auth?: ProviderAuthDescriptor;
49
+ /**
50
+ * Key validation. When `true` (the default), wire `validateOpenAICompatKey`
51
+ * to probe `baseURL`'s `/models`. When `false`, omit `validateKey` entirely
52
+ * (local servers don't authenticate, so probing a possibly-offline box would
53
+ * surface confusing setup errors).
54
+ */
55
+ readonly validate?: boolean;
56
+ /**
57
+ * Optional override for resolving the API key from the narrowed config.
58
+ * Defaults to `config.apiKey`. The `local` provider uses this to fall back to
59
+ * an env var + placeholder, since the OpenAI SDK requires a non-empty key but
60
+ * local servers don't authenticate.
61
+ */
62
+ readonly resolveApiKey?: (config: OpenAICompatConfig) => string | undefined;
63
+ /**
64
+ * Optional override for resolving the base URL from the narrowed config.
65
+ * Defaults to `config.baseURL ?? baseURL`. The `local` provider uses this to
66
+ * insert an env-var fallback (`LOCAL_MODEL_BASE_URL`) between the config and
67
+ * the static default, read per-call.
68
+ */
69
+ readonly resolveBaseURL?: (config: OpenAICompatConfig) => string;
70
+ }
71
+
72
+ /**
73
+ * Build a {@link ProviderDef} for any vendor that speaks the OpenAI Chat
74
+ * Completions wire protocol. Centralizes the construction shared by xai / zai /
75
+ * google / local and the runtime-registered (`provider-admin`) vendors: it
76
+ * reuses the shared {@link OpenAIProvider} with the vendor slug + base URL +
77
+ * default model + catalog forced on, and (unless `validate: false`) wires
78
+ * `validateOpenAICompatKey` against the vendor's base URL.
79
+ *
80
+ * The per-vendor file becomes a single declarative call carrying only the
81
+ * vendor's constants; the cfg-narrowing + slug-forcing + validate wiring live
82
+ * here once.
83
+ */
84
+ export function defineOpenAICompatProvider(spec: DefineOpenAICompatProviderSpec): ProviderDef {
85
+ const resolveApiKey = spec.resolveApiKey ?? ((cfg: OpenAICompatConfig) => cfg.apiKey);
86
+ const resolveBaseURL = spec.resolveBaseURL ?? ((cfg: OpenAICompatConfig) => cfg.baseURL ?? spec.baseURL);
87
+ return defineProvider({
88
+ name: spec.name,
89
+ models: [...spec.models],
90
+ createClient: (config) => {
91
+ const cfg = pickOpenAICompatConfig(config);
92
+ return new OpenAIProvider({
93
+ apiKey: resolveApiKey(cfg),
94
+ name: spec.name,
95
+ baseURL: resolveBaseURL(cfg),
96
+ defaultModel: cfg.defaultModel ?? spec.defaultModel,
97
+ models: spec.models,
98
+ } satisfies OpenAIProviderConfig);
99
+ },
100
+ ...(spec.validate === false
101
+ ? {}
102
+ : { validateKey: (key: string) => validateOpenAICompatKey(key, { baseURL: spec.baseURL }) }),
103
+ ...(spec.auth ? { auth: spec.auth } : {}),
104
+ });
105
+ }
package/src/index.ts ADDED
@@ -0,0 +1,37 @@
1
+ import { defineProvider, definePlugin } from '@moxxy/sdk';
2
+ import { OpenAIProvider, openAIModels, type OpenAIProviderConfig } from './provider.js';
3
+
4
+ export { OpenAIProvider, openAIModels };
5
+ export type { OpenAIProviderConfig };
6
+ export { toOpenAIMessages, toOpenAITools } from './translate.js';
7
+ export { validateKey, type ValidateKeyDeps, type ValidationResult } from './validate.js';
8
+ // DI-friendly alias: any OpenAI-compatible vendor (same `models.list` probe)
9
+ // can reuse this validator. Re-exported under a vendor-neutral name so other
10
+ // packages don't couple to the OpenAI-specific `validateKey` symbol.
11
+ export { validateKey as validateOpenAICompatKey } from './validate.js';
12
+ // Shared factory for OpenAI-Chat-Completions-compatible vendors (xai/zai/google/
13
+ // local + runtime provider-admin entries). Captures the slug-forcing client
14
+ // construction + config narrowing + validateKey wiring in one place.
15
+ export {
16
+ defineOpenAICompatProvider,
17
+ pickOpenAICompatConfig,
18
+ type DefineOpenAICompatProviderSpec,
19
+ type OpenAICompatConfig,
20
+ } from './compat.js';
21
+
22
+ import { validateKey as validateOpenAIKey } from './validate.js';
23
+
24
+ export const openaiProviderDef = defineProvider({
25
+ name: 'openai',
26
+ models: [...openAIModels],
27
+ createClient: (config) => new OpenAIProvider(config as OpenAIProviderConfig),
28
+ validateKey: (key) => validateOpenAIKey(key),
29
+ });
30
+
31
+ export const openaiPlugin = definePlugin({
32
+ name: '@moxxy/plugin-provider-openai',
33
+ version: '0.0.0',
34
+ providers: [openaiProviderDef],
35
+ });
36
+
37
+ export default openaiPlugin;