@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.
Files changed (42) hide show
  1. package/LICENSE.md +26 -0
  2. package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.cjs +11 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.mjs +10 -0
  4. package/dist/_virtual/rolldown_runtime.cjs +29 -0
  5. package/dist/ai-gateway.cjs +1083 -0
  6. package/dist/ai-gateway.d.cts +124 -0
  7. package/dist/ai-gateway.d.cts.map +1 -0
  8. package/dist/ai-gateway.d.mts +124 -0
  9. package/dist/ai-gateway.d.mts.map +1 -0
  10. package/dist/ai-gateway.mjs +1083 -0
  11. package/dist/ai-gateway.mjs.map +1 -0
  12. package/dist/index.cjs +14 -0
  13. package/dist/index.d.cts +5 -0
  14. package/dist/index.d.mts +5 -0
  15. package/dist/index.mjs +6 -0
  16. package/dist/models.cjs +155 -0
  17. package/dist/models.d.cts +8 -0
  18. package/dist/models.d.cts.map +1 -0
  19. package/dist/models.d.mts +8 -0
  20. package/dist/models.d.mts.map +1 -0
  21. package/dist/models.mjs +150 -0
  22. package/dist/models.mjs.map +1 -0
  23. package/dist/models2.cjs +97 -0
  24. package/dist/models2.mjs +96 -0
  25. package/dist/models2.mjs.map +1 -0
  26. package/dist/normalize.cjs +49 -0
  27. package/dist/normalize.d.cts +10 -0
  28. package/dist/normalize.d.cts.map +1 -0
  29. package/dist/normalize.d.mts +10 -0
  30. package/dist/normalize.d.mts.map +1 -0
  31. package/dist/normalize.mjs +47 -0
  32. package/dist/normalize.mjs.map +1 -0
  33. package/dist/types.cjs +80 -0
  34. package/dist/types.d.cts +104 -0
  35. package/dist/types.d.cts.map +1 -0
  36. package/dist/types.d.mts +104 -0
  37. package/dist/types.d.mts.map +1 -0
  38. package/dist/types.mjs +77 -0
  39. package/dist/types.mjs.map +1 -0
  40. package/manifest.json +40 -0
  41. package/models.json +145 -0
  42. 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;