@nebulaos/llm-gateway 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/README.md +68 -0
- package/dist/index.d.mts +50 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.js +318 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +287 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# @nebulaos/llm-gateway
|
|
2
|
+
|
|
3
|
+
NebulaOS LLM Gateway provider for the NebulaOS SDK. Provides OpenAI-compatible chat completions through pre-configured routes with automatic fallback, cost tracking, and access control.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @nebulaos/llm-gateway
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { LLMGateway } from "@nebulaos/llm-gateway";
|
|
15
|
+
import { Agent } from "@nebulaos/core";
|
|
16
|
+
|
|
17
|
+
const agent = new Agent({
|
|
18
|
+
name: "assistente",
|
|
19
|
+
model: new LLMGateway({
|
|
20
|
+
apiKey: "your-nebula-api-key",
|
|
21
|
+
baseUrl: "https://your-nebula-instance.com",
|
|
22
|
+
model: "assistente", // Route alias configured in NebulaOS
|
|
23
|
+
logLevel: "debug",
|
|
24
|
+
}),
|
|
25
|
+
instructions: "You are a helpful assistant.",
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Use the agent
|
|
29
|
+
const response = await agent.generate("Hello, how can I help you?");
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Configuration
|
|
33
|
+
|
|
34
|
+
| Option | Type | Required | Description |
|
|
35
|
+
|--------|------|----------|-------------|
|
|
36
|
+
| `apiKey` | `string` | Yes | API key from NebulaOS |
|
|
37
|
+
| `baseUrl` | `string` | No | NebulaOS instance URL (defaults to localhost:4100) |
|
|
38
|
+
| `model` | `string` | Yes | Route alias configured in NebulaOS |
|
|
39
|
+
| `logLevel` | `string` | No | Logger verbosity ("debug", "info", "warn", "error", "none") |
|
|
40
|
+
|
|
41
|
+
## Routes
|
|
42
|
+
|
|
43
|
+
Routes are configured in the NebulaOS UI and provide:
|
|
44
|
+
|
|
45
|
+
- **Automatic fallback**: If primary model fails, automatically tries backup models
|
|
46
|
+
- **Cost tracking**: All usage is automatically tracked and billed
|
|
47
|
+
- **Access control**: API keys can be restricted to specific routes
|
|
48
|
+
- **Rate limiting**: Configurable limits per route and user
|
|
49
|
+
|
|
50
|
+
## Supported Features
|
|
51
|
+
|
|
52
|
+
- ✅ Text chat completions
|
|
53
|
+
- ✅ Streaming responses
|
|
54
|
+
- ✅ Tool/function calling
|
|
55
|
+
- ✅ JSON mode
|
|
56
|
+
- ✅ Multimodal (images)
|
|
57
|
+
- ✅ Automatic cost tracking
|
|
58
|
+
- ✅ Route-based access control
|
|
59
|
+
|
|
60
|
+
## Error Handling
|
|
61
|
+
|
|
62
|
+
The provider will throw errors for:
|
|
63
|
+
- Invalid API keys
|
|
64
|
+
- Routes not found or inactive
|
|
65
|
+
- Access denied to routes
|
|
66
|
+
- Network issues with NebulaOS
|
|
67
|
+
|
|
68
|
+
All errors include appropriate HTTP status codes and messages.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import OpenAIClient, { ClientOptions } from 'openai';
|
|
2
|
+
import { LogLevel, IModel, Message, ToolDefinitionForLLM, GenerateOptions, ProviderResponse, StreamChunk } from '@nebulaos/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* LLM Gateway Provider Configuration
|
|
6
|
+
*/
|
|
7
|
+
interface LLMGatewayConfig {
|
|
8
|
+
/** API Key from NebulaOS */
|
|
9
|
+
apiKey: string;
|
|
10
|
+
/** Base URL of the NebulaOS LLM Gateway */
|
|
11
|
+
baseUrl?: string;
|
|
12
|
+
/** Route alias (e.g., "assistente", "code-review") */
|
|
13
|
+
model: string;
|
|
14
|
+
/** Logger verbosity for gateway calls */
|
|
15
|
+
logLevel?: LogLevel;
|
|
16
|
+
/** Optional OpenAI client options */
|
|
17
|
+
clientOptions?: ClientOptions;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* NebulaOS LLM Gateway Provider
|
|
21
|
+
*
|
|
22
|
+
* Provides access to NebulaOS LLM Gateway routes through an OpenAI-compatible interface.
|
|
23
|
+
* Routes are pre-configured in NebulaOS and provide automatic fallback, cost tracking,
|
|
24
|
+
* and access control.
|
|
25
|
+
*/
|
|
26
|
+
declare class LLMGateway implements IModel {
|
|
27
|
+
providerName: string;
|
|
28
|
+
modelName: string;
|
|
29
|
+
private client;
|
|
30
|
+
private baseUrl;
|
|
31
|
+
private logger;
|
|
32
|
+
capabilities: {
|
|
33
|
+
readonly inputFiles: {
|
|
34
|
+
readonly mimeTypes: readonly ["image/*"];
|
|
35
|
+
readonly sources: readonly ["url", "base64"];
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
constructor(config: LLMGatewayConfig);
|
|
39
|
+
generate(messages: Message[], tools?: ToolDefinitionForLLM[], options?: GenerateOptions): Promise<ProviderResponse>;
|
|
40
|
+
generateStream(messages: Message[], tools?: ToolDefinitionForLLM[], options?: GenerateOptions): AsyncGenerator<StreamChunk>;
|
|
41
|
+
private extractExtraOptions;
|
|
42
|
+
private buildGatewayHeaders;
|
|
43
|
+
protected convertMessages(messages: Message[]): OpenAIClient.Chat.ChatCompletionMessageParam[];
|
|
44
|
+
private convertTools;
|
|
45
|
+
private mapUsage;
|
|
46
|
+
private mapFinishReason;
|
|
47
|
+
private convertContentPart;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export { LLMGateway, type LLMGatewayConfig };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import OpenAIClient, { ClientOptions } from 'openai';
|
|
2
|
+
import { LogLevel, IModel, Message, ToolDefinitionForLLM, GenerateOptions, ProviderResponse, StreamChunk } from '@nebulaos/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* LLM Gateway Provider Configuration
|
|
6
|
+
*/
|
|
7
|
+
interface LLMGatewayConfig {
|
|
8
|
+
/** API Key from NebulaOS */
|
|
9
|
+
apiKey: string;
|
|
10
|
+
/** Base URL of the NebulaOS LLM Gateway */
|
|
11
|
+
baseUrl?: string;
|
|
12
|
+
/** Route alias (e.g., "assistente", "code-review") */
|
|
13
|
+
model: string;
|
|
14
|
+
/** Logger verbosity for gateway calls */
|
|
15
|
+
logLevel?: LogLevel;
|
|
16
|
+
/** Optional OpenAI client options */
|
|
17
|
+
clientOptions?: ClientOptions;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* NebulaOS LLM Gateway Provider
|
|
21
|
+
*
|
|
22
|
+
* Provides access to NebulaOS LLM Gateway routes through an OpenAI-compatible interface.
|
|
23
|
+
* Routes are pre-configured in NebulaOS and provide automatic fallback, cost tracking,
|
|
24
|
+
* and access control.
|
|
25
|
+
*/
|
|
26
|
+
declare class LLMGateway implements IModel {
|
|
27
|
+
providerName: string;
|
|
28
|
+
modelName: string;
|
|
29
|
+
private client;
|
|
30
|
+
private baseUrl;
|
|
31
|
+
private logger;
|
|
32
|
+
capabilities: {
|
|
33
|
+
readonly inputFiles: {
|
|
34
|
+
readonly mimeTypes: readonly ["image/*"];
|
|
35
|
+
readonly sources: readonly ["url", "base64"];
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
constructor(config: LLMGatewayConfig);
|
|
39
|
+
generate(messages: Message[], tools?: ToolDefinitionForLLM[], options?: GenerateOptions): Promise<ProviderResponse>;
|
|
40
|
+
generateStream(messages: Message[], tools?: ToolDefinitionForLLM[], options?: GenerateOptions): AsyncGenerator<StreamChunk>;
|
|
41
|
+
private extractExtraOptions;
|
|
42
|
+
private buildGatewayHeaders;
|
|
43
|
+
protected convertMessages(messages: Message[]): OpenAIClient.Chat.ChatCompletionMessageParam[];
|
|
44
|
+
private convertTools;
|
|
45
|
+
private mapUsage;
|
|
46
|
+
private mapFinishReason;
|
|
47
|
+
private convertContentPart;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export { LLMGateway, type LLMGatewayConfig };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
LLMGateway: () => LLMGateway
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(index_exports);
|
|
36
|
+
var import_openai = __toESM(require("openai"));
|
|
37
|
+
var import_node_crypto = require("crypto");
|
|
38
|
+
var import_core = require("@nebulaos/core");
|
|
39
|
+
var LLMGateway = class {
|
|
40
|
+
providerName = "llm-gateway";
|
|
41
|
+
modelName;
|
|
42
|
+
client;
|
|
43
|
+
baseUrl;
|
|
44
|
+
logger;
|
|
45
|
+
capabilities = {
|
|
46
|
+
inputFiles: {
|
|
47
|
+
mimeTypes: ["image/*"],
|
|
48
|
+
sources: ["url", "base64"]
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
constructor(config) {
|
|
52
|
+
this.modelName = config.model;
|
|
53
|
+
this.baseUrl = config.baseUrl || "http://localhost:4100";
|
|
54
|
+
const baseURL = this.baseUrl.endsWith("/v1") ? this.baseUrl : `${this.baseUrl}/v1`;
|
|
55
|
+
this.logger = new import_core.ConsoleLogger(config.logLevel || "info", "nebulaos/llm-gateway");
|
|
56
|
+
this.client = new import_openai.default({
|
|
57
|
+
apiKey: config.apiKey,
|
|
58
|
+
baseURL,
|
|
59
|
+
...config.clientOptions
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
async generate(messages, tools, options) {
|
|
63
|
+
const model = `route:${this.modelName}`;
|
|
64
|
+
const headers = this.buildGatewayHeaders();
|
|
65
|
+
this.logger.debug("LLM Gateway request", {
|
|
66
|
+
model,
|
|
67
|
+
baseUrl: this.baseUrl,
|
|
68
|
+
stream: false,
|
|
69
|
+
messageCount: messages.length,
|
|
70
|
+
toolCount: tools?.length ?? 0
|
|
71
|
+
});
|
|
72
|
+
try {
|
|
73
|
+
const response = await this.client.chat.completions.create(
|
|
74
|
+
{
|
|
75
|
+
model,
|
|
76
|
+
messages: this.convertMessages(messages),
|
|
77
|
+
tools: this.convertTools(tools),
|
|
78
|
+
response_format: options?.responseFormat?.type === "json" ? options.responseFormat.schema ? {
|
|
79
|
+
type: "json_schema",
|
|
80
|
+
json_schema: { name: "response", schema: options.responseFormat.schema }
|
|
81
|
+
} : { type: "json_object" } : void 0,
|
|
82
|
+
...this.extractExtraOptions(options)
|
|
83
|
+
},
|
|
84
|
+
{ headers }
|
|
85
|
+
);
|
|
86
|
+
this.logger.debug("LLM Gateway response", {
|
|
87
|
+
model,
|
|
88
|
+
finishReason: response.choices?.[0]?.finish_reason,
|
|
89
|
+
hasUsage: Boolean(response.usage)
|
|
90
|
+
});
|
|
91
|
+
const choice = response.choices[0];
|
|
92
|
+
const message = choice.message;
|
|
93
|
+
return {
|
|
94
|
+
content: message.content || "",
|
|
95
|
+
toolCalls: message.tool_calls?.map((tc) => ({
|
|
96
|
+
id: tc.id,
|
|
97
|
+
type: "function",
|
|
98
|
+
function: {
|
|
99
|
+
name: tc.function.name,
|
|
100
|
+
arguments: tc.function.arguments
|
|
101
|
+
}
|
|
102
|
+
})),
|
|
103
|
+
finishReason: this.mapFinishReason(choice.finish_reason),
|
|
104
|
+
usage: this.mapUsage(response.usage)
|
|
105
|
+
};
|
|
106
|
+
} catch (error) {
|
|
107
|
+
this.logger.error("LLM Gateway request failed", error, void 0, void 0);
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async *generateStream(messages, tools, options) {
|
|
112
|
+
const model = `route:${this.modelName}`;
|
|
113
|
+
const headers = this.buildGatewayHeaders();
|
|
114
|
+
this.logger.debug("LLM Gateway stream request", {
|
|
115
|
+
model,
|
|
116
|
+
baseUrl: this.baseUrl,
|
|
117
|
+
stream: true,
|
|
118
|
+
messageCount: messages.length,
|
|
119
|
+
toolCount: tools?.length ?? 0
|
|
120
|
+
});
|
|
121
|
+
let stream;
|
|
122
|
+
try {
|
|
123
|
+
stream = await this.client.chat.completions.create(
|
|
124
|
+
{
|
|
125
|
+
model,
|
|
126
|
+
messages: this.convertMessages(messages),
|
|
127
|
+
tools: this.convertTools(tools),
|
|
128
|
+
stream: true,
|
|
129
|
+
stream_options: { include_usage: true },
|
|
130
|
+
response_format: options?.responseFormat?.type === "json" ? options.responseFormat.schema ? {
|
|
131
|
+
type: "json_schema",
|
|
132
|
+
json_schema: { name: "response", schema: options.responseFormat.schema }
|
|
133
|
+
} : { type: "json_object" } : void 0,
|
|
134
|
+
...this.extractExtraOptions(options)
|
|
135
|
+
},
|
|
136
|
+
{ headers }
|
|
137
|
+
);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
this.logger.error("LLM Gateway stream request failed", error, void 0, void 0);
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
for await (const chunk of stream) {
|
|
144
|
+
if (chunk.usage) {
|
|
145
|
+
yield {
|
|
146
|
+
type: "finish",
|
|
147
|
+
reason: "stop",
|
|
148
|
+
usage: this.mapUsage(chunk.usage)
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
const choice = chunk.choices?.[0];
|
|
152
|
+
if (!choice) continue;
|
|
153
|
+
if (choice.finish_reason) {
|
|
154
|
+
yield {
|
|
155
|
+
type: "finish",
|
|
156
|
+
reason: this.mapFinishReason(choice.finish_reason)
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const delta = choice.delta;
|
|
160
|
+
if (!delta) continue;
|
|
161
|
+
if (delta.content) {
|
|
162
|
+
yield { type: "content_delta", delta: delta.content };
|
|
163
|
+
}
|
|
164
|
+
if (delta.tool_calls) {
|
|
165
|
+
for (const tc of delta.tool_calls) {
|
|
166
|
+
if (tc.id && tc.function?.name) {
|
|
167
|
+
yield {
|
|
168
|
+
type: "tool_call_start",
|
|
169
|
+
index: tc.index,
|
|
170
|
+
id: tc.id,
|
|
171
|
+
name: tc.function.name
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if (tc.function?.arguments) {
|
|
175
|
+
yield {
|
|
176
|
+
type: "tool_call_delta",
|
|
177
|
+
index: tc.index,
|
|
178
|
+
args: tc.function.arguments
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
} catch (error) {
|
|
185
|
+
this.logger.error("LLM Gateway stream failed", error, void 0, void 0);
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// ==========================================================================
|
|
190
|
+
// Helpers (copied from OpenAI provider)
|
|
191
|
+
// ==========================================================================
|
|
192
|
+
extractExtraOptions(options) {
|
|
193
|
+
if (!options) return {};
|
|
194
|
+
const { responseFormat, ...rest } = options;
|
|
195
|
+
return rest;
|
|
196
|
+
}
|
|
197
|
+
buildGatewayHeaders() {
|
|
198
|
+
const headers = {
|
|
199
|
+
"x-request-id": (0, import_node_crypto.randomUUID)()
|
|
200
|
+
};
|
|
201
|
+
const exec = import_core.ExecutionContext.getOrUndefined();
|
|
202
|
+
if (exec?.executionId) {
|
|
203
|
+
headers["x-execution-id"] = exec.executionId;
|
|
204
|
+
}
|
|
205
|
+
const ctx = import_core.Tracing.getContext();
|
|
206
|
+
if (ctx) {
|
|
207
|
+
headers.traceparent = `00-${ctx.traceId}-${ctx.spanId}-01`;
|
|
208
|
+
} else {
|
|
209
|
+
const traceId = (0, import_node_crypto.randomBytes)(16).toString("hex");
|
|
210
|
+
const spanId = (0, import_node_crypto.randomBytes)(8).toString("hex");
|
|
211
|
+
headers.traceparent = `00-${traceId}-${spanId}-01`;
|
|
212
|
+
}
|
|
213
|
+
return headers;
|
|
214
|
+
}
|
|
215
|
+
convertMessages(messages) {
|
|
216
|
+
const allowedToolCallIds = /* @__PURE__ */ new Set();
|
|
217
|
+
return messages.flatMap((m) => {
|
|
218
|
+
if (m.role === "tool") {
|
|
219
|
+
if (!m.tool_call_id || !allowedToolCallIds.has(m.tool_call_id)) {
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
role: "tool",
|
|
224
|
+
tool_call_id: m.tool_call_id,
|
|
225
|
+
content: typeof m.content === "string" ? m.content : JSON.stringify(m.content)
|
|
226
|
+
// Tool output usually string
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
if (m.role === "assistant") {
|
|
230
|
+
let assistantContent = null;
|
|
231
|
+
if (typeof m.content === "string") {
|
|
232
|
+
assistantContent = m.content;
|
|
233
|
+
}
|
|
234
|
+
if (!assistantContent && (!m.tool_calls || m.tool_calls.length === 0)) {
|
|
235
|
+
assistantContent = "";
|
|
236
|
+
}
|
|
237
|
+
if (m.tool_calls) {
|
|
238
|
+
m.tool_calls.forEach((tc) => allowedToolCallIds.add(tc.id));
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
role: "assistant",
|
|
242
|
+
content: assistantContent,
|
|
243
|
+
tool_calls: m.tool_calls?.map((tc) => ({
|
|
244
|
+
id: tc.id,
|
|
245
|
+
type: "function",
|
|
246
|
+
function: {
|
|
247
|
+
name: tc.function.name,
|
|
248
|
+
arguments: tc.function.arguments
|
|
249
|
+
}
|
|
250
|
+
}))
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
const content = Array.isArray(
|
|
254
|
+
m.content
|
|
255
|
+
) ? m.content.map((part) => this.convertContentPart(part)) : m.content || "";
|
|
256
|
+
return {
|
|
257
|
+
role: m.role,
|
|
258
|
+
content,
|
|
259
|
+
name: m.name
|
|
260
|
+
};
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
convertTools(tools) {
|
|
264
|
+
if (!tools || tools.length === 0) return void 0;
|
|
265
|
+
return tools.map((t) => ({
|
|
266
|
+
type: "function",
|
|
267
|
+
function: {
|
|
268
|
+
name: t.function.name,
|
|
269
|
+
description: t.function.description,
|
|
270
|
+
parameters: t.function.parameters
|
|
271
|
+
}
|
|
272
|
+
}));
|
|
273
|
+
}
|
|
274
|
+
mapUsage(usage) {
|
|
275
|
+
if (!usage) return void 0;
|
|
276
|
+
return {
|
|
277
|
+
promptTokens: usage.prompt_tokens,
|
|
278
|
+
completionTokens: usage.completion_tokens,
|
|
279
|
+
totalTokens: usage.total_tokens,
|
|
280
|
+
reasoningTokens: usage.completion_tokens_details?.reasoning_tokens
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
mapFinishReason(reason) {
|
|
284
|
+
switch (reason) {
|
|
285
|
+
case "stop":
|
|
286
|
+
return "stop";
|
|
287
|
+
case "length":
|
|
288
|
+
return "length";
|
|
289
|
+
case "tool_calls":
|
|
290
|
+
return "tool_calls";
|
|
291
|
+
case "content_filter":
|
|
292
|
+
return "content_filter";
|
|
293
|
+
default:
|
|
294
|
+
return void 0;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
convertContentPart(part) {
|
|
298
|
+
if (part.type === "text") return { type: "text", text: part.text };
|
|
299
|
+
if (part.type === "file") {
|
|
300
|
+
const { mimeType, source } = part.file;
|
|
301
|
+
if (!mimeType.startsWith("image/")) {
|
|
302
|
+
throw new Error(`LLM Gateway: file mimeType '${mimeType}' is not supported yet`);
|
|
303
|
+
}
|
|
304
|
+
const url = source.type === "url" ? source.url : `data:${mimeType};base64,${source.base64}`;
|
|
305
|
+
return { type: "image_url", image_url: { url } };
|
|
306
|
+
}
|
|
307
|
+
if (part.type === "image_url") {
|
|
308
|
+
return { type: "image_url", image_url: { url: part.image_url.url } };
|
|
309
|
+
}
|
|
310
|
+
const _exhaustive = part;
|
|
311
|
+
throw new Error(`Unsupported content type: ${_exhaustive.type}`);
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
315
|
+
0 && (module.exports = {
|
|
316
|
+
LLMGateway
|
|
317
|
+
});
|
|
318
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import OpenAIClient, { ClientOptions } from \"openai\";\nimport { randomBytes, randomUUID } from \"node:crypto\";\nimport {\n IModel,\n Message,\n ProviderResponse,\n StreamChunk,\n ToolDefinitionForLLM,\n GenerateOptions,\n TokenUsage,\n ContentPart,\n ConsoleLogger,\n type LogLevel,\n ExecutionContext,\n Tracing,\n} from \"@nebulaos/core\";\n\n/**\n * LLM Gateway Provider Configuration\n */\nexport interface LLMGatewayConfig {\n /** API Key from NebulaOS */\n apiKey: string;\n /** Base URL of the NebulaOS LLM Gateway */\n baseUrl?: string;\n /** Route alias (e.g., \"assistente\", \"code-review\") */\n model: string;\n /** Logger verbosity for gateway calls */\n logLevel?: LogLevel;\n /** Optional OpenAI client options */\n clientOptions?: ClientOptions;\n}\n\n/**\n * NebulaOS LLM Gateway Provider\n *\n * Provides access to NebulaOS LLM Gateway routes through an OpenAI-compatible interface.\n * Routes are pre-configured in NebulaOS and provide automatic fallback, cost tracking,\n * and access control.\n */\nexport class LLMGateway implements IModel {\n providerName = \"llm-gateway\";\n modelName: string;\n private client: OpenAIClient;\n private baseUrl: string;\n private logger: ConsoleLogger;\n\n capabilities = {\n inputFiles: {\n mimeTypes: [\"image/*\"],\n sources: [\"url\", \"base64\"] as const,\n },\n } as const;\n\n constructor(config: LLMGatewayConfig) {\n this.modelName = config.model;\n\n this.baseUrl = config.baseUrl || \"http://localhost:4100\";\n const baseURL = this.baseUrl.endsWith(\"/v1\") ? this.baseUrl : `${this.baseUrl}/v1`;\n this.logger = new ConsoleLogger(config.logLevel || \"info\", \"nebulaos/llm-gateway\");\n\n this.client = new OpenAIClient({\n apiKey: config.apiKey,\n baseURL,\n ...config.clientOptions,\n });\n }\n\n async generate(\n messages: Message[],\n tools?: ToolDefinitionForLLM[],\n options?: GenerateOptions,\n ): Promise<ProviderResponse> {\n const model = `route:${this.modelName}`;\n const headers = this.buildGatewayHeaders();\n this.logger.debug(\"LLM Gateway request\", {\n model,\n baseUrl: this.baseUrl,\n stream: false,\n messageCount: messages.length,\n toolCount: tools?.length ?? 0,\n });\n\n try {\n const response = await this.client.chat.completions.create(\n {\n model,\n messages: this.convertMessages(messages),\n tools: this.convertTools(tools),\n response_format:\n options?.responseFormat?.type === \"json\"\n ? options.responseFormat.schema\n ? {\n type: \"json_schema\",\n json_schema: { name: \"response\", schema: options.responseFormat.schema as any },\n }\n : { type: \"json_object\" }\n : undefined,\n ...this.extractExtraOptions(options),\n },\n { headers },\n );\n\n this.logger.debug(\"LLM Gateway response\", {\n model,\n finishReason: response.choices?.[0]?.finish_reason,\n hasUsage: Boolean(response.usage),\n });\n\n const choice = response.choices[0];\n const message = choice.message;\n\n return {\n content: message.content || \"\",\n toolCalls: message.tool_calls?.map((tc) => ({\n id: tc.id,\n type: \"function\",\n function: {\n name: tc.function.name,\n arguments: tc.function.arguments,\n },\n })),\n finishReason: this.mapFinishReason(choice.finish_reason),\n usage: this.mapUsage(response.usage),\n };\n } catch (error) {\n this.logger.error(\"LLM Gateway request failed\", error, undefined, undefined);\n throw error;\n }\n }\n\n async *generateStream(\n messages: Message[],\n tools?: ToolDefinitionForLLM[],\n options?: GenerateOptions,\n ): AsyncGenerator<StreamChunk> {\n const model = `route:${this.modelName}`;\n const headers = this.buildGatewayHeaders();\n this.logger.debug(\"LLM Gateway stream request\", {\n model,\n baseUrl: this.baseUrl,\n stream: true,\n messageCount: messages.length,\n toolCount: tools?.length ?? 0,\n });\n\n let stream;\n try {\n stream = await this.client.chat.completions.create(\n {\n model,\n messages: this.convertMessages(messages),\n tools: this.convertTools(tools),\n stream: true,\n stream_options: { include_usage: true },\n response_format:\n options?.responseFormat?.type === \"json\"\n ? options.responseFormat.schema\n ? {\n type: \"json_schema\",\n json_schema: { name: \"response\", schema: options.responseFormat.schema as any },\n }\n : { type: \"json_object\" }\n : undefined,\n ...this.extractExtraOptions(options),\n },\n { headers },\n );\n } catch (error) {\n this.logger.error(\"LLM Gateway stream request failed\", error, undefined, undefined);\n throw error;\n }\n\n try {\n for await (const chunk of stream) {\n if (chunk.usage) {\n yield {\n type: \"finish\",\n reason: \"stop\",\n usage: this.mapUsage(chunk.usage),\n };\n }\n\n const choice = chunk.choices?.[0];\n if (!choice) continue;\n\n if (choice.finish_reason) {\n yield {\n type: \"finish\",\n reason: this.mapFinishReason(choice.finish_reason),\n };\n }\n\n const delta = choice.delta;\n if (!delta) continue;\n\n if (delta.content) {\n yield { type: \"content_delta\", delta: delta.content };\n }\n\n if (delta.tool_calls) {\n for (const tc of delta.tool_calls) {\n if (tc.id && tc.function?.name) {\n yield {\n type: \"tool_call_start\",\n index: tc.index,\n id: tc.id,\n name: tc.function.name,\n };\n }\n\n if (tc.function?.arguments) {\n yield {\n type: \"tool_call_delta\",\n index: tc.index,\n args: tc.function.arguments,\n };\n }\n }\n }\n }\n } catch (error) {\n this.logger.error(\"LLM Gateway stream failed\", error, undefined, undefined);\n throw error;\n }\n }\n\n // ==========================================================================\n // Helpers (copied from OpenAI provider)\n // ==========================================================================\n\n private extractExtraOptions(options?: GenerateOptions): Record<string, any> {\n if (!options) return {};\n const { responseFormat, ...rest } = options;\n return rest;\n }\n\n private buildGatewayHeaders(): Record<string, string> {\n const headers: Record<string, string> = {\n \"x-request-id\": randomUUID(),\n };\n\n const exec = ExecutionContext.getOrUndefined();\n if (exec?.executionId) {\n headers[\"x-execution-id\"] = exec.executionId;\n }\n\n const ctx = Tracing.getContext();\n if (ctx) {\n // IDs já estão em formato W3C (hex)\n headers.traceparent = `00-${ctx.traceId}-${ctx.spanId}-01`;\n } else {\n // Still emit a root trace context so the gateway can correlate requests by traceId.\n const traceId = randomBytes(16).toString(\"hex\");\n const spanId = randomBytes(8).toString(\"hex\");\n headers.traceparent = `00-${traceId}-${spanId}-01`;\n }\n\n return headers;\n }\n\n protected convertMessages(messages: Message[]): OpenAIClient.Chat.ChatCompletionMessageParam[] {\n // Ensure tools have a preceding assistant message with tool_calls\n const allowedToolCallIds = new Set<string>();\n\n return messages.flatMap((m) => {\n if (m.role === \"tool\") {\n // Skip orphan tool messages (no preceding tool_calls)\n if (!m.tool_call_id || !allowedToolCallIds.has(m.tool_call_id)) {\n return [];\n }\n\n return {\n role: \"tool\",\n tool_call_id: m.tool_call_id!,\n content: typeof m.content === \"string\" ? m.content : JSON.stringify(m.content), // Tool output usually string\n };\n }\n\n if (m.role === \"assistant\") {\n // OpenAI rules:\n // - content is required (string | null)\n // - if tool_calls is present, content can be null\n // - if tool_calls is NOT present, content must be string (cannot be null/empty if strict, but usually empty string is fine)\n\n let assistantContent: string | null = null;\n\n if (typeof m.content === \"string\") {\n assistantContent = m.content;\n }\n\n // If content is null/empty AND no tool_calls, force empty string to avoid API error\n if (!assistantContent && (!m.tool_calls || m.tool_calls.length === 0)) {\n assistantContent = \"\";\n }\n\n if (m.tool_calls) {\n m.tool_calls.forEach((tc) => allowedToolCallIds.add(tc.id));\n }\n\n return {\n role: \"assistant\",\n content: assistantContent,\n tool_calls: m.tool_calls?.map((tc) => ({\n id: tc.id,\n type: \"function\",\n function: {\n name: tc.function.name,\n arguments: tc.function.arguments,\n },\n })),\n };\n }\n\n // User / System with potential multimodal content\n const content: OpenAIClient.Chat.ChatCompletionContentPart[] | string = Array.isArray(\n m.content,\n )\n ? m.content.map((part) => this.convertContentPart(part))\n : m.content || \"\";\n\n return {\n role: m.role as \"system\" | \"user\",\n content,\n name: m.name,\n } as any;\n });\n }\n\n private convertTools(\n tools?: ToolDefinitionForLLM[],\n ): OpenAIClient.Chat.ChatCompletionTool[] | undefined {\n if (!tools || tools.length === 0) return undefined;\n return tools.map((t) => ({\n type: \"function\",\n function: {\n name: t.function.name,\n description: t.function.description,\n parameters: t.function.parameters,\n },\n }));\n }\n\n private mapUsage(usage?: OpenAIClient.CompletionUsage): TokenUsage | undefined {\n if (!usage) return undefined;\n return {\n promptTokens: usage.prompt_tokens,\n completionTokens: usage.completion_tokens,\n totalTokens: usage.total_tokens,\n reasoningTokens: (usage as any).completion_tokens_details?.reasoning_tokens,\n };\n }\n\n private mapFinishReason(reason: string | null): ProviderResponse[\"finishReason\"] {\n switch (reason) {\n case \"stop\":\n return \"stop\";\n case \"length\":\n return \"length\";\n case \"tool_calls\":\n return \"tool_calls\";\n case \"content_filter\":\n return \"content_filter\";\n default:\n return undefined;\n }\n }\n\n private convertContentPart(part: ContentPart): OpenAIClient.Chat.ChatCompletionContentPart {\n if (part.type === \"text\") return { type: \"text\", text: part.text };\n\n if (part.type === \"file\") {\n const { mimeType, source } = part.file;\n if (!mimeType.startsWith(\"image/\")) {\n throw new Error(`LLM Gateway: file mimeType '${mimeType}' is not supported yet`);\n }\n\n const url = source.type === \"url\" ? source.url : `data:${mimeType};base64,${source.base64}`;\n\n return { type: \"image_url\", image_url: { url } };\n }\n\n if (part.type === \"image_url\") {\n return { type: \"image_url\", image_url: { url: part.image_url.url } };\n }\n\n // Exhaustive check - should never reach here with proper ContentPart\n const _exhaustive: never = part;\n throw new Error(`Unsupported content type: ${(_exhaustive as any).type}`);\n }\n}\n"],"mappings":"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","names":["OpenAIClient"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import OpenAIClient from "openai";
|
|
3
|
+
import { randomBytes, randomUUID } from "crypto";
|
|
4
|
+
import {
|
|
5
|
+
ConsoleLogger,
|
|
6
|
+
ExecutionContext,
|
|
7
|
+
Tracing
|
|
8
|
+
} from "@nebulaos/core";
|
|
9
|
+
var LLMGateway = class {
|
|
10
|
+
providerName = "llm-gateway";
|
|
11
|
+
modelName;
|
|
12
|
+
client;
|
|
13
|
+
baseUrl;
|
|
14
|
+
logger;
|
|
15
|
+
capabilities = {
|
|
16
|
+
inputFiles: {
|
|
17
|
+
mimeTypes: ["image/*"],
|
|
18
|
+
sources: ["url", "base64"]
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
constructor(config) {
|
|
22
|
+
this.modelName = config.model;
|
|
23
|
+
this.baseUrl = config.baseUrl || "http://localhost:4100";
|
|
24
|
+
const baseURL = this.baseUrl.endsWith("/v1") ? this.baseUrl : `${this.baseUrl}/v1`;
|
|
25
|
+
this.logger = new ConsoleLogger(config.logLevel || "info", "nebulaos/llm-gateway");
|
|
26
|
+
this.client = new OpenAIClient({
|
|
27
|
+
apiKey: config.apiKey,
|
|
28
|
+
baseURL,
|
|
29
|
+
...config.clientOptions
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
async generate(messages, tools, options) {
|
|
33
|
+
const model = `route:${this.modelName}`;
|
|
34
|
+
const headers = this.buildGatewayHeaders();
|
|
35
|
+
this.logger.debug("LLM Gateway request", {
|
|
36
|
+
model,
|
|
37
|
+
baseUrl: this.baseUrl,
|
|
38
|
+
stream: false,
|
|
39
|
+
messageCount: messages.length,
|
|
40
|
+
toolCount: tools?.length ?? 0
|
|
41
|
+
});
|
|
42
|
+
try {
|
|
43
|
+
const response = await this.client.chat.completions.create(
|
|
44
|
+
{
|
|
45
|
+
model,
|
|
46
|
+
messages: this.convertMessages(messages),
|
|
47
|
+
tools: this.convertTools(tools),
|
|
48
|
+
response_format: options?.responseFormat?.type === "json" ? options.responseFormat.schema ? {
|
|
49
|
+
type: "json_schema",
|
|
50
|
+
json_schema: { name: "response", schema: options.responseFormat.schema }
|
|
51
|
+
} : { type: "json_object" } : void 0,
|
|
52
|
+
...this.extractExtraOptions(options)
|
|
53
|
+
},
|
|
54
|
+
{ headers }
|
|
55
|
+
);
|
|
56
|
+
this.logger.debug("LLM Gateway response", {
|
|
57
|
+
model,
|
|
58
|
+
finishReason: response.choices?.[0]?.finish_reason,
|
|
59
|
+
hasUsage: Boolean(response.usage)
|
|
60
|
+
});
|
|
61
|
+
const choice = response.choices[0];
|
|
62
|
+
const message = choice.message;
|
|
63
|
+
return {
|
|
64
|
+
content: message.content || "",
|
|
65
|
+
toolCalls: message.tool_calls?.map((tc) => ({
|
|
66
|
+
id: tc.id,
|
|
67
|
+
type: "function",
|
|
68
|
+
function: {
|
|
69
|
+
name: tc.function.name,
|
|
70
|
+
arguments: tc.function.arguments
|
|
71
|
+
}
|
|
72
|
+
})),
|
|
73
|
+
finishReason: this.mapFinishReason(choice.finish_reason),
|
|
74
|
+
usage: this.mapUsage(response.usage)
|
|
75
|
+
};
|
|
76
|
+
} catch (error) {
|
|
77
|
+
this.logger.error("LLM Gateway request failed", error, void 0, void 0);
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async *generateStream(messages, tools, options) {
|
|
82
|
+
const model = `route:${this.modelName}`;
|
|
83
|
+
const headers = this.buildGatewayHeaders();
|
|
84
|
+
this.logger.debug("LLM Gateway stream request", {
|
|
85
|
+
model,
|
|
86
|
+
baseUrl: this.baseUrl,
|
|
87
|
+
stream: true,
|
|
88
|
+
messageCount: messages.length,
|
|
89
|
+
toolCount: tools?.length ?? 0
|
|
90
|
+
});
|
|
91
|
+
let stream;
|
|
92
|
+
try {
|
|
93
|
+
stream = await this.client.chat.completions.create(
|
|
94
|
+
{
|
|
95
|
+
model,
|
|
96
|
+
messages: this.convertMessages(messages),
|
|
97
|
+
tools: this.convertTools(tools),
|
|
98
|
+
stream: true,
|
|
99
|
+
stream_options: { include_usage: true },
|
|
100
|
+
response_format: options?.responseFormat?.type === "json" ? options.responseFormat.schema ? {
|
|
101
|
+
type: "json_schema",
|
|
102
|
+
json_schema: { name: "response", schema: options.responseFormat.schema }
|
|
103
|
+
} : { type: "json_object" } : void 0,
|
|
104
|
+
...this.extractExtraOptions(options)
|
|
105
|
+
},
|
|
106
|
+
{ headers }
|
|
107
|
+
);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
this.logger.error("LLM Gateway stream request failed", error, void 0, void 0);
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
for await (const chunk of stream) {
|
|
114
|
+
if (chunk.usage) {
|
|
115
|
+
yield {
|
|
116
|
+
type: "finish",
|
|
117
|
+
reason: "stop",
|
|
118
|
+
usage: this.mapUsage(chunk.usage)
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const choice = chunk.choices?.[0];
|
|
122
|
+
if (!choice) continue;
|
|
123
|
+
if (choice.finish_reason) {
|
|
124
|
+
yield {
|
|
125
|
+
type: "finish",
|
|
126
|
+
reason: this.mapFinishReason(choice.finish_reason)
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const delta = choice.delta;
|
|
130
|
+
if (!delta) continue;
|
|
131
|
+
if (delta.content) {
|
|
132
|
+
yield { type: "content_delta", delta: delta.content };
|
|
133
|
+
}
|
|
134
|
+
if (delta.tool_calls) {
|
|
135
|
+
for (const tc of delta.tool_calls) {
|
|
136
|
+
if (tc.id && tc.function?.name) {
|
|
137
|
+
yield {
|
|
138
|
+
type: "tool_call_start",
|
|
139
|
+
index: tc.index,
|
|
140
|
+
id: tc.id,
|
|
141
|
+
name: tc.function.name
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (tc.function?.arguments) {
|
|
145
|
+
yield {
|
|
146
|
+
type: "tool_call_delta",
|
|
147
|
+
index: tc.index,
|
|
148
|
+
args: tc.function.arguments
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
this.logger.error("LLM Gateway stream failed", error, void 0, void 0);
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// ==========================================================================
|
|
160
|
+
// Helpers (copied from OpenAI provider)
|
|
161
|
+
// ==========================================================================
|
|
162
|
+
extractExtraOptions(options) {
|
|
163
|
+
if (!options) return {};
|
|
164
|
+
const { responseFormat, ...rest } = options;
|
|
165
|
+
return rest;
|
|
166
|
+
}
|
|
167
|
+
buildGatewayHeaders() {
|
|
168
|
+
const headers = {
|
|
169
|
+
"x-request-id": randomUUID()
|
|
170
|
+
};
|
|
171
|
+
const exec = ExecutionContext.getOrUndefined();
|
|
172
|
+
if (exec?.executionId) {
|
|
173
|
+
headers["x-execution-id"] = exec.executionId;
|
|
174
|
+
}
|
|
175
|
+
const ctx = Tracing.getContext();
|
|
176
|
+
if (ctx) {
|
|
177
|
+
headers.traceparent = `00-${ctx.traceId}-${ctx.spanId}-01`;
|
|
178
|
+
} else {
|
|
179
|
+
const traceId = randomBytes(16).toString("hex");
|
|
180
|
+
const spanId = randomBytes(8).toString("hex");
|
|
181
|
+
headers.traceparent = `00-${traceId}-${spanId}-01`;
|
|
182
|
+
}
|
|
183
|
+
return headers;
|
|
184
|
+
}
|
|
185
|
+
convertMessages(messages) {
|
|
186
|
+
const allowedToolCallIds = /* @__PURE__ */ new Set();
|
|
187
|
+
return messages.flatMap((m) => {
|
|
188
|
+
if (m.role === "tool") {
|
|
189
|
+
if (!m.tool_call_id || !allowedToolCallIds.has(m.tool_call_id)) {
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
role: "tool",
|
|
194
|
+
tool_call_id: m.tool_call_id,
|
|
195
|
+
content: typeof m.content === "string" ? m.content : JSON.stringify(m.content)
|
|
196
|
+
// Tool output usually string
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
if (m.role === "assistant") {
|
|
200
|
+
let assistantContent = null;
|
|
201
|
+
if (typeof m.content === "string") {
|
|
202
|
+
assistantContent = m.content;
|
|
203
|
+
}
|
|
204
|
+
if (!assistantContent && (!m.tool_calls || m.tool_calls.length === 0)) {
|
|
205
|
+
assistantContent = "";
|
|
206
|
+
}
|
|
207
|
+
if (m.tool_calls) {
|
|
208
|
+
m.tool_calls.forEach((tc) => allowedToolCallIds.add(tc.id));
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
role: "assistant",
|
|
212
|
+
content: assistantContent,
|
|
213
|
+
tool_calls: m.tool_calls?.map((tc) => ({
|
|
214
|
+
id: tc.id,
|
|
215
|
+
type: "function",
|
|
216
|
+
function: {
|
|
217
|
+
name: tc.function.name,
|
|
218
|
+
arguments: tc.function.arguments
|
|
219
|
+
}
|
|
220
|
+
}))
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const content = Array.isArray(
|
|
224
|
+
m.content
|
|
225
|
+
) ? m.content.map((part) => this.convertContentPart(part)) : m.content || "";
|
|
226
|
+
return {
|
|
227
|
+
role: m.role,
|
|
228
|
+
content,
|
|
229
|
+
name: m.name
|
|
230
|
+
};
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
convertTools(tools) {
|
|
234
|
+
if (!tools || tools.length === 0) return void 0;
|
|
235
|
+
return tools.map((t) => ({
|
|
236
|
+
type: "function",
|
|
237
|
+
function: {
|
|
238
|
+
name: t.function.name,
|
|
239
|
+
description: t.function.description,
|
|
240
|
+
parameters: t.function.parameters
|
|
241
|
+
}
|
|
242
|
+
}));
|
|
243
|
+
}
|
|
244
|
+
mapUsage(usage) {
|
|
245
|
+
if (!usage) return void 0;
|
|
246
|
+
return {
|
|
247
|
+
promptTokens: usage.prompt_tokens,
|
|
248
|
+
completionTokens: usage.completion_tokens,
|
|
249
|
+
totalTokens: usage.total_tokens,
|
|
250
|
+
reasoningTokens: usage.completion_tokens_details?.reasoning_tokens
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
mapFinishReason(reason) {
|
|
254
|
+
switch (reason) {
|
|
255
|
+
case "stop":
|
|
256
|
+
return "stop";
|
|
257
|
+
case "length":
|
|
258
|
+
return "length";
|
|
259
|
+
case "tool_calls":
|
|
260
|
+
return "tool_calls";
|
|
261
|
+
case "content_filter":
|
|
262
|
+
return "content_filter";
|
|
263
|
+
default:
|
|
264
|
+
return void 0;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
convertContentPart(part) {
|
|
268
|
+
if (part.type === "text") return { type: "text", text: part.text };
|
|
269
|
+
if (part.type === "file") {
|
|
270
|
+
const { mimeType, source } = part.file;
|
|
271
|
+
if (!mimeType.startsWith("image/")) {
|
|
272
|
+
throw new Error(`LLM Gateway: file mimeType '${mimeType}' is not supported yet`);
|
|
273
|
+
}
|
|
274
|
+
const url = source.type === "url" ? source.url : `data:${mimeType};base64,${source.base64}`;
|
|
275
|
+
return { type: "image_url", image_url: { url } };
|
|
276
|
+
}
|
|
277
|
+
if (part.type === "image_url") {
|
|
278
|
+
return { type: "image_url", image_url: { url: part.image_url.url } };
|
|
279
|
+
}
|
|
280
|
+
const _exhaustive = part;
|
|
281
|
+
throw new Error(`Unsupported content type: ${_exhaustive.type}`);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
export {
|
|
285
|
+
LLMGateway
|
|
286
|
+
};
|
|
287
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import OpenAIClient, { ClientOptions } from \"openai\";\nimport { randomBytes, randomUUID } from \"node:crypto\";\nimport {\n IModel,\n Message,\n ProviderResponse,\n StreamChunk,\n ToolDefinitionForLLM,\n GenerateOptions,\n TokenUsage,\n ContentPart,\n ConsoleLogger,\n type LogLevel,\n ExecutionContext,\n Tracing,\n} from \"@nebulaos/core\";\n\n/**\n * LLM Gateway Provider Configuration\n */\nexport interface LLMGatewayConfig {\n /** API Key from NebulaOS */\n apiKey: string;\n /** Base URL of the NebulaOS LLM Gateway */\n baseUrl?: string;\n /** Route alias (e.g., \"assistente\", \"code-review\") */\n model: string;\n /** Logger verbosity for gateway calls */\n logLevel?: LogLevel;\n /** Optional OpenAI client options */\n clientOptions?: ClientOptions;\n}\n\n/**\n * NebulaOS LLM Gateway Provider\n *\n * Provides access to NebulaOS LLM Gateway routes through an OpenAI-compatible interface.\n * Routes are pre-configured in NebulaOS and provide automatic fallback, cost tracking,\n * and access control.\n */\nexport class LLMGateway implements IModel {\n providerName = \"llm-gateway\";\n modelName: string;\n private client: OpenAIClient;\n private baseUrl: string;\n private logger: ConsoleLogger;\n\n capabilities = {\n inputFiles: {\n mimeTypes: [\"image/*\"],\n sources: [\"url\", \"base64\"] as const,\n },\n } as const;\n\n constructor(config: LLMGatewayConfig) {\n this.modelName = config.model;\n\n this.baseUrl = config.baseUrl || \"http://localhost:4100\";\n const baseURL = this.baseUrl.endsWith(\"/v1\") ? this.baseUrl : `${this.baseUrl}/v1`;\n this.logger = new ConsoleLogger(config.logLevel || \"info\", \"nebulaos/llm-gateway\");\n\n this.client = new OpenAIClient({\n apiKey: config.apiKey,\n baseURL,\n ...config.clientOptions,\n });\n }\n\n async generate(\n messages: Message[],\n tools?: ToolDefinitionForLLM[],\n options?: GenerateOptions,\n ): Promise<ProviderResponse> {\n const model = `route:${this.modelName}`;\n const headers = this.buildGatewayHeaders();\n this.logger.debug(\"LLM Gateway request\", {\n model,\n baseUrl: this.baseUrl,\n stream: false,\n messageCount: messages.length,\n toolCount: tools?.length ?? 0,\n });\n\n try {\n const response = await this.client.chat.completions.create(\n {\n model,\n messages: this.convertMessages(messages),\n tools: this.convertTools(tools),\n response_format:\n options?.responseFormat?.type === \"json\"\n ? options.responseFormat.schema\n ? {\n type: \"json_schema\",\n json_schema: { name: \"response\", schema: options.responseFormat.schema as any },\n }\n : { type: \"json_object\" }\n : undefined,\n ...this.extractExtraOptions(options),\n },\n { headers },\n );\n\n this.logger.debug(\"LLM Gateway response\", {\n model,\n finishReason: response.choices?.[0]?.finish_reason,\n hasUsage: Boolean(response.usage),\n });\n\n const choice = response.choices[0];\n const message = choice.message;\n\n return {\n content: message.content || \"\",\n toolCalls: message.tool_calls?.map((tc) => ({\n id: tc.id,\n type: \"function\",\n function: {\n name: tc.function.name,\n arguments: tc.function.arguments,\n },\n })),\n finishReason: this.mapFinishReason(choice.finish_reason),\n usage: this.mapUsage(response.usage),\n };\n } catch (error) {\n this.logger.error(\"LLM Gateway request failed\", error, undefined, undefined);\n throw error;\n }\n }\n\n async *generateStream(\n messages: Message[],\n tools?: ToolDefinitionForLLM[],\n options?: GenerateOptions,\n ): AsyncGenerator<StreamChunk> {\n const model = `route:${this.modelName}`;\n const headers = this.buildGatewayHeaders();\n this.logger.debug(\"LLM Gateway stream request\", {\n model,\n baseUrl: this.baseUrl,\n stream: true,\n messageCount: messages.length,\n toolCount: tools?.length ?? 0,\n });\n\n let stream;\n try {\n stream = await this.client.chat.completions.create(\n {\n model,\n messages: this.convertMessages(messages),\n tools: this.convertTools(tools),\n stream: true,\n stream_options: { include_usage: true },\n response_format:\n options?.responseFormat?.type === \"json\"\n ? options.responseFormat.schema\n ? {\n type: \"json_schema\",\n json_schema: { name: \"response\", schema: options.responseFormat.schema as any },\n }\n : { type: \"json_object\" }\n : undefined,\n ...this.extractExtraOptions(options),\n },\n { headers },\n );\n } catch (error) {\n this.logger.error(\"LLM Gateway stream request failed\", error, undefined, undefined);\n throw error;\n }\n\n try {\n for await (const chunk of stream) {\n if (chunk.usage) {\n yield {\n type: \"finish\",\n reason: \"stop\",\n usage: this.mapUsage(chunk.usage),\n };\n }\n\n const choice = chunk.choices?.[0];\n if (!choice) continue;\n\n if (choice.finish_reason) {\n yield {\n type: \"finish\",\n reason: this.mapFinishReason(choice.finish_reason),\n };\n }\n\n const delta = choice.delta;\n if (!delta) continue;\n\n if (delta.content) {\n yield { type: \"content_delta\", delta: delta.content };\n }\n\n if (delta.tool_calls) {\n for (const tc of delta.tool_calls) {\n if (tc.id && tc.function?.name) {\n yield {\n type: \"tool_call_start\",\n index: tc.index,\n id: tc.id,\n name: tc.function.name,\n };\n }\n\n if (tc.function?.arguments) {\n yield {\n type: \"tool_call_delta\",\n index: tc.index,\n args: tc.function.arguments,\n };\n }\n }\n }\n }\n } catch (error) {\n this.logger.error(\"LLM Gateway stream failed\", error, undefined, undefined);\n throw error;\n }\n }\n\n // ==========================================================================\n // Helpers (copied from OpenAI provider)\n // ==========================================================================\n\n private extractExtraOptions(options?: GenerateOptions): Record<string, any> {\n if (!options) return {};\n const { responseFormat, ...rest } = options;\n return rest;\n }\n\n private buildGatewayHeaders(): Record<string, string> {\n const headers: Record<string, string> = {\n \"x-request-id\": randomUUID(),\n };\n\n const exec = ExecutionContext.getOrUndefined();\n if (exec?.executionId) {\n headers[\"x-execution-id\"] = exec.executionId;\n }\n\n const ctx = Tracing.getContext();\n if (ctx) {\n // IDs já estão em formato W3C (hex)\n headers.traceparent = `00-${ctx.traceId}-${ctx.spanId}-01`;\n } else {\n // Still emit a root trace context so the gateway can correlate requests by traceId.\n const traceId = randomBytes(16).toString(\"hex\");\n const spanId = randomBytes(8).toString(\"hex\");\n headers.traceparent = `00-${traceId}-${spanId}-01`;\n }\n\n return headers;\n }\n\n protected convertMessages(messages: Message[]): OpenAIClient.Chat.ChatCompletionMessageParam[] {\n // Ensure tools have a preceding assistant message with tool_calls\n const allowedToolCallIds = new Set<string>();\n\n return messages.flatMap((m) => {\n if (m.role === \"tool\") {\n // Skip orphan tool messages (no preceding tool_calls)\n if (!m.tool_call_id || !allowedToolCallIds.has(m.tool_call_id)) {\n return [];\n }\n\n return {\n role: \"tool\",\n tool_call_id: m.tool_call_id!,\n content: typeof m.content === \"string\" ? m.content : JSON.stringify(m.content), // Tool output usually string\n };\n }\n\n if (m.role === \"assistant\") {\n // OpenAI rules:\n // - content is required (string | null)\n // - if tool_calls is present, content can be null\n // - if tool_calls is NOT present, content must be string (cannot be null/empty if strict, but usually empty string is fine)\n\n let assistantContent: string | null = null;\n\n if (typeof m.content === \"string\") {\n assistantContent = m.content;\n }\n\n // If content is null/empty AND no tool_calls, force empty string to avoid API error\n if (!assistantContent && (!m.tool_calls || m.tool_calls.length === 0)) {\n assistantContent = \"\";\n }\n\n if (m.tool_calls) {\n m.tool_calls.forEach((tc) => allowedToolCallIds.add(tc.id));\n }\n\n return {\n role: \"assistant\",\n content: assistantContent,\n tool_calls: m.tool_calls?.map((tc) => ({\n id: tc.id,\n type: \"function\",\n function: {\n name: tc.function.name,\n arguments: tc.function.arguments,\n },\n })),\n };\n }\n\n // User / System with potential multimodal content\n const content: OpenAIClient.Chat.ChatCompletionContentPart[] | string = Array.isArray(\n m.content,\n )\n ? m.content.map((part) => this.convertContentPart(part))\n : m.content || \"\";\n\n return {\n role: m.role as \"system\" | \"user\",\n content,\n name: m.name,\n } as any;\n });\n }\n\n private convertTools(\n tools?: ToolDefinitionForLLM[],\n ): OpenAIClient.Chat.ChatCompletionTool[] | undefined {\n if (!tools || tools.length === 0) return undefined;\n return tools.map((t) => ({\n type: \"function\",\n function: {\n name: t.function.name,\n description: t.function.description,\n parameters: t.function.parameters,\n },\n }));\n }\n\n private mapUsage(usage?: OpenAIClient.CompletionUsage): TokenUsage | undefined {\n if (!usage) return undefined;\n return {\n promptTokens: usage.prompt_tokens,\n completionTokens: usage.completion_tokens,\n totalTokens: usage.total_tokens,\n reasoningTokens: (usage as any).completion_tokens_details?.reasoning_tokens,\n };\n }\n\n private mapFinishReason(reason: string | null): ProviderResponse[\"finishReason\"] {\n switch (reason) {\n case \"stop\":\n return \"stop\";\n case \"length\":\n return \"length\";\n case \"tool_calls\":\n return \"tool_calls\";\n case \"content_filter\":\n return \"content_filter\";\n default:\n return undefined;\n }\n }\n\n private convertContentPart(part: ContentPart): OpenAIClient.Chat.ChatCompletionContentPart {\n if (part.type === \"text\") return { type: \"text\", text: part.text };\n\n if (part.type === \"file\") {\n const { mimeType, source } = part.file;\n if (!mimeType.startsWith(\"image/\")) {\n throw new Error(`LLM Gateway: file mimeType '${mimeType}' is not supported yet`);\n }\n\n const url = source.type === \"url\" ? source.url : `data:${mimeType};base64,${source.base64}`;\n\n return { type: \"image_url\", image_url: { url } };\n }\n\n if (part.type === \"image_url\") {\n return { type: \"image_url\", image_url: { url: part.image_url.url } };\n }\n\n // Exhaustive check - should never reach here with proper ContentPart\n const _exhaustive: never = part;\n throw new Error(`Unsupported content type: ${(_exhaustive as any).type}`);\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nebulaos/llm-gateway",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "NebulaOS LLM Gateway provider - OpenAI-compatible chat completions",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.mjs",
|
|
11
|
+
"require": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"sideEffects": false,
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/Psyco-AI-Tech/nebulaos.git",
|
|
22
|
+
"directory": "packages/providers/llm-gateway"
|
|
23
|
+
},
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"openai": "^4.52.7",
|
|
30
|
+
"@nebulaos/core": "0.1.1"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^20.0.0",
|
|
34
|
+
"tsup": "^8.0.0",
|
|
35
|
+
"typescript": "^5.0.0",
|
|
36
|
+
"vitest": "^1.0.0"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsup",
|
|
40
|
+
"dev": "tsup --watch",
|
|
41
|
+
"test": "vitest",
|
|
42
|
+
"typecheck": "tsc --noEmit"
|
|
43
|
+
}
|
|
44
|
+
}
|