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