@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.
- package/LICENSE +21 -0
- package/README.md +63 -0
- package/dist/compat.d.ts +72 -0
- package/dist/compat.d.ts.map +1 -0
- package/dist/compat.js +53 -0
- package/dist/compat.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/provider.d.ts +44 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +261 -0
- package/dist/provider.js.map +1 -0
- package/dist/translate.d.ts +41 -0
- package/dist/translate.d.ts.map +1 -0
- package/dist/translate.js +92 -0
- package/dist/translate.js.map +1 -0
- package/dist/validate.d.ts +28 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +24 -0
- package/dist/validate.js.map +1 -0
- package/package.json +64 -0
- package/src/compat.test.ts +127 -0
- package/src/compat.ts +105 -0
- package/src/index.ts +37 -0
- package/src/provider.test.ts +350 -0
- package/src/provider.ts +339 -0
- package/src/translate.test.ts +137 -0
- package/src/translate.ts +120 -0
- package/src/validate.test.ts +32 -0
- package/src/validate.ts +42 -0
|
@@ -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"}
|
package/dist/validate.js
ADDED
|
@@ -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;
|