@aigne/afs-ai-gateway 1.12.0-beta.5
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.md +26 -0
- package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.cjs +11 -0
- package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.mjs +10 -0
- package/dist/_virtual/rolldown_runtime.cjs +29 -0
- package/dist/ai-gateway.cjs +1083 -0
- package/dist/ai-gateway.d.cts +124 -0
- package/dist/ai-gateway.d.cts.map +1 -0
- package/dist/ai-gateway.d.mts +124 -0
- package/dist/ai-gateway.d.mts.map +1 -0
- package/dist/ai-gateway.mjs +1083 -0
- package/dist/ai-gateway.mjs.map +1 -0
- package/dist/index.cjs +14 -0
- package/dist/index.d.cts +5 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.mjs +6 -0
- package/dist/models.cjs +155 -0
- package/dist/models.d.cts +8 -0
- package/dist/models.d.cts.map +1 -0
- package/dist/models.d.mts +8 -0
- package/dist/models.d.mts.map +1 -0
- package/dist/models.mjs +150 -0
- package/dist/models.mjs.map +1 -0
- package/dist/models2.cjs +97 -0
- package/dist/models2.mjs +96 -0
- package/dist/models2.mjs.map +1 -0
- package/dist/normalize.cjs +49 -0
- package/dist/normalize.d.cts +10 -0
- package/dist/normalize.d.cts.map +1 -0
- package/dist/normalize.d.mts +10 -0
- package/dist/normalize.d.mts.map +1 -0
- package/dist/normalize.mjs +47 -0
- package/dist/normalize.mjs.map +1 -0
- package/dist/types.cjs +80 -0
- package/dist/types.d.cts +104 -0
- package/dist/types.d.cts.map +1 -0
- package/dist/types.d.mts +104 -0
- package/dist/types.d.mts.map +1 -0
- package/dist/types.mjs +77 -0
- package/dist/types.mjs.map +1 -0
- package/manifest.json +40 -0
- package/models.json +145 -0
- package/package.json +61 -0
|
@@ -0,0 +1,1083 @@
|
|
|
1
|
+
const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
|
|
2
|
+
const require_types = require('./types.cjs');
|
|
3
|
+
const require_models = require('./models2.cjs');
|
|
4
|
+
const require_normalize = require('./normalize.cjs');
|
|
5
|
+
const require_decorate = require('./_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.cjs');
|
|
6
|
+
let _aigne_afs = require("@aigne/afs");
|
|
7
|
+
let _aigne_afs_utils_zod = require("@aigne/afs/utils/zod");
|
|
8
|
+
let _aigne_model_base = require("@aigne/model-base");
|
|
9
|
+
let _aigne_openai = require("@aigne/openai");
|
|
10
|
+
let ufo = require("ufo");
|
|
11
|
+
|
|
12
|
+
//#region src/ai-gateway.ts
|
|
13
|
+
const log = (0, _aigne_afs.makeNsLog)("provider:ai-gateway");
|
|
14
|
+
const TYPES = [
|
|
15
|
+
"chat",
|
|
16
|
+
"embed",
|
|
17
|
+
"image",
|
|
18
|
+
"video"
|
|
19
|
+
];
|
|
20
|
+
const TYPE_ACTION = {
|
|
21
|
+
chat: "chat",
|
|
22
|
+
embed: "embed",
|
|
23
|
+
image: "generateImage",
|
|
24
|
+
video: "generateVideo"
|
|
25
|
+
};
|
|
26
|
+
const CHAT_SCHEMA = {
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: {
|
|
29
|
+
prompt: { type: "string" },
|
|
30
|
+
messages: {
|
|
31
|
+
type: "array",
|
|
32
|
+
items: {
|
|
33
|
+
type: "object",
|
|
34
|
+
properties: {
|
|
35
|
+
role: { type: "string" },
|
|
36
|
+
content: { oneOf: [
|
|
37
|
+
{ type: "string" },
|
|
38
|
+
{ type: "null" },
|
|
39
|
+
{ type: "array" }
|
|
40
|
+
] },
|
|
41
|
+
tool_calls: { type: "array" },
|
|
42
|
+
tool_call_id: { type: "string" }
|
|
43
|
+
},
|
|
44
|
+
required: ["role"]
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
temperature: { type: "number" },
|
|
48
|
+
topP: { type: "number" },
|
|
49
|
+
maxTokens: { type: "number" },
|
|
50
|
+
responseFormat: { type: "object" }
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const EMBED_SCHEMA = {
|
|
54
|
+
type: "object",
|
|
55
|
+
properties: {
|
|
56
|
+
input: {
|
|
57
|
+
oneOf: [{ type: "string" }, {
|
|
58
|
+
type: "array",
|
|
59
|
+
items: { type: "string" }
|
|
60
|
+
}],
|
|
61
|
+
description: "Text(s) to embed"
|
|
62
|
+
},
|
|
63
|
+
text: {
|
|
64
|
+
type: "string",
|
|
65
|
+
description: "Shorthand for single-text input"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const LIST_MODELS_SCHEMA = {
|
|
70
|
+
type: "object",
|
|
71
|
+
properties: { type: {
|
|
72
|
+
type: "string",
|
|
73
|
+
enum: [
|
|
74
|
+
"chat",
|
|
75
|
+
"embed",
|
|
76
|
+
"image",
|
|
77
|
+
"video"
|
|
78
|
+
]
|
|
79
|
+
} }
|
|
80
|
+
};
|
|
81
|
+
const HUB_ACTIONS = [{
|
|
82
|
+
name: "listModels",
|
|
83
|
+
description: "Return every model exposed by this hub as { entries: [{ model, nativeModelId, available, vendor, type }] }.",
|
|
84
|
+
schema: LIST_MODELS_SCHEMA
|
|
85
|
+
}, {
|
|
86
|
+
name: "refresh",
|
|
87
|
+
description: "Reset client caches (noop for static bundled lists; honored for contract parity).",
|
|
88
|
+
schema: {
|
|
89
|
+
type: "object",
|
|
90
|
+
properties: {}
|
|
91
|
+
}
|
|
92
|
+
}];
|
|
93
|
+
/**
|
|
94
|
+
* Adapt a `response_format: { type: "json_schema", ... }` payload so it
|
|
95
|
+
* passes Cloudflare AI Gateway's stricter validation than vanilla OpenAI:
|
|
96
|
+
*
|
|
97
|
+
* 1. `strict` defaults to `true` — CF rejects requests where the field
|
|
98
|
+
* is omitted (`Field required`) and accepts only `true`
|
|
99
|
+
* (`Input should be True`).
|
|
100
|
+
* 2. Every nested object in the schema gets `additionalProperties: false`
|
|
101
|
+
* and `required` listing all of its `properties` keys — OpenAI's
|
|
102
|
+
* strict mode demands this. Without it CF returns 400 like
|
|
103
|
+
* `For 'object' type, 'additionalProperties' must be explicitly set
|
|
104
|
+
* to false`.
|
|
105
|
+
*
|
|
106
|
+
* Both transformations are no-ops on schemas that already comply, so
|
|
107
|
+
* callers who write strict-correct schemas keep working unchanged. Without
|
|
108
|
+
* this normaliser scan-spam (`SPAM_JUDGE_SCHEMA`) failed every request
|
|
109
|
+
* with `LLM call failed after 3 attempts` and agent-run returned
|
|
110
|
+
* `{ status: "error" }`, which scan-spam translated to
|
|
111
|
+
* `reason: "no result"` for every message.
|
|
112
|
+
*/
|
|
113
|
+
function normalizeResponseFormatForOpenAI(rf) {
|
|
114
|
+
if (!rf || typeof rf !== "object") return rf;
|
|
115
|
+
const obj = rf;
|
|
116
|
+
if (obj.type !== "json_schema") return rf;
|
|
117
|
+
const js = obj.jsonSchema;
|
|
118
|
+
if (!js || typeof js !== "object") return rf;
|
|
119
|
+
const schema = js.schema;
|
|
120
|
+
return {
|
|
121
|
+
...obj,
|
|
122
|
+
jsonSchema: {
|
|
123
|
+
...js,
|
|
124
|
+
strict: typeof js.strict === "boolean" ? js.strict : true,
|
|
125
|
+
...schema ? { schema: enforceStrictSchema(schema) } : {}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Recursively patch a JSON schema for OpenAI strict mode:
|
|
131
|
+
* - `additionalProperties: false` on every object node
|
|
132
|
+
* - `required` listing every key in `properties`
|
|
133
|
+
* - Strip keywords OpenAI strict rejects (`minimum`, `maximum`,
|
|
134
|
+
* `minLength`, `maxLength`, `minItems`, `maxItems`, `pattern`,
|
|
135
|
+
* `format`, `multipleOf`, `exclusiveMinimum`, `exclusiveMaximum`).
|
|
136
|
+
* These are validation hints — losing them means the model's output
|
|
137
|
+
* isn't bounds-checked server-side, but it's far better than the
|
|
138
|
+
* alternative (request fails with 400, scan-spam sees "no result"
|
|
139
|
+
* for every message). Caller-side schema validation can re-apply the
|
|
140
|
+
* bounds afterward if needed.
|
|
141
|
+
*/
|
|
142
|
+
const STRICT_INCOMPATIBLE_KEYWORDS = [
|
|
143
|
+
"minimum",
|
|
144
|
+
"maximum",
|
|
145
|
+
"exclusiveMinimum",
|
|
146
|
+
"exclusiveMaximum",
|
|
147
|
+
"multipleOf",
|
|
148
|
+
"minLength",
|
|
149
|
+
"maxLength",
|
|
150
|
+
"pattern",
|
|
151
|
+
"format",
|
|
152
|
+
"minItems",
|
|
153
|
+
"maxItems",
|
|
154
|
+
"uniqueItems",
|
|
155
|
+
"minProperties",
|
|
156
|
+
"maxProperties"
|
|
157
|
+
];
|
|
158
|
+
function enforceStrictSchema(node) {
|
|
159
|
+
if (!node || typeof node !== "object" || Array.isArray(node)) return node;
|
|
160
|
+
const n = node;
|
|
161
|
+
const out = {};
|
|
162
|
+
for (const [k, v] of Object.entries(n)) {
|
|
163
|
+
if (STRICT_INCOMPATIBLE_KEYWORDS.includes(k)) continue;
|
|
164
|
+
out[k] = v;
|
|
165
|
+
}
|
|
166
|
+
if (n.type === "object" && n.properties && typeof n.properties === "object") {
|
|
167
|
+
const props = n.properties;
|
|
168
|
+
const patchedProps = {};
|
|
169
|
+
for (const [k, v] of Object.entries(props)) patchedProps[k] = enforceStrictSchema(v);
|
|
170
|
+
out.properties = patchedProps;
|
|
171
|
+
if (out.additionalProperties === void 0) out.additionalProperties = false;
|
|
172
|
+
if (!Array.isArray(out.required)) out.required = Object.keys(patchedProps);
|
|
173
|
+
}
|
|
174
|
+
if (n.type === "array" && n.items) out.items = Array.isArray(n.items) ? n.items.map((it) => enforceStrictSchema(it)) : enforceStrictSchema(n.items);
|
|
175
|
+
for (const composer of [
|
|
176
|
+
"anyOf",
|
|
177
|
+
"oneOf",
|
|
178
|
+
"allOf"
|
|
179
|
+
]) if (Array.isArray(n[composer])) out[composer] = n[composer].map((x) => enforceStrictSchema(x));
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
function inferenceError(modelName, err) {
|
|
183
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
184
|
+
return {
|
|
185
|
+
success: false,
|
|
186
|
+
error: {
|
|
187
|
+
code: "INFERENCE_ERROR",
|
|
188
|
+
message: raw.includes("fetch failed") || raw.includes("ECONNREFUSED") || raw.includes("ECONNRESET") || raw.includes("ETIMEDOUT") ? `Connection to gateway lost for ${modelName} (will retry with fresh connection)` : `Inference failed for ${modelName}: ${raw}`
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
const VALID_ROLES = new Set([
|
|
193
|
+
"system",
|
|
194
|
+
"user",
|
|
195
|
+
"agent",
|
|
196
|
+
"tool"
|
|
197
|
+
]);
|
|
198
|
+
function mapRole(role) {
|
|
199
|
+
if (role === "assistant") return "agent";
|
|
200
|
+
if (typeof role === "string" && VALID_ROLES.has(role)) return role;
|
|
201
|
+
return "user";
|
|
202
|
+
}
|
|
203
|
+
function blockHasCacheMarker(b) {
|
|
204
|
+
const raw = b;
|
|
205
|
+
return raw.cache_control !== void 0 || raw.cacheControl !== void 0;
|
|
206
|
+
}
|
|
207
|
+
function messagesHaveCacheMarkers(messages) {
|
|
208
|
+
return messages.some((m) => Array.isArray(m.content) && m.content.some(blockHasCacheMarker));
|
|
209
|
+
}
|
|
210
|
+
/** AIGNE role → OpenAI wire role. */
|
|
211
|
+
function toOpenAIRole(role) {
|
|
212
|
+
return role === "agent" ? "assistant" : role;
|
|
213
|
+
}
|
|
214
|
+
/** Serialise AIGNE messages to the OpenAI-compat JSON shape, forwarding cache_control. */
|
|
215
|
+
function serializeMessagesForWire(messages) {
|
|
216
|
+
return messages.map((m) => {
|
|
217
|
+
const out = {
|
|
218
|
+
role: toOpenAIRole(m.role),
|
|
219
|
+
content: typeof m.content === "string" ? m.content : m.content?.map((b) => {
|
|
220
|
+
const raw = b;
|
|
221
|
+
if (raw.type === "text") {
|
|
222
|
+
const cc = raw.cache_control ?? raw.cacheControl;
|
|
223
|
+
return {
|
|
224
|
+
type: "text",
|
|
225
|
+
text: raw.text,
|
|
226
|
+
...cc !== void 0 ? { cache_control: cc } : {}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
if (raw.type === "url") return {
|
|
230
|
+
type: "image_url",
|
|
231
|
+
image_url: { url: raw.url }
|
|
232
|
+
};
|
|
233
|
+
if (raw.type === "file") return {
|
|
234
|
+
type: "image_url",
|
|
235
|
+
image_url: { url: `data:${raw.mimeType || "image/png"};base64,${raw.data}` }
|
|
236
|
+
};
|
|
237
|
+
return raw;
|
|
238
|
+
})
|
|
239
|
+
};
|
|
240
|
+
if (m.toolCalls) out.tool_calls = m.toolCalls.map((tc) => ({
|
|
241
|
+
...tc,
|
|
242
|
+
function: {
|
|
243
|
+
...tc.function,
|
|
244
|
+
arguments: JSON.stringify(tc.function?.arguments)
|
|
245
|
+
}
|
|
246
|
+
}));
|
|
247
|
+
if (m.toolCallId) out.tool_call_id = m.toolCallId;
|
|
248
|
+
if (m.name) out.name = m.name;
|
|
249
|
+
return out;
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
/** Parse an OpenAI-wire tool_calls array (string `function.arguments`) into
|
|
253
|
+
* the AIGNE `toolCalls` shape (parsed-object `function.arguments`). */
|
|
254
|
+
function parseWireToolCalls(raw) {
|
|
255
|
+
if (!Array.isArray(raw) || raw.length === 0) return void 0;
|
|
256
|
+
return raw.map((tc) => {
|
|
257
|
+
const fn = tc.function;
|
|
258
|
+
const rawArgs = fn?.arguments;
|
|
259
|
+
let args = rawArgs;
|
|
260
|
+
if (typeof rawArgs === "string") try {
|
|
261
|
+
args = JSON.parse(rawArgs);
|
|
262
|
+
} catch {
|
|
263
|
+
args = rawArgs;
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
id: tc.id,
|
|
267
|
+
type: tc.type ?? "function",
|
|
268
|
+
function: {
|
|
269
|
+
name: fn?.name,
|
|
270
|
+
arguments: args
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
/** Convert a `normalizeResponseFormatForOpenAI`-normalized responseFormat
|
|
276
|
+
* (AIGNE shape: `{ type: "json_schema", jsonSchema: {...} }`) into the
|
|
277
|
+
* OpenAI wire shape (`{ type: "json_schema", json_schema: {...} }`) used by
|
|
278
|
+
* the raw-fetch bypass body. Non-`json_schema` shapes (e.g. `json_object`)
|
|
279
|
+
* pass through untouched — same convention as the SDK path. */
|
|
280
|
+
function toWireResponseFormat(normalized) {
|
|
281
|
+
if (!normalized || typeof normalized !== "object") return normalized;
|
|
282
|
+
const obj = normalized;
|
|
283
|
+
if (obj.type !== "json_schema") return obj;
|
|
284
|
+
return {
|
|
285
|
+
type: "json_schema",
|
|
286
|
+
json_schema: obj.jsonSchema
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
/** Convert an OpenAI-compat chat-completion response to the AIGNE output shape. */
|
|
290
|
+
function openAiRespToOutput(data) {
|
|
291
|
+
const message = (data.choices?.[0])?.message;
|
|
292
|
+
const text = message?.content ?? null;
|
|
293
|
+
const u = data.usage;
|
|
294
|
+
const out = { text };
|
|
295
|
+
const toolCalls = parseWireToolCalls(message?.tool_calls);
|
|
296
|
+
if (toolCalls) out.toolCalls = toolCalls;
|
|
297
|
+
if (u) out.usage = {
|
|
298
|
+
inputTokens: u.prompt_tokens ?? 0,
|
|
299
|
+
outputTokens: u.completion_tokens ?? 0
|
|
300
|
+
};
|
|
301
|
+
return out;
|
|
302
|
+
}
|
|
303
|
+
/** OpenAI → AIGNE message shape (assistant → agent, snake_case → camelCase). */
|
|
304
|
+
function normalizeMessage(msg) {
|
|
305
|
+
const out = { role: mapRole(msg.role) };
|
|
306
|
+
if (msg.content !== void 0 && msg.content !== null) out.content = msg.content;
|
|
307
|
+
const toolCalls = msg.toolCalls ?? msg.tool_calls;
|
|
308
|
+
if (toolCalls !== void 0) out.toolCalls = toolCalls;
|
|
309
|
+
const toolCallId = msg.toolCallId ?? msg.tool_call_id;
|
|
310
|
+
if (typeof toolCallId === "string") out.toolCallId = toolCallId;
|
|
311
|
+
if (typeof msg.name === "string") out.name = msg.name;
|
|
312
|
+
return out;
|
|
313
|
+
}
|
|
314
|
+
var AFSAIGateway = class extends _aigne_afs.AFSBaseProvider {
|
|
315
|
+
name;
|
|
316
|
+
description;
|
|
317
|
+
accessMode = "readwrite";
|
|
318
|
+
apiKey;
|
|
319
|
+
baseURL;
|
|
320
|
+
extraHeaders;
|
|
321
|
+
listStrategy;
|
|
322
|
+
defaultVendor;
|
|
323
|
+
endpointTimeoutMs;
|
|
324
|
+
/**
|
|
325
|
+
* The fallback catalog used (a) directly in bundled mode and (b) when the
|
|
326
|
+
* endpoint fetch fails in endpoint mode.
|
|
327
|
+
*/
|
|
328
|
+
fallbackModels;
|
|
329
|
+
/**
|
|
330
|
+
* Current resolved catalog. For `bundled` this is just `fallbackModels`.
|
|
331
|
+
* For `endpoint` this is `null` until the first fetch completes; subsequent
|
|
332
|
+
* access goes through `ensureModelsLoaded()`.
|
|
333
|
+
*/
|
|
334
|
+
cachedModels;
|
|
335
|
+
/** De-duplicate concurrent endpoint fetches. */
|
|
336
|
+
loadInflight = null;
|
|
337
|
+
chatCache = /* @__PURE__ */ new Map();
|
|
338
|
+
embedCache = /* @__PURE__ */ new Map();
|
|
339
|
+
constructor(options) {
|
|
340
|
+
super();
|
|
341
|
+
const parsed = (0, _aigne_afs_utils_zod.zodParse)(require_types.afsAIGatewayOptionsSchema, options);
|
|
342
|
+
this.name = parsed.name;
|
|
343
|
+
this.description = parsed.description;
|
|
344
|
+
this.baseURL = parsed.baseURL;
|
|
345
|
+
this.apiKey = parsed.apiKey;
|
|
346
|
+
this.extraHeaders = parsed.extraHeaders ?? {};
|
|
347
|
+
this.listStrategy = parsed.listStrategy;
|
|
348
|
+
this.defaultVendor = parsed.vendor;
|
|
349
|
+
this.endpointTimeoutMs = parsed.endpointTimeoutMs ?? 5e3;
|
|
350
|
+
this.fallbackModels = parsed.models && parsed.models.length > 0 ? parsed.models : require_models.DEFAULT_MODELS;
|
|
351
|
+
this.cachedModels = this.listStrategy === "bundled" ? this.fallbackModels : null;
|
|
352
|
+
}
|
|
353
|
+
/** Getter for current models — lazy resolves endpoint strategy on first read. */
|
|
354
|
+
async ensureModelsLoaded() {
|
|
355
|
+
if (this.cachedModels) return this.cachedModels;
|
|
356
|
+
if (!this.loadInflight) this.loadInflight = this.loadFromEndpoint();
|
|
357
|
+
try {
|
|
358
|
+
await this.loadInflight;
|
|
359
|
+
} finally {
|
|
360
|
+
this.loadInflight = null;
|
|
361
|
+
}
|
|
362
|
+
return this.cachedModels ?? this.fallbackModels;
|
|
363
|
+
}
|
|
364
|
+
async loadFromEndpoint() {
|
|
365
|
+
try {
|
|
366
|
+
this.cachedModels = await require_models.fetchModelsFromEndpoint(this.baseURL, this.apiKey, {
|
|
367
|
+
...this.defaultVendor ? { defaultVendor: this.defaultVendor } : {},
|
|
368
|
+
timeoutMs: this.endpointTimeoutMs
|
|
369
|
+
});
|
|
370
|
+
} catch (err) {
|
|
371
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
372
|
+
log.warn(`[ai-gateway:${this.name}] /models fetch failed, using fallback catalog: ${detail}`);
|
|
373
|
+
if (!this.cachedModels) this.cachedModels = this.fallbackModels;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/** Synchronous read — for routes that need the list right now. */
|
|
377
|
+
get models() {
|
|
378
|
+
return this.cachedModels ?? this.fallbackModels;
|
|
379
|
+
}
|
|
380
|
+
static manifest() {
|
|
381
|
+
return {
|
|
382
|
+
name: "ai-gateway",
|
|
383
|
+
description: "OpenAI-compatible AI gateway bridge (Cloudflare AI Gateway / OpenRouter / LiteLLM / OpenAI direct)",
|
|
384
|
+
uriTemplate: "ai-gateway://{name?}",
|
|
385
|
+
category: "ai",
|
|
386
|
+
schema: require_types.afsAIGatewayOptionsSchema,
|
|
387
|
+
tags: [
|
|
388
|
+
"ai",
|
|
389
|
+
"llm",
|
|
390
|
+
"gateway",
|
|
391
|
+
"openai-compatible",
|
|
392
|
+
"ai:hub"
|
|
393
|
+
],
|
|
394
|
+
capabilityTags: [
|
|
395
|
+
"auth:token",
|
|
396
|
+
"remote",
|
|
397
|
+
"http",
|
|
398
|
+
"rate-limited",
|
|
399
|
+
"streaming"
|
|
400
|
+
],
|
|
401
|
+
security: {
|
|
402
|
+
riskLevel: "external",
|
|
403
|
+
resourceAccess: ["internet"],
|
|
404
|
+
dataSensitivity: ["credentials"],
|
|
405
|
+
notes: ["Forwards prompts to a remote OpenAI-compatible gateway — API key authorizes usage and incurs cost."]
|
|
406
|
+
},
|
|
407
|
+
capabilities: {
|
|
408
|
+
network: { egress: true },
|
|
409
|
+
secrets: ["ai-gateway/api-key"]
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
static treeSchema() {
|
|
414
|
+
return {
|
|
415
|
+
operations: [
|
|
416
|
+
"list",
|
|
417
|
+
"read",
|
|
418
|
+
"exec",
|
|
419
|
+
"stat",
|
|
420
|
+
"explain"
|
|
421
|
+
],
|
|
422
|
+
tree: {
|
|
423
|
+
"/": {
|
|
424
|
+
kind: "ai-gateway:root",
|
|
425
|
+
operations: [
|
|
426
|
+
"list",
|
|
427
|
+
"read",
|
|
428
|
+
"exec"
|
|
429
|
+
],
|
|
430
|
+
actions: ["listModels", "refresh"]
|
|
431
|
+
},
|
|
432
|
+
"/models": {
|
|
433
|
+
kind: "ai-gateway:directory",
|
|
434
|
+
operations: ["list", "read"]
|
|
435
|
+
},
|
|
436
|
+
"/models/{model}": {
|
|
437
|
+
kind: "ai-gateway:model",
|
|
438
|
+
operations: ["read", "exec"],
|
|
439
|
+
actions: [
|
|
440
|
+
"chat",
|
|
441
|
+
"embed",
|
|
442
|
+
"generateImage",
|
|
443
|
+
"generateVideo"
|
|
444
|
+
]
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
auth: {
|
|
448
|
+
type: "token",
|
|
449
|
+
env: ["AI_GATEWAY_API_KEY"]
|
|
450
|
+
},
|
|
451
|
+
bestFor: ["AI inference via OpenAI-compatible gateways"],
|
|
452
|
+
notFor: ["data storage", "model training"]
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
findModel(canonical) {
|
|
456
|
+
const hit = require_normalize.findModelEntry(canonical, this.models);
|
|
457
|
+
if (!hit) {
|
|
458
|
+
const avail = this.models.map((m) => m.model).slice(0, 10);
|
|
459
|
+
throw new _aigne_afs.AFSNotFoundError((0, ufo.joinURL)("/models", canonical), `Model '${canonical}' not found. Available: ${avail.join(", ")}${this.models.length > 10 ? "…" : ""}`);
|
|
460
|
+
}
|
|
461
|
+
return hit;
|
|
462
|
+
}
|
|
463
|
+
openAIClientOptions() {
|
|
464
|
+
return Object.keys(this.extraHeaders).length > 0 ? { defaultHeaders: this.extraHeaders } : void 0;
|
|
465
|
+
}
|
|
466
|
+
getChatClient(nativeId) {
|
|
467
|
+
let m = this.chatCache.get(nativeId);
|
|
468
|
+
if (!m) {
|
|
469
|
+
const opts = this.openAIClientOptions();
|
|
470
|
+
m = new _aigne_openai.OpenAIChatModel({
|
|
471
|
+
baseURL: this.baseURL,
|
|
472
|
+
apiKey: this.apiKey,
|
|
473
|
+
model: nativeId,
|
|
474
|
+
...opts ? { clientOptions: opts } : {}
|
|
475
|
+
});
|
|
476
|
+
this.chatCache.set(nativeId, m);
|
|
477
|
+
}
|
|
478
|
+
return m;
|
|
479
|
+
}
|
|
480
|
+
getEmbedClient(nativeId) {
|
|
481
|
+
let m = this.embedCache.get(nativeId);
|
|
482
|
+
if (!m) {
|
|
483
|
+
const opts = this.openAIClientOptions();
|
|
484
|
+
m = new _aigne_openai.OpenAIEmbeddingModel({
|
|
485
|
+
baseURL: this.baseURL,
|
|
486
|
+
apiKey: this.apiKey,
|
|
487
|
+
model: nativeId,
|
|
488
|
+
...opts ? { clientOptions: opts } : {}
|
|
489
|
+
});
|
|
490
|
+
this.embedCache.set(nativeId, m);
|
|
491
|
+
}
|
|
492
|
+
return m;
|
|
493
|
+
}
|
|
494
|
+
async listRoot(_ctx) {
|
|
495
|
+
const models = await this.ensureModelsLoaded();
|
|
496
|
+
return { data: [this.buildEntry("/models", { meta: {
|
|
497
|
+
childrenCount: models.length,
|
|
498
|
+
kind: "ai-gateway:directory"
|
|
499
|
+
} })] };
|
|
500
|
+
}
|
|
501
|
+
async readRoot(_ctx) {
|
|
502
|
+
const models = await this.ensureModelsLoaded();
|
|
503
|
+
return this.buildEntry("/", {
|
|
504
|
+
content: {
|
|
505
|
+
provider: "ai-gateway",
|
|
506
|
+
baseURL: this.baseURL,
|
|
507
|
+
listStrategy: this.listStrategy,
|
|
508
|
+
modelCount: models.length
|
|
509
|
+
},
|
|
510
|
+
meta: {
|
|
511
|
+
childrenCount: 1,
|
|
512
|
+
kind: "ai-gateway:root"
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
async rootMeta(_ctx) {
|
|
517
|
+
const models = await this.ensureModelsLoaded();
|
|
518
|
+
const byType = {};
|
|
519
|
+
for (const m of models) byType[m.type] = (byType[m.type] ?? 0) + 1;
|
|
520
|
+
return this.buildEntry("/.meta", {
|
|
521
|
+
content: {
|
|
522
|
+
provider: "ai-gateway",
|
|
523
|
+
baseURL: this.baseURL,
|
|
524
|
+
description: this.description,
|
|
525
|
+
listStrategy: this.listStrategy,
|
|
526
|
+
modelCount: models.length,
|
|
527
|
+
modelsByType: byType
|
|
528
|
+
},
|
|
529
|
+
meta: { kind: "ai-gateway:root" }
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
async statRoot(_ctx) {
|
|
533
|
+
return { data: this.buildEntry("/", { meta: {
|
|
534
|
+
childrenCount: 1,
|
|
535
|
+
kind: "ai-gateway:root"
|
|
536
|
+
} }) };
|
|
537
|
+
}
|
|
538
|
+
async readCapabilities(_ctx) {
|
|
539
|
+
const operations = this.getOperationsDeclaration();
|
|
540
|
+
const manifest = {
|
|
541
|
+
schemaVersion: 1,
|
|
542
|
+
provider: this.name,
|
|
543
|
+
description: this.description || "AI Gateway provider",
|
|
544
|
+
tools: [],
|
|
545
|
+
operations,
|
|
546
|
+
actions: [{
|
|
547
|
+
description: "Hub-level actions",
|
|
548
|
+
catalog: [{
|
|
549
|
+
name: "listModels",
|
|
550
|
+
description: "List every bundled model with vendor + type metadata",
|
|
551
|
+
inputSchema: LIST_MODELS_SCHEMA
|
|
552
|
+
}, {
|
|
553
|
+
name: "refresh",
|
|
554
|
+
description: "Clear per-model SDK caches",
|
|
555
|
+
inputSchema: {
|
|
556
|
+
type: "object",
|
|
557
|
+
properties: {}
|
|
558
|
+
}
|
|
559
|
+
}],
|
|
560
|
+
discovery: {
|
|
561
|
+
pathTemplate: "/.actions",
|
|
562
|
+
note: "Hub-level actions: listModels, refresh"
|
|
563
|
+
}
|
|
564
|
+
}, {
|
|
565
|
+
description: "Model-level actions (varies by model type)",
|
|
566
|
+
catalog: [{
|
|
567
|
+
name: "chat",
|
|
568
|
+
description: "Chat inference (chat models)",
|
|
569
|
+
inputSchema: CHAT_SCHEMA
|
|
570
|
+
}, {
|
|
571
|
+
name: "embed",
|
|
572
|
+
description: "Embeddings (embed models)",
|
|
573
|
+
inputSchema: EMBED_SCHEMA
|
|
574
|
+
}],
|
|
575
|
+
discovery: {
|
|
576
|
+
pathTemplate: "/models/:canonical/.actions",
|
|
577
|
+
note: "Model-level actions depend on type. Chat models expose 'chat', embedding models expose 'embed'."
|
|
578
|
+
}
|
|
579
|
+
}]
|
|
580
|
+
};
|
|
581
|
+
return this.buildEntry("/.meta/.capabilities", {
|
|
582
|
+
content: manifest,
|
|
583
|
+
meta: {
|
|
584
|
+
kind: "afs:capabilities",
|
|
585
|
+
...operations
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
async explainRoot(_ctx) {
|
|
590
|
+
const models = await this.ensureModelsLoaded();
|
|
591
|
+
const typesWithEntries = TYPES.filter((t) => models.some((m) => m.type === t));
|
|
592
|
+
return {
|
|
593
|
+
format: "markdown",
|
|
594
|
+
content: [
|
|
595
|
+
`# ${this.name}`,
|
|
596
|
+
"",
|
|
597
|
+
this.description ?? "",
|
|
598
|
+
"",
|
|
599
|
+
`- **baseURL**: \`${this.baseURL}\``,
|
|
600
|
+
`- **listStrategy**: \`${this.listStrategy}\``,
|
|
601
|
+
`- **models**: ${models.length} (${typesWithEntries.join(", ")})`,
|
|
602
|
+
"",
|
|
603
|
+
"## Hub Contract",
|
|
604
|
+
"- `/.actions/listModels` → canonical model list",
|
|
605
|
+
"- `/models/{canonical}/.actions/chat` (or `embed`, `generateImage`, `generateVideo`)"
|
|
606
|
+
].join("\n")
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
async listRootActions(_ctx) {
|
|
610
|
+
return { data: HUB_ACTIONS.map((a) => this.buildEntry((0, ufo.joinURL)("/.actions", a.name), {
|
|
611
|
+
content: {
|
|
612
|
+
description: a.description,
|
|
613
|
+
schema: a.schema
|
|
614
|
+
},
|
|
615
|
+
meta: { kind: "afs:executable" }
|
|
616
|
+
})) };
|
|
617
|
+
}
|
|
618
|
+
async execListModels(_ctx, args) {
|
|
619
|
+
const models = await this.ensureModelsLoaded();
|
|
620
|
+
const requested = typeof args.type === "string" ? args.type : void 0;
|
|
621
|
+
const entries = [];
|
|
622
|
+
for (const m of models) {
|
|
623
|
+
if (requested && m.type !== requested) continue;
|
|
624
|
+
entries.push({
|
|
625
|
+
model: m.model,
|
|
626
|
+
nativeModelId: m.nativeModelId,
|
|
627
|
+
type: m.type,
|
|
628
|
+
vendor: m.vendor,
|
|
629
|
+
available: m.available ?? true
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
return {
|
|
633
|
+
success: true,
|
|
634
|
+
data: { entries }
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
async execRefresh() {
|
|
638
|
+
this.chatCache.clear();
|
|
639
|
+
this.embedCache.clear();
|
|
640
|
+
let refetchError = null;
|
|
641
|
+
if (this.listStrategy === "endpoint") {
|
|
642
|
+
this.cachedModels = null;
|
|
643
|
+
try {
|
|
644
|
+
await this.ensureModelsLoaded();
|
|
645
|
+
} catch (err) {
|
|
646
|
+
refetchError = err instanceof Error ? err.message : String(err);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return {
|
|
650
|
+
success: true,
|
|
651
|
+
data: {
|
|
652
|
+
message: refetchError ? `Caches cleared; endpoint re-fetch failed (${refetchError})` : "Gateway caches cleared.",
|
|
653
|
+
listStrategy: this.listStrategy,
|
|
654
|
+
modelCount: this.models.length
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
async listModels(_ctx) {
|
|
659
|
+
return { data: (await this.ensureModelsLoaded()).map((m) => this.buildEntry((0, ufo.joinURL)("/models", m.model), {
|
|
660
|
+
content: {
|
|
661
|
+
model: m.model,
|
|
662
|
+
vendor: m.vendor,
|
|
663
|
+
type: m.type,
|
|
664
|
+
nativeModelId: m.nativeModelId
|
|
665
|
+
},
|
|
666
|
+
meta: {
|
|
667
|
+
childrenCount: 2,
|
|
668
|
+
kind: "ai-gateway:model",
|
|
669
|
+
vendor: m.vendor,
|
|
670
|
+
capacity: { type: m.type }
|
|
671
|
+
}
|
|
672
|
+
})) };
|
|
673
|
+
}
|
|
674
|
+
async readModels(_ctx) {
|
|
675
|
+
const models = await this.ensureModelsLoaded();
|
|
676
|
+
return this.buildEntry("/models", {
|
|
677
|
+
content: { count: models.length },
|
|
678
|
+
meta: {
|
|
679
|
+
childrenCount: models.length,
|
|
680
|
+
kind: "ai-gateway:directory"
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
async metaModels(_ctx) {
|
|
685
|
+
const models = await this.ensureModelsLoaded();
|
|
686
|
+
return this.buildEntry("/models/.meta", {
|
|
687
|
+
content: { count: models.length },
|
|
688
|
+
meta: { kind: "ai-gateway:directory" }
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
async statModels(_ctx) {
|
|
692
|
+
const models = await this.ensureModelsLoaded();
|
|
693
|
+
return { data: this.buildEntry("/models", { meta: {
|
|
694
|
+
childrenCount: models.length,
|
|
695
|
+
kind: "ai-gateway:directory"
|
|
696
|
+
} }) };
|
|
697
|
+
}
|
|
698
|
+
async listModel(ctx) {
|
|
699
|
+
await this.ensureModelsLoaded();
|
|
700
|
+
const base = (0, ufo.joinURL)("/models", this.findModel(ctx.params.canonical).model);
|
|
701
|
+
return { data: [this.buildEntry((0, ufo.joinURL)(base, ".meta"), { meta: { kind: "ai-gateway:model-meta" } })] };
|
|
702
|
+
}
|
|
703
|
+
async readModel(ctx) {
|
|
704
|
+
await this.ensureModelsLoaded();
|
|
705
|
+
const m = this.findModel(ctx.params.canonical);
|
|
706
|
+
return this.buildEntry((0, ufo.joinURL)("/models", m.model), {
|
|
707
|
+
content: {
|
|
708
|
+
model: m.model,
|
|
709
|
+
nativeModelId: m.nativeModelId,
|
|
710
|
+
vendor: m.vendor,
|
|
711
|
+
type: m.type,
|
|
712
|
+
aliases: m.aliases ?? []
|
|
713
|
+
},
|
|
714
|
+
meta: {
|
|
715
|
+
childrenCount: 1,
|
|
716
|
+
kind: "ai-gateway:model",
|
|
717
|
+
vendor: m.vendor,
|
|
718
|
+
capacity: { type: m.type }
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
async metaModel(ctx) {
|
|
723
|
+
await this.ensureModelsLoaded();
|
|
724
|
+
const m = this.findModel(ctx.params.canonical);
|
|
725
|
+
return this.buildEntry((0, ufo.joinURL)("/models", m.model, ".meta"), {
|
|
726
|
+
content: {
|
|
727
|
+
model: m.model,
|
|
728
|
+
nativeModelId: m.nativeModelId,
|
|
729
|
+
vendor: m.vendor,
|
|
730
|
+
capacity: { type: m.type },
|
|
731
|
+
aliases: m.aliases ?? [],
|
|
732
|
+
available: m.available ?? true
|
|
733
|
+
},
|
|
734
|
+
meta: { kind: "ai-gateway:model-meta" }
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
async statModel(ctx) {
|
|
738
|
+
await this.ensureModelsLoaded();
|
|
739
|
+
const m = this.findModel(ctx.params.canonical);
|
|
740
|
+
return { data: this.buildEntry((0, ufo.joinURL)("/models", m.model), { meta: {
|
|
741
|
+
childrenCount: 1,
|
|
742
|
+
kind: "ai-gateway:model",
|
|
743
|
+
vendor: m.vendor,
|
|
744
|
+
capacity: { type: m.type }
|
|
745
|
+
} }) };
|
|
746
|
+
}
|
|
747
|
+
async explainModel(ctx) {
|
|
748
|
+
await this.ensureModelsLoaded();
|
|
749
|
+
const m = this.findModel(ctx.params.canonical);
|
|
750
|
+
const action = TYPE_ACTION[m.type];
|
|
751
|
+
return {
|
|
752
|
+
format: "markdown",
|
|
753
|
+
content: [
|
|
754
|
+
`# ${m.model}`,
|
|
755
|
+
"",
|
|
756
|
+
`Vendor: **${m.vendor}** · Type: **${m.type}**`,
|
|
757
|
+
"",
|
|
758
|
+
`Native id: \`${m.nativeModelId}\``,
|
|
759
|
+
"",
|
|
760
|
+
"## Invoke",
|
|
761
|
+
`\`exec ${(0, ufo.joinURL)("/models", m.model, ".actions", action)} { … }\``
|
|
762
|
+
].join("\n")
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
async listModelActions(ctx) {
|
|
766
|
+
await this.ensureModelsLoaded();
|
|
767
|
+
const m = this.findModel(ctx.params.canonical);
|
|
768
|
+
const base = (0, ufo.joinURL)("/models", m.model, ".actions");
|
|
769
|
+
const action = TYPE_ACTION[m.type];
|
|
770
|
+
const schema = m.type === "chat" ? CHAT_SCHEMA : m.type === "embed" ? EMBED_SCHEMA : { type: "object" };
|
|
771
|
+
return { data: [this.buildEntry((0, ufo.joinURL)(base, action), {
|
|
772
|
+
content: {
|
|
773
|
+
description: `${m.type} inference on ${m.model}`,
|
|
774
|
+
schema
|
|
775
|
+
},
|
|
776
|
+
meta: { kind: "afs:executable" }
|
|
777
|
+
})] };
|
|
778
|
+
}
|
|
779
|
+
async execChat(ctx, args) {
|
|
780
|
+
await this.ensureModelsLoaded();
|
|
781
|
+
const m = this.findModel(ctx.params.canonical);
|
|
782
|
+
if (m.type !== "chat") return {
|
|
783
|
+
success: false,
|
|
784
|
+
error: {
|
|
785
|
+
code: "INVALID_ACTION",
|
|
786
|
+
message: `Model '${m.model}' is type '${m.type}', not 'chat'.`
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
try {
|
|
790
|
+
const messages = this.buildMessages(args);
|
|
791
|
+
if (messages.length === 0) return {
|
|
792
|
+
success: false,
|
|
793
|
+
error: {
|
|
794
|
+
code: "MISSING_PARAM",
|
|
795
|
+
message: "'prompt' or 'messages' is required"
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
if (m.vendor === "anthropic" && messagesHaveCacheMarkers(messages)) return await this.rawChatFetch(m, messages, args, ctx);
|
|
799
|
+
const client = this.getChatClient(m.nativeModelId);
|
|
800
|
+
const input = { messages };
|
|
801
|
+
const modelOptions = {};
|
|
802
|
+
if (typeof args.temperature === "number") modelOptions.temperature = args.temperature;
|
|
803
|
+
if (typeof args.topP === "number") modelOptions.topP = args.topP;
|
|
804
|
+
if (Object.keys(modelOptions).length > 0) input.modelOptions = modelOptions;
|
|
805
|
+
if (args.responseFormat) input.responseFormat = normalizeResponseFormatForOpenAI(args.responseFormat);
|
|
806
|
+
if (args.tools !== void 0) input.tools = args.tools;
|
|
807
|
+
if (args.toolChoice !== void 0) input.toolChoice = args.toolChoice;
|
|
808
|
+
const onChunk = ctx.options?.onChunk;
|
|
809
|
+
if (onChunk) {
|
|
810
|
+
const output = await this.streamChat(client, input, onChunk);
|
|
811
|
+
return {
|
|
812
|
+
success: true,
|
|
813
|
+
data: this.buildChatData(m, output)
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
const result = await client.invoke(input);
|
|
817
|
+
return {
|
|
818
|
+
success: true,
|
|
819
|
+
data: this.buildChatData(m, result)
|
|
820
|
+
};
|
|
821
|
+
} catch (err) {
|
|
822
|
+
return inferenceError(m.model, err);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Drive a streaming chat completion, relaying text/thoughts deltas to
|
|
827
|
+
* `onChunk`, and return the fully-merged `ChatModelOutput` (identical to the
|
|
828
|
+
* buffered `invoke()` result). Runtime-agnostic: uses `getReader()` so it
|
|
829
|
+
* runs on the Cloudflare workerd isolate as well as Node.
|
|
830
|
+
*/
|
|
831
|
+
async streamChat(client, input, onChunk) {
|
|
832
|
+
const reader = (await client.invoke(input, { streaming: true })).getReader();
|
|
833
|
+
let merged = {};
|
|
834
|
+
try {
|
|
835
|
+
for (;;) {
|
|
836
|
+
const { done, value } = await reader.read();
|
|
837
|
+
if (done) break;
|
|
838
|
+
const chunk = value;
|
|
839
|
+
merged = (0, _aigne_model_base.mergeModelResponseChunk)(merged, chunk);
|
|
840
|
+
const textDelta = chunk.delta?.text;
|
|
841
|
+
if (textDelta?.text) onChunk({ text: textDelta.text });
|
|
842
|
+
if (textDelta?.thoughts) onChunk({ thoughts: textDelta.thoughts });
|
|
843
|
+
}
|
|
844
|
+
} finally {
|
|
845
|
+
reader.releaseLock();
|
|
846
|
+
}
|
|
847
|
+
return merged;
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Raw HTTP fetch path for Anthropic models that carry prompt-cache markers.
|
|
851
|
+
* Bypasses OpenAIChatModel.contentsFromInputMessages so cache_control is not
|
|
852
|
+
* stripped before it reaches the gateway / Anthropic API. Handles both
|
|
853
|
+
* buffered and SSE-streaming modes.
|
|
854
|
+
*/
|
|
855
|
+
async rawChatFetch(m, messages, args, ctx) {
|
|
856
|
+
const onChunk = ctx.options?.onChunk;
|
|
857
|
+
const body = {
|
|
858
|
+
model: m.nativeModelId,
|
|
859
|
+
messages: serializeMessagesForWire(messages)
|
|
860
|
+
};
|
|
861
|
+
if (typeof args.temperature === "number") body.temperature = args.temperature;
|
|
862
|
+
if (typeof args.topP === "number") body.top_p = args.topP;
|
|
863
|
+
if (args.tools !== void 0) body.tools = args.tools;
|
|
864
|
+
if (args.toolChoice !== void 0) body.tool_choice = args.toolChoice;
|
|
865
|
+
if (args.responseFormat) body.response_format = toWireResponseFormat(normalizeResponseFormatForOpenAI(args.responseFormat));
|
|
866
|
+
if (onChunk) body.stream = true;
|
|
867
|
+
let resp;
|
|
868
|
+
try {
|
|
869
|
+
resp = await fetch(`${this.baseURL}/chat/completions`, {
|
|
870
|
+
method: "POST",
|
|
871
|
+
headers: {
|
|
872
|
+
"Content-Type": "application/json",
|
|
873
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
874
|
+
...this.extraHeaders
|
|
875
|
+
},
|
|
876
|
+
body: JSON.stringify(body)
|
|
877
|
+
});
|
|
878
|
+
} catch (err) {
|
|
879
|
+
return inferenceError(m.model, err);
|
|
880
|
+
}
|
|
881
|
+
if (!resp.ok) {
|
|
882
|
+
const errText = await resp.text().catch(() => resp.statusText);
|
|
883
|
+
return inferenceError(m.model, /* @__PURE__ */ new Error(`HTTP ${resp.status}: ${errText}`));
|
|
884
|
+
}
|
|
885
|
+
if (onChunk) return this.streamRawChat(m, resp, onChunk);
|
|
886
|
+
try {
|
|
887
|
+
const data = await resp.json();
|
|
888
|
+
return {
|
|
889
|
+
success: true,
|
|
890
|
+
data: this.buildChatData(m, openAiRespToOutput(data))
|
|
891
|
+
};
|
|
892
|
+
} catch (err) {
|
|
893
|
+
return inferenceError(m.model, err);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
/** Consume an OpenAI-compat SSE stream, forwarding text deltas via onChunk. */
|
|
897
|
+
async streamRawChat(m, resp, onChunk) {
|
|
898
|
+
const reader = resp.body.getReader();
|
|
899
|
+
const decoder = new TextDecoder();
|
|
900
|
+
let buffer = "";
|
|
901
|
+
let fullText = "";
|
|
902
|
+
let usage;
|
|
903
|
+
const toolCallsByIndex = /* @__PURE__ */ new Map();
|
|
904
|
+
try {
|
|
905
|
+
for (;;) {
|
|
906
|
+
const { done, value } = await reader.read();
|
|
907
|
+
if (done) break;
|
|
908
|
+
buffer += decoder.decode(value, { stream: true });
|
|
909
|
+
const lines = buffer.split("\n");
|
|
910
|
+
buffer = lines.pop() ?? "";
|
|
911
|
+
for (const line of lines) {
|
|
912
|
+
if (!line.startsWith("data: ")) continue;
|
|
913
|
+
const raw = line.slice(6).trim();
|
|
914
|
+
if (raw === "[DONE]") continue;
|
|
915
|
+
try {
|
|
916
|
+
const chunk = JSON.parse(raw);
|
|
917
|
+
const delta = chunk.choices?.[0]?.delta;
|
|
918
|
+
const text = delta?.content;
|
|
919
|
+
if (text) {
|
|
920
|
+
fullText += text;
|
|
921
|
+
onChunk({ text });
|
|
922
|
+
}
|
|
923
|
+
const deltaToolCalls = delta?.tool_calls;
|
|
924
|
+
if (Array.isArray(deltaToolCalls)) for (const tc of deltaToolCalls) {
|
|
925
|
+
const index = typeof tc.index === "number" ? tc.index : 0;
|
|
926
|
+
const fn = tc.function;
|
|
927
|
+
const existing = toolCallsByIndex.get(index) ?? { arguments: "" };
|
|
928
|
+
if (typeof tc.id === "string") existing.id = tc.id;
|
|
929
|
+
if (typeof tc.type === "string") existing.type = tc.type;
|
|
930
|
+
if (typeof fn?.name === "string") existing.name = fn.name;
|
|
931
|
+
if (typeof fn?.arguments === "string") existing.arguments += fn.arguments;
|
|
932
|
+
toolCallsByIndex.set(index, existing);
|
|
933
|
+
}
|
|
934
|
+
if (chunk.usage) {
|
|
935
|
+
const u = chunk.usage;
|
|
936
|
+
usage = {
|
|
937
|
+
inputTokens: u.prompt_tokens ?? 0,
|
|
938
|
+
outputTokens: u.completion_tokens ?? 0
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
} catch {}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
} catch (err) {
|
|
945
|
+
return inferenceError(m.model, err);
|
|
946
|
+
} finally {
|
|
947
|
+
reader.releaseLock();
|
|
948
|
+
}
|
|
949
|
+
const toolCalls = parseWireToolCalls(toolCallsByIndex.size > 0 ? [...toolCallsByIndex.entries()].sort(([a], [b]) => a - b).map(([, tc]) => ({
|
|
950
|
+
id: tc.id,
|
|
951
|
+
type: tc.type ?? "function",
|
|
952
|
+
function: {
|
|
953
|
+
name: tc.name,
|
|
954
|
+
arguments: tc.arguments
|
|
955
|
+
}
|
|
956
|
+
})) : void 0);
|
|
957
|
+
return {
|
|
958
|
+
success: true,
|
|
959
|
+
data: this.buildChatData(m, {
|
|
960
|
+
text: fullText,
|
|
961
|
+
...toolCalls ? { toolCalls } : {},
|
|
962
|
+
...usage ? { usage } : {}
|
|
963
|
+
})
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Build the `/.actions/chat` result `data` from a `ChatModelOutput`. Shared
|
|
968
|
+
* by the buffered and streaming paths so they can never drift. Forwards
|
|
969
|
+
* structured-output JSON + reasoning trace + tool calls + usage (without
|
|
970
|
+
* this, callers using `responseFormat: { type: "json_schema" }` lose the
|
|
971
|
+
* parsed object — caused scan-spam to log `"reason": "no result"`).
|
|
972
|
+
*/
|
|
973
|
+
buildChatData(m, output) {
|
|
974
|
+
const data = {
|
|
975
|
+
model: m.model,
|
|
976
|
+
text: output.text ?? null
|
|
977
|
+
};
|
|
978
|
+
if (output.json !== void 0) data.json = output.json;
|
|
979
|
+
if (output.thoughts !== void 0) data.thoughts = output.thoughts;
|
|
980
|
+
if (output.toolCalls) data.toolCalls = output.toolCalls;
|
|
981
|
+
if (output.usage) data.usage = output.usage;
|
|
982
|
+
return data;
|
|
983
|
+
}
|
|
984
|
+
async execEmbed(ctx, args) {
|
|
985
|
+
await this.ensureModelsLoaded();
|
|
986
|
+
const m = this.findModel(ctx.params.canonical);
|
|
987
|
+
if (m.type !== "embed") return {
|
|
988
|
+
success: false,
|
|
989
|
+
error: {
|
|
990
|
+
code: "INVALID_ACTION",
|
|
991
|
+
message: `Model '${m.model}' is type '${m.type}', not 'embed'.`
|
|
992
|
+
}
|
|
993
|
+
};
|
|
994
|
+
const input = args.input ?? args.text;
|
|
995
|
+
if (input === void 0) return {
|
|
996
|
+
success: false,
|
|
997
|
+
error: {
|
|
998
|
+
code: "MISSING_PARAM",
|
|
999
|
+
message: "'input' (or 'text') is required"
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
try {
|
|
1003
|
+
const output = await this.getEmbedClient(m.nativeModelId).invoke({ input });
|
|
1004
|
+
return {
|
|
1005
|
+
success: true,
|
|
1006
|
+
data: {
|
|
1007
|
+
model: m.model,
|
|
1008
|
+
embeddings: output.embeddings,
|
|
1009
|
+
...output.usage ? { usage: output.usage } : {}
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
1012
|
+
} catch (err) {
|
|
1013
|
+
return inferenceError(m.model, err);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
async execGenerateImage(ctx) {
|
|
1017
|
+
await this.ensureModelsLoaded();
|
|
1018
|
+
return {
|
|
1019
|
+
success: false,
|
|
1020
|
+
error: {
|
|
1021
|
+
code: "NOT_IMPLEMENTED",
|
|
1022
|
+
message: `generateImage for ${this.findModel(ctx.params.canonical).model} not supported by this provider yet.`
|
|
1023
|
+
}
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
async execGenerateVideo(ctx) {
|
|
1027
|
+
await this.ensureModelsLoaded();
|
|
1028
|
+
return {
|
|
1029
|
+
success: false,
|
|
1030
|
+
error: {
|
|
1031
|
+
code: "NOT_IMPLEMENTED",
|
|
1032
|
+
message: `generateVideo for ${this.findModel(ctx.params.canonical).model} not supported by this provider yet.`
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
buildMessages(args) {
|
|
1037
|
+
const raw = args.messages;
|
|
1038
|
+
if (Array.isArray(raw)) return raw.map((m) => normalizeMessage(m));
|
|
1039
|
+
if (typeof args.prompt === "string" && args.prompt.length > 0) return [{
|
|
1040
|
+
role: "user",
|
|
1041
|
+
content: args.prompt
|
|
1042
|
+
}];
|
|
1043
|
+
return [];
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
require_decorate.__decorate([(0, _aigne_afs.List)("/")], AFSAIGateway.prototype, "listRoot", null);
|
|
1047
|
+
require_decorate.__decorate([(0, _aigne_afs.Read)("/")], AFSAIGateway.prototype, "readRoot", null);
|
|
1048
|
+
require_decorate.__decorate([(0, _aigne_afs.Meta)("/")], AFSAIGateway.prototype, "rootMeta", null);
|
|
1049
|
+
require_decorate.__decorate([(0, _aigne_afs.Stat)("/")], AFSAIGateway.prototype, "statRoot", null);
|
|
1050
|
+
require_decorate.__decorate([(0, _aigne_afs.Read)("/.meta/.capabilities")], AFSAIGateway.prototype, "readCapabilities", null);
|
|
1051
|
+
require_decorate.__decorate([(0, _aigne_afs.Explain)("/")], AFSAIGateway.prototype, "explainRoot", null);
|
|
1052
|
+
require_decorate.__decorate([(0, _aigne_afs.Actions)("/")], AFSAIGateway.prototype, "listRootActions", null);
|
|
1053
|
+
require_decorate.__decorate([_aigne_afs.Actions.Exec("/", "listModels", void 0, { effect: "read" })], AFSAIGateway.prototype, "execListModels", null);
|
|
1054
|
+
require_decorate.__decorate([_aigne_afs.Actions.Exec("/", "refresh", void 0, { effect: "write" })], AFSAIGateway.prototype, "execRefresh", null);
|
|
1055
|
+
require_decorate.__decorate([(0, _aigne_afs.List)("/models")], AFSAIGateway.prototype, "listModels", null);
|
|
1056
|
+
require_decorate.__decorate([(0, _aigne_afs.Read)("/models")], AFSAIGateway.prototype, "readModels", null);
|
|
1057
|
+
require_decorate.__decorate([(0, _aigne_afs.Meta)("/models")], AFSAIGateway.prototype, "metaModels", null);
|
|
1058
|
+
require_decorate.__decorate([(0, _aigne_afs.Stat)("/models")], AFSAIGateway.prototype, "statModels", null);
|
|
1059
|
+
require_decorate.__decorate([(0, _aigne_afs.List)("/models/:canonical")], AFSAIGateway.prototype, "listModel", null);
|
|
1060
|
+
require_decorate.__decorate([(0, _aigne_afs.Read)("/models/:canonical")], AFSAIGateway.prototype, "readModel", null);
|
|
1061
|
+
require_decorate.__decorate([(0, _aigne_afs.Meta)("/models/:canonical")], AFSAIGateway.prototype, "metaModel", null);
|
|
1062
|
+
require_decorate.__decorate([(0, _aigne_afs.Stat)("/models/:canonical")], AFSAIGateway.prototype, "statModel", null);
|
|
1063
|
+
require_decorate.__decorate([(0, _aigne_afs.Explain)("/models/:canonical")], AFSAIGateway.prototype, "explainModel", null);
|
|
1064
|
+
require_decorate.__decorate([(0, _aigne_afs.Actions)("/models/:canonical")], AFSAIGateway.prototype, "listModelActions", null);
|
|
1065
|
+
require_decorate.__decorate([_aigne_afs.Actions.Exec("/models/:canonical", "chat", void 0, {
|
|
1066
|
+
effect: "read",
|
|
1067
|
+
billable: true
|
|
1068
|
+
})], AFSAIGateway.prototype, "execChat", null);
|
|
1069
|
+
require_decorate.__decorate([_aigne_afs.Actions.Exec("/models/:canonical", "embed", void 0, {
|
|
1070
|
+
effect: "read",
|
|
1071
|
+
billable: true
|
|
1072
|
+
})], AFSAIGateway.prototype, "execEmbed", null);
|
|
1073
|
+
require_decorate.__decorate([_aigne_afs.Actions.Exec("/models/:canonical", "generateImage", void 0, {
|
|
1074
|
+
effect: "read",
|
|
1075
|
+
billable: true
|
|
1076
|
+
})], AFSAIGateway.prototype, "execGenerateImage", null);
|
|
1077
|
+
require_decorate.__decorate([_aigne_afs.Actions.Exec("/models/:canonical", "generateVideo", void 0, {
|
|
1078
|
+
effect: "read",
|
|
1079
|
+
billable: true
|
|
1080
|
+
})], AFSAIGateway.prototype, "execGenerateVideo", null);
|
|
1081
|
+
|
|
1082
|
+
//#endregion
|
|
1083
|
+
exports.AFSAIGateway = AFSAIGateway;
|