@alisio/plugin-openai-compatible 0.1.0-alpha.10
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 +57 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +122 -0
- package/dist/provider.d.ts +23 -0
- package/dist/provider.js +264 -0
- package/dist/version.d.ts +8 -0
- package/dist/version.js +28 -0
- package/package.json +39 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gustavo Gutiérrez
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# @alisio/plugin-openai-compatible
|
|
2
|
+
|
|
3
|
+
**The built-in OpenAI-compatible model provider for [Alisio](https://github.com/GustavoGutierrez/alisio).**
|
|
4
|
+
Point it at any Chat Completions or Responses endpoint — OpenAI, a gateway, or a local server such
|
|
5
|
+
as llama.cpp — and use it from `/connect`.
|
|
6
|
+
|
|
7
|
+
## What it is
|
|
8
|
+
|
|
9
|
+
A `model-provider` plugin that registers the `openai-compatible` provider with the Alisio host. It
|
|
10
|
+
ships as a built-in of the `alisio` CLI and is enabled by default. Configuration and credentials
|
|
11
|
+
are supplied by the Alisio provider registry; the plugin does not persist secrets itself.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
Preinstalled in the `alisio` CLI. For an embedded host, register it through `createApplication`'s
|
|
16
|
+
`builtins` list.
|
|
17
|
+
|
|
18
|
+
## Using it: `/connect`
|
|
19
|
+
|
|
20
|
+
1. Run `alisio`, then `/connect` and choose **OpenAI compatible**.
|
|
21
|
+
2. Fields: **Base URL** (default `https://api.openai.com/v1`), **API key** (secret, optional),
|
|
22
|
+
**API key environment variable** (default `OPENAI_API_KEY`), **API mode** (`chat` or
|
|
23
|
+
`responses`), **Authentication** (`bearer` or `none`), **Token parameter** (`max_tokens`,
|
|
24
|
+
`max_completion_tokens` or `omit`), **Stream usage** (boolean).
|
|
25
|
+
3. Pick a model from the discovered catalog — or enter one manually when the endpoint exposes no
|
|
26
|
+
`GET /models` catalog (`/connect` then asks for an optional context window in tokens).
|
|
27
|
+
|
|
28
|
+
The key is read from the stored credential first, then from `process.env[apiKeyEnv]`:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
export OPENAI_API_KEY=... # never put the key in config files, profiles or READMEs
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
- Model discovery via `GET /models`; per-model context windows (overridable per profile).
|
|
37
|
+
- Chat Completions and Responses modes; bearer or no authentication; configurable token parameter.
|
|
38
|
+
- Tool calls, usage metadata, reasoning deltas and image attachments.
|
|
39
|
+
- Provider-native web search available when the API mode is `responses`.
|
|
40
|
+
- Legacy `provider` configuration (`baseURL`, `apiKeyEnv`, `model`, `apiMode`, `auth`,
|
|
41
|
+
`tokenParameter`, `streamUsage`) keeps working for startup and headless runs.
|
|
42
|
+
|
|
43
|
+
While registered, the provider also appears in `/model` and `alisio doctor`.
|
|
44
|
+
|
|
45
|
+
## Docs
|
|
46
|
+
|
|
47
|
+
[Configuration](https://gustavogutierrez.github.io/alisio/configuration) ·
|
|
48
|
+
[Plugins](https://gustavogutierrez.github.io/alisio/plugins).
|
|
49
|
+
|
|
50
|
+
## Requirements
|
|
51
|
+
|
|
52
|
+
Node.js **>= 22.16** or Bun **>= 1.4.2**.
|
|
53
|
+
|
|
54
|
+
## License
|
|
55
|
+
|
|
56
|
+
MIT. Maintained by Gustavo Gutiérrez Mercado. Source: <https://github.com/GustavoGutierrez/alisio> ·
|
|
57
|
+
npm: <https://www.npmjs.com/settings/alisio/packages>.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type Plugin } from "@alisio/sdk";
|
|
2
|
+
export declare function createOpenAICompatiblePlugin(): Plugin;
|
|
3
|
+
export type { OpenAICompatibleConfig } from "./provider.ts";
|
|
4
|
+
export { OpenAICompatibleProvider } from "./provider.ts";
|
|
5
|
+
declare const _default: Plugin;
|
|
6
|
+
export default _default;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { definePlugin, } from "@alisio/sdk";
|
|
2
|
+
import { OpenAICompatibleProvider } from "./provider.js";
|
|
3
|
+
import { loadVersion } from "./version.js";
|
|
4
|
+
const defaults = {
|
|
5
|
+
baseURL: "https://api.openai.com/v1",
|
|
6
|
+
apiKeyEnv: "OPENAI_API_KEY",
|
|
7
|
+
model: "",
|
|
8
|
+
apiMode: "chat",
|
|
9
|
+
auth: "bearer",
|
|
10
|
+
tokenParameter: "max_tokens",
|
|
11
|
+
streamUsage: false,
|
|
12
|
+
};
|
|
13
|
+
const value = (request, key, fallback) => (request.profile[key] ?? request.legacy?.[key] ?? fallback);
|
|
14
|
+
export function createOpenAICompatiblePlugin() {
|
|
15
|
+
return definePlugin({
|
|
16
|
+
id: "openai-compatible",
|
|
17
|
+
name: "OpenAI compatible",
|
|
18
|
+
description: "OpenAI Chat Completions or Responses compatible endpoint",
|
|
19
|
+
categories: ["model-provider"],
|
|
20
|
+
version: loadVersion(import.meta.url),
|
|
21
|
+
apiVersion: 1,
|
|
22
|
+
setup(api) {
|
|
23
|
+
api.providers.register({
|
|
24
|
+
id: "openai-compatible",
|
|
25
|
+
name: "OpenAI compatible",
|
|
26
|
+
description: "OpenAI Chat Completions or Responses compatible endpoint",
|
|
27
|
+
capabilities: { nativeWebSearch: { field: "apiMode", values: ["responses"] } },
|
|
28
|
+
fields: [
|
|
29
|
+
{
|
|
30
|
+
key: "baseURL",
|
|
31
|
+
label: "Base URL",
|
|
32
|
+
kind: "url",
|
|
33
|
+
required: true,
|
|
34
|
+
defaultValue: defaults.baseURL,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
key: "apiKey",
|
|
38
|
+
label: "API key",
|
|
39
|
+
kind: "secret",
|
|
40
|
+
description: "Stored only in the global credentials file",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
key: "apiKeyEnv",
|
|
44
|
+
label: "API key environment variable",
|
|
45
|
+
kind: "text",
|
|
46
|
+
defaultValue: defaults.apiKeyEnv,
|
|
47
|
+
description: "Environment variable consulted when no stored API key exists; the connection remembers this name",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
key: "apiMode",
|
|
51
|
+
label: "API mode",
|
|
52
|
+
kind: "select",
|
|
53
|
+
required: true,
|
|
54
|
+
defaultValue: "chat",
|
|
55
|
+
options: [
|
|
56
|
+
{ value: "chat", label: "Chat Completions" },
|
|
57
|
+
{ value: "responses", label: "Responses" },
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
key: "auth",
|
|
62
|
+
label: "Authentication",
|
|
63
|
+
kind: "select",
|
|
64
|
+
required: true,
|
|
65
|
+
defaultValue: "bearer",
|
|
66
|
+
options: [
|
|
67
|
+
{ value: "bearer", label: "Bearer token" },
|
|
68
|
+
{ value: "none", label: "None" },
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
key: "tokenParameter",
|
|
73
|
+
label: "Token parameter",
|
|
74
|
+
kind: "select",
|
|
75
|
+
required: true,
|
|
76
|
+
defaultValue: "max_tokens",
|
|
77
|
+
options: [
|
|
78
|
+
{ value: "max_tokens", label: "max_tokens" },
|
|
79
|
+
{ value: "max_completion_tokens", label: "max_completion_tokens" },
|
|
80
|
+
{ value: "omit", label: "Omit" },
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
key: "streamUsage",
|
|
85
|
+
label: "Request stream usage",
|
|
86
|
+
kind: "boolean",
|
|
87
|
+
defaultValue: false,
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
create(request) {
|
|
91
|
+
const auth = value(request, "auth", defaults.auth);
|
|
92
|
+
const apiKey = request.credentials.apiKey;
|
|
93
|
+
const rawApiKeyEnv = value(request, "apiKeyEnv", defaults.apiKeyEnv);
|
|
94
|
+
const apiKeyEnv = typeof rawApiKeyEnv === "string" && rawApiKeyEnv.trim()
|
|
95
|
+
? rawApiKeyEnv.trim()
|
|
96
|
+
: defaults.apiKeyEnv;
|
|
97
|
+
if (auth !== "none" && !apiKey && !process.env[apiKeyEnv])
|
|
98
|
+
throw new Error("API key is required (enter one in /connect or set the configured environment variable)");
|
|
99
|
+
const config = {
|
|
100
|
+
baseURL: value(request, "baseURL", defaults.baseURL),
|
|
101
|
+
apiKeyEnv,
|
|
102
|
+
model: value(request, "model", defaults.model),
|
|
103
|
+
apiMode: value(request, "apiMode", defaults.apiMode),
|
|
104
|
+
auth,
|
|
105
|
+
tokenParameter: value(request, "tokenParameter", defaults.tokenParameter),
|
|
106
|
+
streamUsage: value(request, "streamUsage", defaults.streamUsage),
|
|
107
|
+
...(typeof request.profile.contextWindow === "number"
|
|
108
|
+
? { contextWindow: request.profile.contextWindow }
|
|
109
|
+
: {}),
|
|
110
|
+
...(apiKey ? { apiKey } : {}),
|
|
111
|
+
};
|
|
112
|
+
const url = new URL(config.baseURL);
|
|
113
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password)
|
|
114
|
+
throw new Error("Base URL must be HTTP(S) and must not contain credentials");
|
|
115
|
+
return new OpenAICompatibleProvider(config);
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
export { OpenAICompatibleProvider } from "./provider.js";
|
|
122
|
+
export default createOpenAICompatiblePlugin();
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ModelInfo, ModelProvider, ProviderEvent } from "@alisio/sdk";
|
|
2
|
+
import OpenAI from "openai";
|
|
3
|
+
export interface OpenAICompatibleConfig {
|
|
4
|
+
baseURL: string;
|
|
5
|
+
apiKey?: string;
|
|
6
|
+
apiKeyEnv: string;
|
|
7
|
+
model: string;
|
|
8
|
+
apiMode: "chat" | "responses";
|
|
9
|
+
auth: "bearer" | "none";
|
|
10
|
+
tokenParameter: "max_tokens" | "max_completion_tokens" | "omit";
|
|
11
|
+
streamUsage: boolean;
|
|
12
|
+
contextWindow?: number;
|
|
13
|
+
}
|
|
14
|
+
export declare class OpenAICompatibleProvider implements ModelProvider {
|
|
15
|
+
#private;
|
|
16
|
+
readonly id: string;
|
|
17
|
+
readonly model: string;
|
|
18
|
+
constructor(config: OpenAICompatibleConfig, client?: OpenAI);
|
|
19
|
+
/** Lists models from GET /models. Context window fields are provider extensions. */
|
|
20
|
+
listModels(signal: AbortSignal): Promise<ModelInfo[]>;
|
|
21
|
+
stream(request: Parameters<ModelProvider["stream"]>[0]): AsyncIterable<ProviderEvent>;
|
|
22
|
+
private responses;
|
|
23
|
+
}
|
package/dist/provider.js
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import OpenAI from "openai";
|
|
2
|
+
const dataUrl = (a) => `data:${a.mimeType};base64,${a.data}`;
|
|
3
|
+
export class OpenAICompatibleProvider {
|
|
4
|
+
id;
|
|
5
|
+
model;
|
|
6
|
+
#client;
|
|
7
|
+
#config;
|
|
8
|
+
constructor(config, client) {
|
|
9
|
+
this.model = config.model;
|
|
10
|
+
const { apiKey: _apiKey, ...safeConfig } = config;
|
|
11
|
+
this.#config = safeConfig;
|
|
12
|
+
const key = config.auth === "none" ? "unused" : (config.apiKey ?? process.env[config.apiKeyEnv]);
|
|
13
|
+
if (!client && !key)
|
|
14
|
+
throw new Error(`Missing API key environment variable: ${config.apiKeyEnv}`);
|
|
15
|
+
this.id = `openai-compatible:${config.apiMode}:${config.baseURL.replace(/\/$/, "")}`;
|
|
16
|
+
this.#client =
|
|
17
|
+
client ??
|
|
18
|
+
new OpenAI({
|
|
19
|
+
baseURL: config.baseURL,
|
|
20
|
+
apiKey: key,
|
|
21
|
+
maxRetries: 0,
|
|
22
|
+
timeout: 120_000,
|
|
23
|
+
...(config.auth === "none" ? { defaultHeaders: { Authorization: null } } : {}),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/** Lists models from GET /models. Context window fields are provider extensions. */
|
|
27
|
+
async listModels(signal) {
|
|
28
|
+
const models = [];
|
|
29
|
+
for await (const m of this.#client.models.list({ signal })) {
|
|
30
|
+
const extra = m;
|
|
31
|
+
const window = [extra.context_window, extra.context_length, extra.max_context_length].find((v) => typeof v === "number" && Number.isFinite(v) && v > 0);
|
|
32
|
+
models.push({ id: m.id, ...(window ? { contextWindow: window } : {}) });
|
|
33
|
+
if (models.length >= 1000)
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
return models;
|
|
37
|
+
}
|
|
38
|
+
async *stream(request) {
|
|
39
|
+
if (this.#config.apiMode === "responses") {
|
|
40
|
+
yield* this.responses(request);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const messages = [
|
|
44
|
+
{ role: "system", content: request.instructions },
|
|
45
|
+
];
|
|
46
|
+
for (const m of request.messages) {
|
|
47
|
+
if (m.role === "user") {
|
|
48
|
+
if (m.attachments?.length) {
|
|
49
|
+
const parts = [];
|
|
50
|
+
if (m.text.trim())
|
|
51
|
+
parts.push({ type: "text", text: m.text });
|
|
52
|
+
for (const a of m.attachments)
|
|
53
|
+
parts.push({ type: "image_url", image_url: { url: dataUrl(a) } });
|
|
54
|
+
messages.push({ role: "user", content: parts });
|
|
55
|
+
}
|
|
56
|
+
else
|
|
57
|
+
messages.push({ role: "user", content: m.text });
|
|
58
|
+
}
|
|
59
|
+
else if (m.role === "tool")
|
|
60
|
+
messages.push({ role: "tool", tool_call_id: m.callId, content: JSON.stringify(m.result) });
|
|
61
|
+
else
|
|
62
|
+
messages.push({
|
|
63
|
+
role: "assistant",
|
|
64
|
+
content: m.text || null,
|
|
65
|
+
...(m.calls.length
|
|
66
|
+
? {
|
|
67
|
+
tool_calls: m.calls.map((c) => ({
|
|
68
|
+
id: c.id,
|
|
69
|
+
type: "function",
|
|
70
|
+
function: { name: c.name, arguments: c.arguments },
|
|
71
|
+
})),
|
|
72
|
+
}
|
|
73
|
+
: {}),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
const limit = this.#config.tokenParameter === "omit"
|
|
77
|
+
? {}
|
|
78
|
+
: { [this.#config.tokenParameter]: request.maxOutputTokens };
|
|
79
|
+
const stream = await this.#client.chat.completions.create({
|
|
80
|
+
model: request.model || this.model,
|
|
81
|
+
messages,
|
|
82
|
+
stream: true,
|
|
83
|
+
...limit,
|
|
84
|
+
...(this.#config.streamUsage ? { stream_options: { include_usage: true } } : {}),
|
|
85
|
+
...(request.tools.length || request.nativeTools?.length
|
|
86
|
+
? {
|
|
87
|
+
tools: [
|
|
88
|
+
...request.tools.map((t) => ({
|
|
89
|
+
type: "function",
|
|
90
|
+
function: { name: t.name, description: t.description, parameters: t.inputSchema },
|
|
91
|
+
})),
|
|
92
|
+
...(request.nativeTools ?? []),
|
|
93
|
+
],
|
|
94
|
+
}
|
|
95
|
+
: {}),
|
|
96
|
+
}, { signal: request.signal });
|
|
97
|
+
let text = "", finish = null, usage;
|
|
98
|
+
const calls = new Map();
|
|
99
|
+
for await (const chunk of stream) {
|
|
100
|
+
if (chunk.usage) {
|
|
101
|
+
// DeepSeek reports prompt_cache_hit_tokens; OpenAI uses prompt_tokens_details.
|
|
102
|
+
const extra = chunk.usage;
|
|
103
|
+
const cached = chunk.usage.prompt_tokens_details?.cached_tokens ?? extra.prompt_cache_hit_tokens;
|
|
104
|
+
usage = {
|
|
105
|
+
input: chunk.usage.prompt_tokens,
|
|
106
|
+
output: chunk.usage.completion_tokens,
|
|
107
|
+
...(typeof cached === "number" ? { cachedInput: cached } : {}),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
const choice = chunk.choices[0];
|
|
111
|
+
if (!choice)
|
|
112
|
+
continue;
|
|
113
|
+
const reasoning = choice.delta.reasoning_content;
|
|
114
|
+
if (typeof reasoning === "string" && reasoning)
|
|
115
|
+
yield { type: "reasoning_delta", delta: reasoning };
|
|
116
|
+
if (choice.delta.content) {
|
|
117
|
+
text += choice.delta.content;
|
|
118
|
+
yield { type: "text_delta", delta: choice.delta.content };
|
|
119
|
+
}
|
|
120
|
+
if (choice.delta.refusal)
|
|
121
|
+
throw new Error(`Provider refusal: ${choice.delta.refusal}`);
|
|
122
|
+
for (const part of choice.delta.tool_calls ?? []) {
|
|
123
|
+
const c = calls.get(part.index) ?? { id: "", name: "", arguments: "" };
|
|
124
|
+
if (part.id)
|
|
125
|
+
c.id = part.id;
|
|
126
|
+
if (part.function?.name)
|
|
127
|
+
c.name += part.function.name;
|
|
128
|
+
if (part.function?.arguments)
|
|
129
|
+
c.arguments += part.function.arguments;
|
|
130
|
+
calls.set(part.index, c);
|
|
131
|
+
}
|
|
132
|
+
if (choice.finish_reason)
|
|
133
|
+
finish = choice.finish_reason;
|
|
134
|
+
}
|
|
135
|
+
if (finish !== "stop" && finish !== "tool_calls" && finish !== "length")
|
|
136
|
+
throw new Error(`Provider response incomplete: ${finish ?? "stream ended"}`);
|
|
137
|
+
const completed = [...calls.entries()].sort(([a], [b]) => a - b).map(([, c]) => c);
|
|
138
|
+
// A response cut by the output limit is reported as it is (`truncated: true`), even when it
|
|
139
|
+
// has no text or its tool calls are partial: the host decides whether to recover, and never
|
|
140
|
+
// executes a call whose arguments are incomplete.
|
|
141
|
+
if (finish !== "length" && completed.some((c) => !c.id || !c.name))
|
|
142
|
+
throw new Error("Incomplete tool call");
|
|
143
|
+
yield {
|
|
144
|
+
type: "completed",
|
|
145
|
+
message: {
|
|
146
|
+
role: "assistant",
|
|
147
|
+
text,
|
|
148
|
+
calls: completed,
|
|
149
|
+
...(finish === "length" ? { truncated: true } : {}),
|
|
150
|
+
},
|
|
151
|
+
usage,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
async *responses(request) {
|
|
155
|
+
const input = [];
|
|
156
|
+
for (const m of request.messages) {
|
|
157
|
+
if (m.role === "user") {
|
|
158
|
+
if (m.attachments?.length) {
|
|
159
|
+
const content = [];
|
|
160
|
+
if (m.text.trim())
|
|
161
|
+
content.push({ type: "input_text", text: m.text });
|
|
162
|
+
for (const a of m.attachments)
|
|
163
|
+
content.push({ type: "input_image", image_url: dataUrl(a), detail: "auto" });
|
|
164
|
+
input.push({ role: "user", content });
|
|
165
|
+
}
|
|
166
|
+
else
|
|
167
|
+
input.push({ role: "user", content: m.text });
|
|
168
|
+
}
|
|
169
|
+
else if (m.role === "tool")
|
|
170
|
+
input.push({
|
|
171
|
+
type: "function_call_output",
|
|
172
|
+
call_id: m.callId,
|
|
173
|
+
output: JSON.stringify(m.result),
|
|
174
|
+
});
|
|
175
|
+
else if (m.providerData)
|
|
176
|
+
input.push(...m.providerData);
|
|
177
|
+
else
|
|
178
|
+
throw new Error("Missing provider continuation data for Responses session");
|
|
179
|
+
}
|
|
180
|
+
const stream = await this.#client.responses.create({
|
|
181
|
+
model: request.model || this.model,
|
|
182
|
+
instructions: request.instructions,
|
|
183
|
+
input,
|
|
184
|
+
stream: true,
|
|
185
|
+
store: false,
|
|
186
|
+
include: ["reasoning.encrypted_content"],
|
|
187
|
+
max_output_tokens: request.maxOutputTokens,
|
|
188
|
+
tools: [
|
|
189
|
+
...request.tools.map((t) => ({
|
|
190
|
+
type: "function",
|
|
191
|
+
name: t.name,
|
|
192
|
+
description: t.description,
|
|
193
|
+
parameters: t.inputSchema,
|
|
194
|
+
strict: false,
|
|
195
|
+
})),
|
|
196
|
+
...(request.nativeTools ?? []),
|
|
197
|
+
],
|
|
198
|
+
}, { signal: request.signal });
|
|
199
|
+
let complete = false;
|
|
200
|
+
for await (const event of stream) {
|
|
201
|
+
if (event.type === "response.output_text.delta")
|
|
202
|
+
yield { type: "text_delta", delta: event.delta };
|
|
203
|
+
if (event.type === "response.reasoning_summary_text.delta")
|
|
204
|
+
yield { type: "reasoning_delta", delta: event.delta };
|
|
205
|
+
if (event.type === "response.failed" || event.type === "error")
|
|
206
|
+
throw new Error(`Provider response failed: ${event.type}`);
|
|
207
|
+
if (event.type === "response.incomplete") {
|
|
208
|
+
complete = true;
|
|
209
|
+
const r = event.response;
|
|
210
|
+
const calls = r.output
|
|
211
|
+
.filter((x) => x.type === "function_call")
|
|
212
|
+
.map((x) => ({ id: x.call_id, name: x.name, arguments: x.arguments }));
|
|
213
|
+
const text = r.output
|
|
214
|
+
.filter((x) => x.type === "message")
|
|
215
|
+
.flatMap((x) => x.content)
|
|
216
|
+
.filter((x) => x.type === "output_text")
|
|
217
|
+
.map((x) => x.text)
|
|
218
|
+
.join("");
|
|
219
|
+
// Reported as truncated even when empty or with partial calls: the host recovers.
|
|
220
|
+
yield {
|
|
221
|
+
type: "completed",
|
|
222
|
+
message: { role: "assistant", text, calls, providerData: r.output, truncated: true },
|
|
223
|
+
usage: r.usage
|
|
224
|
+
? {
|
|
225
|
+
input: r.usage.input_tokens,
|
|
226
|
+
output: r.usage.output_tokens,
|
|
227
|
+
...(typeof r.usage.input_tokens_details?.cached_tokens === "number"
|
|
228
|
+
? { cachedInput: r.usage.input_tokens_details.cached_tokens }
|
|
229
|
+
: {}),
|
|
230
|
+
}
|
|
231
|
+
: undefined,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
if (event.type === "response.completed") {
|
|
235
|
+
complete = true;
|
|
236
|
+
const r = event.response;
|
|
237
|
+
const calls = r.output
|
|
238
|
+
.filter((x) => x.type === "function_call")
|
|
239
|
+
.map((x) => ({ id: x.call_id, name: x.name, arguments: x.arguments }));
|
|
240
|
+
const text = r.output
|
|
241
|
+
.filter((x) => x.type === "message")
|
|
242
|
+
.flatMap((x) => x.content)
|
|
243
|
+
.filter((x) => x.type === "output_text")
|
|
244
|
+
.map((x) => x.text)
|
|
245
|
+
.join("");
|
|
246
|
+
yield {
|
|
247
|
+
type: "completed",
|
|
248
|
+
message: { role: "assistant", text, calls, providerData: r.output },
|
|
249
|
+
usage: r.usage
|
|
250
|
+
? {
|
|
251
|
+
input: r.usage.input_tokens,
|
|
252
|
+
output: r.usage.output_tokens,
|
|
253
|
+
...(typeof r.usage.input_tokens_details?.cached_tokens === "number"
|
|
254
|
+
? { cachedInput: r.usage.input_tokens_details.cached_tokens }
|
|
255
|
+
: {}),
|
|
256
|
+
}
|
|
257
|
+
: undefined,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (!complete)
|
|
262
|
+
throw new Error("Responses stream ended before completion");
|
|
263
|
+
}
|
|
264
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads this plugin package's version at runtime so plugin metadata, user-agent defaults and CLI
|
|
3
|
+
* reports stay in sync with the published package without a build-time constant to update.
|
|
4
|
+
* The manifest is resolved relative to the module (`src/version.ts` → `../package.json` in the
|
|
5
|
+
* source tree; `dist/version.js` → `../package.json` of the installed package). Standalone
|
|
6
|
+
* binaries inject the version at build time via ALISIO_PACKAGE_VERSION (scripts/binary-build.ts).
|
|
7
|
+
*/
|
|
8
|
+
export declare function loadVersion(fromHere: string): string;
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
/**
|
|
5
|
+
* Fallback used when neither the build-time injection nor the package manifest is readable
|
|
6
|
+
* (e.g. an unpackaged embed). Keep it a development marker, never a publish literal, so it
|
|
7
|
+
* cannot silently desync from the published package.
|
|
8
|
+
*/
|
|
9
|
+
const FALLBACK = "dev";
|
|
10
|
+
/**
|
|
11
|
+
* Reads this plugin package's version at runtime so plugin metadata, user-agent defaults and CLI
|
|
12
|
+
* reports stay in sync with the published package without a build-time constant to update.
|
|
13
|
+
* The manifest is resolved relative to the module (`src/version.ts` → `../package.json` in the
|
|
14
|
+
* source tree; `dist/version.js` → `../package.json` of the installed package). Standalone
|
|
15
|
+
* binaries inject the version at build time via ALISIO_PACKAGE_VERSION (scripts/binary-build.ts).
|
|
16
|
+
*/
|
|
17
|
+
export function loadVersion(fromHere) {
|
|
18
|
+
if (process.env.ALISIO_PACKAGE_VERSION)
|
|
19
|
+
return process.env.ALISIO_PACKAGE_VERSION;
|
|
20
|
+
try {
|
|
21
|
+
const manifest = join(dirname(fileURLToPath(fromHere)), "..", "package.json");
|
|
22
|
+
const parsed = JSON.parse(readFileSync(manifest, "utf8"));
|
|
23
|
+
return typeof parsed.version === "string" && parsed.version ? parsed.version : FALLBACK;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return FALLBACK;
|
|
27
|
+
}
|
|
28
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@alisio/plugin-openai-compatible",
|
|
3
|
+
"version": "0.1.0-alpha.10",
|
|
4
|
+
"description": "Built-in OpenAI-compatible model provider for Alisio.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=22.16"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"openai": "7.23.0"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@alisio/sdk": "^0.1.0-alpha.20"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@alisio/sdk": "0.1.0-alpha.20"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public",
|
|
34
|
+
"registry": "https://registry.npmjs.com/"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc -p tsconfig.build.json"
|
|
38
|
+
}
|
|
39
|
+
}
|