@juspay/neurolink 12.48.0 → 12.48.2

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.
@@ -4,7 +4,7 @@
4
4
  * Koa is known for its elegant middleware composition using async/await
5
5
  */
6
6
  import { logger } from "../../utils/logger.js";
7
- import { AlreadyRunningError, ServerStopError, wrapError } from "../errors.js";
7
+ import { AlreadyRunningError, ServerAdapterError, ServerStopError, wrapError, } from "../errors.js";
8
8
  import { BaseServerAdapter } from "../abstract/baseServerAdapter.js";
9
9
  import { isErrorResponse } from "../utils/validation.js";
10
10
  /**
@@ -19,6 +19,10 @@ export class KoaServerAdapter extends BaseServerAdapter {
19
19
  rateLimitStore = new Map();
20
20
  rateLimitCleanupInterval;
21
21
  sockets = new Set();
22
+ // Middleware from registerMiddleware(). It runs through one dispatcher that
23
+ // is mounted ahead of the router: app.use() after the router is mounted
24
+ // would only ever see requests no route matched.
25
+ registeredMiddleware = [];
22
26
  constructor(neurolink, config = {}) {
23
27
  super(neurolink, config);
24
28
  }
@@ -59,21 +63,19 @@ export class KoaServerAdapter extends BaseServerAdapter {
59
63
  catch (error) {
60
64
  const err = error;
61
65
  const requestId = ctx.state.requestId;
62
- logger.error("[KoaAdapter] Request error", {
63
- requestId,
64
- error: err.message,
65
- stack: err.stack,
66
- });
67
- this.emit("error", {
66
+ this.logRequestError("KoaAdapter", requestId, err);
67
+ this.emitRequestError({
68
68
  requestId,
69
69
  error: err,
70
70
  timestamp: new Date(),
71
71
  });
72
- const statusCode = err.status || err.statusCode || 500;
72
+ const adapterError = error instanceof ServerAdapterError ? error : undefined;
73
+ const statusCode = adapterError?.getHttpStatus() || err.status || err.statusCode || 500;
73
74
  ctx.status = statusCode;
74
75
  ctx.body = {
75
76
  error: {
76
- code: statusCode === 500 ? "INTERNAL_ERROR" : `HTTP_${statusCode}`,
77
+ code: adapterError?.code ??
78
+ (statusCode === 500 ? "INTERNAL_ERROR" : `HTTP_${statusCode}`),
77
79
  message: statusCode === 500 ? "An internal error occurred" : err.message,
78
80
  },
79
81
  metadata: {
@@ -130,6 +132,7 @@ export class KoaServerAdapter extends BaseServerAdapter {
130
132
  });
131
133
  });
132
134
  }
135
+ this.app.use((ctx, next) => this.runRegisteredMiddleware(ctx, next));
133
136
  // Mount router
134
137
  this.app.use(this.router.routes());
135
138
  this.app.use(this.router.allowedMethods());
@@ -349,7 +352,7 @@ export class KoaServerAdapter extends BaseServerAdapter {
349
352
  * Register middleware with Koa
350
353
  */
351
354
  registerFrameworkMiddleware(middleware) {
352
- this.app.use(async (ctx, next) => {
355
+ this.registeredMiddleware.push(async (ctx, next) => {
353
356
  // Skip excluded paths
354
357
  if (middleware.excludePaths?.some((p) => ctx.path.startsWith(p))) {
355
358
  return next();
@@ -390,6 +393,21 @@ export class KoaServerAdapter extends BaseServerAdapter {
390
393
  }
391
394
  });
392
395
  }
396
+ /**
397
+ * Run registered middleware in registration order, then continue to the
398
+ * router. Reads the list per request, so later registrations apply too.
399
+ */
400
+ runRegisteredMiddleware(ctx, next) {
401
+ const dispatch = async (index) => {
402
+ const middleware = this.registeredMiddleware[index];
403
+ if (!middleware) {
404
+ await next();
405
+ return;
406
+ }
407
+ await middleware(ctx, () => dispatch(index + 1));
408
+ };
409
+ return dispatch(0);
410
+ }
393
411
  /**
394
412
  * Start the Koa server
395
413
  */
@@ -1420,6 +1420,7 @@ export type ServerInstance = {
1420
1420
  start: () => Promise<void>;
1421
1421
  stop: () => Promise<void>;
1422
1422
  registerRouteGroup: (group: import("./server.js").RouteGroup) => void;
1423
+ registerMiddleware: (middleware: import("./server.js").MiddlewareDefinition) => void;
1423
1424
  listRoutes?: () => import("./server.js").RouteDefinition[];
1424
1425
  };
1425
1426
  /** Persisted state for a running `neurolink serve` process. */
@@ -10339,7 +10339,7 @@
10339
10339
  {"objectID":"7521a69b63b695e0520c8bdda8325d29e48dd83ae908720d7f7b410883378aac","title":"Speech-to-Text (STT)","url":"/docs/reference/provider-selection#speech-to-text-stt","content":"Best accuracy: whisper (OpenAI)\n\nBest streaming: deepgram (WebSocket real-time)\n\nBest for Google Cloud users: google-stt\n\nEnterprise: azure-stt","hierarchy":{"lvl0":"Reference","lvl1":"Provider Selection Guide","lvl2":"Speech-to-Text (STT)","lvl3":""}},
10340
10340
  {"objectID":"3427711eb9150365a61d73b04412297131d3907ea8bb6702490b0a17da835969","title":"For Cost Optimization","url":"/docs/reference/provider-selection#for-cost-optimization","content":"Implement multi-provider routing - Use free/cheap providers for simple tasks, premium for complex ones.","hierarchy":{"lvl0":"Reference","lvl1":"Provider Selection Guide","lvl2":"For Cost Optimization","lvl3":""}},
10341
10341
  {"objectID":"0b16af903a7247815151028845f383f4064871833468f92ce5bb6d17dc92c895","title":"Related Resources","url":"/docs/reference/provider-selection#related-resources","content":"Provider Comparison - Detailed feature and pricing comparison\nProvider Capabilities Audit - Technical compatibility matrix\nConfiguration Reference - Environment setup for all providers\nTroubleshooting - Common issues and solutions\nMulti-Provider Fallback Cookbook - Implementation patterns\nCost Optimization Cookbook - Strategies to reduce costs\nVoice Providers Comparison - TTS, STT, and Realtime provider matrix\nVoice Providers Index - Voice provider setup cards","hierarchy":{"lvl0":"Reference","lvl1":"Provider Selection Guide","lvl2":"Related Resources","lvl3":""}},
10342
- {"objectID":"cb85ceec305e148d1d4a1107ca240b5d762fca89bbb65cf67c847a0bfa0bbbda","title":"Srvr Cofiguratio Rfrc []","url":"/docs/reference/server-configuration","content":"Server Adapter Configuration Reference\n\nThis document provides a comprehensive reference for all configuration options available in NeuroLink Server Adapters.\n\nConfiguration via CLI\n\nIn addition to programmatic configuration, NeuroLink provides CLI commands to view and manage server settings.\n\nViewing Configuration\n\nModifying Configuration\n\nConfiguration File Location\n\nCLI configuration is stored at:\nConfig file: ~/.neurolink/server-config.json\nServer state: ~/.neurolink/server-state.json\n\nCLI vs Programmatic Configuration\n\n| Aspect | CLI Config | Programmatic Config |\n| ----------- | ----------------------------- | -------------------------------- |\n| Persistence | File-based, survives restarts | In-memory, per-instance |\n| Scope | Global defaults | Per-server instance |\n| Use Case | Development, quick changes | Production, fine-grained control |\n\nThe CLI configuration provides default values that can be overridden programmatically:\n\nServerAdapterConfig\n\nThe main configuration object for server adapters.\n\nCore Options\n\n| Option | Type | Default | Description |\n| ---------------------- | --------- | ----------- | ------------------------------------------------ |\n| port | number | 3000 | Server port to listen on |\n| host | string | \"0.0.0.0\" | Server host/interface to bind |\n| basePath | string | \"/api\" | Base path prefix for all routes |\n| timeout | number | 30000 | Request timeout in milliseconds |\n| enableMetrics | boolean | true | Enable metrics endpoint |\n| enableSwagger | boolean | false | Enable OpenAPI/Swagger documentation (see below) |\n| disableBuiltInHealth | boolean | false | Disable built-in health routes |\n\nOpenAPI/Swagger Documentation (enableSwagger)\n\nWhen enableSwagger is set to true, the server exposes interactive API documentation endpoints:\n\n| Endpoint | Description |\n| ----------------------------- | ---------------------------------------- |\n| GET {basePath}/openapi.json | OpenAPI 3.1 specification in JSON format |\n| GET {basePath}/openapi.yaml | OpenAPI 3.1 specification in YAML format |\n| GET {basePath}/docs | Interactive Swagger UI documentation |\n\nExample URLs (with default basePath /api):\nhttp://localhost:3000/api/openapi.json\nhttp://localhost:3000/api/openapi.yaml\nhttp://localhost:3000/api/docs\n\nThe Swagger UI provides an interactive interface where you can:\nBrowse all available API endpoints\nView request/response schemas\nTest API calls directly from the browser\nDownload the OpenAPI specification\n\nSecurity Consideration: In production environments, consider disabling enableSwagger to prevent exposing internal API structure. Alternatively, protect the documentation endpoints with authentication middleware.\n\nExample: Basic Configuration\n\nCORS Configuration\n\n| Option | Type | Default | Description |\n| ------------- | ---------- | ------------------------------------------------------ | ---------------------------------- |\n| enabled | boolean | true | Enable CORS support |\n| origins | string[] | [\"*\"] | Allowed origins |\n| methods | string[] | [\"GET\", \"POST\", \"PUT\", \"DELETE\", \"PATCH\", \"OPTIONS\"] | Allowed HTTP methods |\n| headers | string[] | [\"Content-Type\", \"Authorization\"] | Allowed headers |\n| credentials | boolean | false | Allow credentials |\n| maxAge | number | 86400 | Preflight cache max age in seconds |\n\nSecurity Warning: The default wildcard origin [\"*\"] allows requests from any domain. In production environments, always specify explicit allowed origins to prevent unauthorized cross-origin requests.\n\nExample: Restrictive CORS\n\nRate Limit Configuration\n\n| Option | Type | Default | Description |\n| -------------- | ---------- | ------------------------ | ------------------------------------------ |\n| enabled | boolean | true | Enable rate limiting |\n| windowMs | number | 900000 (15 min) | Time window in milliseconds |\n| maxRequests | number | 100 | Maximum requests per window |\n| message | string | \"Too many requests...\" | Error message when limit exceeded |\n| skipPaths | str","hierarchy":{"lvl0":"Reference","lvl1":"Srvr Cofiguratio Rfrc []","lvl2":"","lvl3":""}},
10342
+ {"objectID":"cb85ceec305e148d1d4a1107ca240b5d762fca89bbb65cf67c847a0bfa0bbbda","title":"Srvr Cofiguratio Rfrc []","url":"/docs/reference/server-configuration","content":"Server Adapter Configuration Reference\n\nThis document provides a comprehensive reference for all configuration options available in NeuroLink Server Adapters.\n\nConfiguration via CLI\n\nIn addition to programmatic configuration, NeuroLink provides CLI commands to view and manage server settings.\n\nViewing Configuration\n\nModifying Configuration\n\nConfiguration File Location\n\nCLI configuration is stored at:\nConfig file: ~/.neurolink/server-config.json\nServer state: ~/.neurolink/server-state.json\n\nCLI vs Programmatic Configuration\n\n| Aspect | CLI Config | Programmatic Config |\n| ----------- | ----------------------------- | -------------------------------- |\n| Persistence | File-based, survives restarts | In-memory, per-instance |\n| Scope | Global defaults | Per-server instance |\n| Use Case | Development, quick changes | Production, fine-grained control |\n\nRunning neurolink serve as a Service\n\nneurolink serve resolves each setting from, in order: its command-line flag, the\n--config JSON file, then the built-in default. The listen port additionally\nreads the PORT environment variable after the config file, which is what\ncontainer platforms set:\n\n| Setting | Order |\n| --------- | ----------------------------------------------------- |\n| Port | --port → config port → $PORT → 3000 |\n| Host | --host → config host → 0.0.0.0 |\n| Framework | --framework → config framework → hono |\n| Base path | --basePath → config basePath → /api |\n| CORS | --cors → config cors.enabled → enabled |\n| Rate | --rate-limit → config rateLimit.maxRequests → 100 |\n\nAPI key. Set NEUROLINK_SERVER_API_KEY to one key, or several separated by\ncommas for rotation, and every route except <basePath>/health* requires one,\nsent as Authorization: Bearer <key> or X-API-Key: <key>. Rejected requests get\n401. Without the variable the server is unauthenticated — keep it on a private\nnetwork — and the startup banner says so. neurolink server start reads the\nsame variable.\n\nContainer image. The repository's Dockerfile builds an image that runs\nneurolink serve as a non-root user, configured by environment only:\n\nUse GET /api/health/live for the liveness probe and GET /api/health/ready for\nreadiness; both stay open when an API key is set. SIGTERM drains and exits 0.\n\nThe CLI configuration provides default values that can be overridden programmatically:\n\nServerAdapterConfig\n\nThe main configuration object for server adapters.\n\nCore Options\n\n| Option | Type | Default | Description |\n| ---------------------- | --------- | ----------- | ------------------------------------------------ |\n| port | number | 3000 | Server port to listen on |\n| host | string | \"0.0.0.0\" | Server host/interface to bind |\n| basePath | string | \"/api\" | Base path prefix for all routes |\n| timeout | number | 30000 | Request timeout in milliseconds |\n| enableMetrics | boolean | true | Enable metrics endpoint |\n| enableSwagger | boolean | false | Enable OpenAPI/Swagger documentation (see below) |\n| disableBuiltInHealth | boolean | false | Disable built-in health routes |\n\nOpenAPI/Swagger Documentation (enableSwagger)\n\nWhen enableSwagger is set to true, the server exposes interactive API documentation endpoints:\n\n| Endpoint | Description |\n| ----------------------------- | ---------------------------------------- |\n| GET {basePath}/openapi.json | OpenAPI 3.1 specification in JSON format |\n| GET {basePath}/openapi.yaml | OpenAPI 3.1 specification in YAML format |\n| GET {basePath}/docs | Interactive Swagger UI documentation |\n\nExample URLs (with default basePath /api):\nhttp://localhost:3000/api/openapi.json\nhttp://localhost:3000/api/openapi.yaml\nhttp://localhost:3000/api/docs\n\nThe Swagger UI provides an interactive interface where you can:\nBrowse all available API endpoints\nView request/response schemas\nTest API calls directly from the browser\nDownload the OpenAPI specification\n\nSecurity Consideration: In production environments, consider disabling enableSwagger to prevent exposing internal API structure. Alternatively, protect the documentation endpoints with authentication middleware.\n\nExample: Basic Configuration\n\nCORS Configuration\n\n| Option | Type | Default | Description |\n| ------------- | ---------- | ------------------------------------------------------ | ---------------------------------- |\n| enabled | boolean | true ","hierarchy":{"lvl0":"Reference","lvl1":"Srvr Cofiguratio Rfrc []","lvl2":"","lvl3":""}},
10343
10343
  {"objectID":"d187276d2e01714a0fd53862da97600cad53354c8e420712cda9f3af1effef79","title":"Server Adapter Configuration Reference","url":"/docs/reference/server-configuration#server-adapter-configuration-reference","content":"This document provides a comprehensive reference for all configuration options available in NeuroLink Server Adapters.","hierarchy":{"lvl0":"Reference","lvl1":"Srvr Cofiguratio Rfrc []","lvl2":"Server Adapter Configuration Reference","lvl3":""}},
10344
10344
  {"objectID":"a1610833698011b8e9f9392407beea37119d8df2cc628412a1d8ffa1c536e834","title":"Configuration via CLI","url":"/docs/reference/server-configuration#configuration-via-cli","content":"In addition to programmatic configuration, NeuroLink provides CLI commands to view and manage server settings.","hierarchy":{"lvl0":"Reference","lvl1":"Srvr Cofiguratio Rfrc []","lvl2":"Configuration via CLI","lvl3":""}},
10345
10345
  {"objectID":"1159a0fdaab76d1050b04e8f660c9b39fa4d6720261cf7a826a599293e07cb5e","title":"Viewing Configuration","url":"/docs/reference/server-configuration#viewing-configuration","content":"`bash","hierarchy":{"lvl0":"Reference","lvl1":"Srvr Cofiguratio Rfrc []","lvl2":"Viewing Configuration","lvl3":""}},
@@ -10350,7 +10350,8 @@
10350
10350
  {"objectID":"53bb617878765e8a421b080b85198e2f81cc332607354f28671817b96faa8503","title":"Set configuration values","url":"/docs/reference/server-configuration#set-configuration-values","content":"neurolink server config --set defaultPort=8080\nneurolink server config --set defaultFramework=express\nneurolink server config --set cors.enabled=true\nneurolink server config --set rateLimit.maxRequests=200","hierarchy":{"lvl0":"Reference","lvl1":"Srvr Cofiguratio Rfrc []","lvl2":"Set configuration values","lvl3":""}},
10351
10351
  {"objectID":"6513dcd3aeda4fc3550260a66ce88edfd1f34e43ac235c85ddb191ec78e0f6e4","title":"Reset to defaults","url":"/docs/reference/server-configuration#reset-to-defaults","content":"neurolink server config --reset\n`","hierarchy":{"lvl0":"Reference","lvl1":"Srvr Cofiguratio Rfrc []","lvl2":"Reset to defaults","lvl3":""}},
10352
10352
  {"objectID":"0cd357f5a33e7cf878b93cbcf9aad870da227f44c99c28f4e648fdc9e6a7b360","title":"Configuration File Location","url":"/docs/reference/server-configuration#configuration-file-location","content":"CLI configuration is stored at:\nConfig file: ~/.neurolink/server-config.json\nServer state: ~/.neurolink/server-state.json","hierarchy":{"lvl0":"Reference","lvl1":"Srvr Cofiguratio Rfrc []","lvl2":"Configuration File Location","lvl3":""}},
10353
- {"objectID":"d2db092f2ae3972d7bb142238c67230664b2939eb4fa36af9d38bba749ebc5cd","title":"CLI vs Programmatic Configuration","url":"/docs/reference/server-configuration#cli-vs-programmatic-configuration","content":"| Aspect | CLI Config | Programmatic Config |\n| ----------- | ----------------------------- | -------------------------------- |\n| Persistence | File-based, survives restarts | In-memory, per-instance |\n| Scope | Global defaults | Per-server instance |\n| Use Case | Development, quick changes | Production, fine-grained control |\n\nThe CLI configuration provides default values that can be overridden programmatically:","hierarchy":{"lvl0":"Reference","lvl1":"Srvr Cofiguratio Rfrc []","lvl2":"CLI vs Programmatic Configuration","lvl3":""}},
10353
+ {"objectID":"d2db092f2ae3972d7bb142238c67230664b2939eb4fa36af9d38bba749ebc5cd","title":"CLI vs Programmatic Configuration","url":"/docs/reference/server-configuration#cli-vs-programmatic-configuration","content":"| Aspect | CLI Config | Programmatic Config |\n| ----------- | ----------------------------- | -------------------------------- |\n| Persistence | File-based, survives restarts | In-memory, per-instance |\n| Scope | Global defaults | Per-server instance |\n| Use Case | Development, quick changes | Production, fine-grained control |","hierarchy":{"lvl0":"Reference","lvl1":"Srvr Cofiguratio Rfrc []","lvl2":"CLI vs Programmatic Configuration","lvl3":""}},
10354
+ {"objectID":"7879b0a9ef2756d71baef46931796ce4a195665c14dcdede04d3aedbbda30307","title":"Running neurolink serve as a Service","url":"/docs/reference/server-configuration#running-neurolink-serve-as-a-service","content":"neurolink serve resolves each setting from, in order: its command-line flag, the\n--config JSON file, then the built-in default. The listen port additionally\nreads the PORT environment variable after the config file, which is what\ncontainer platforms set:\n\n| Setting | Order |\n| --------- | ----------------------------------------------------- |\n| Port | --port → config port → $PORT → 3000 |\n| Host | --host → config host → 0.0.0.0 |\n| Framework | --framework → config framework → hono |\n| Base path | --basePath → config basePath → /api |\n| CORS | --cors → config cors.enabled → enabled |\n| Rate | --rate-limit → config rateLimit.maxRequests → 100 |\n\nAPI key. Set NEUROLINK_SERVER_API_KEY to one key, or several separated by\ncommas for rotation, and every route except <basePath>/health* requires one,\nsent as Authorization: Bearer <key> or X-API-Key: <key>. Rejected requests get\n401. Without the variable the server is unauthenticated — keep it on a private\nnetwork — and the startup banner says so. neurolink server start reads the\nsame variable.\n\nContainer image. The repository's Dockerfile builds an image that runs\nneurolink serve as a non-root user, configured by environment only:\n\nUse GET /api/health/live for the liveness probe and GET /api/health/ready for\nreadiness; both stay open when an API key is set. SIGTERM drains and exits 0.\n\nThe CLI configuration provides default values that can be overridden programmatically:","hierarchy":{"lvl0":"Reference","lvl1":"Srvr Cofiguratio Rfrc []","lvl2":"Running neurolink serve as a Service","lvl3":""}},
10354
10355
  {"objectID":"ebf0e8b83f1c274852b8367930f04784c3499eb3af2bf086dc5c01462975c0c2","title":"ServerAdapterConfig","url":"/docs/reference/server-configuration#serveradapterconfig","content":"The main configuration object for server adapters.","hierarchy":{"lvl0":"Reference","lvl1":"Srvr Cofiguratio Rfrc []","lvl2":"ServerAdapterConfig","lvl3":""}},
10355
10356
  {"objectID":"677188568d1a1f62404df0b7a4c3655f9e19f645c9918030d046d433a6429339","title":"Core Options","url":"/docs/reference/server-configuration#core-options","content":"| Option | Type | Default | Description |\n| ---------------------- | --------- | ----------- | ------------------------------------------------ |\n| port | number | 3000 | Server port to listen on |\n| host | string | \"0.0.0.0\" | Server host/interface to bind |\n| basePath | string | \"/api\" | Base path prefix for all routes |\n| timeout | number | 30000 | Request timeout in milliseconds |\n| enableMetrics | boolean | true | Enable metrics endpoint |\n| enableSwagger | boolean | false | Enable OpenAPI/Swagger documentation (see below) |\n| disableBuiltInHealth | boolean | false | Disable built-in health routes |","hierarchy":{"lvl0":"Reference","lvl1":"Srvr Cofiguratio Rfrc []","lvl2":"Core Options","lvl3":""}},
10356
10357
  {"objectID":"679f1af42104e68e108d5e6009e16718cab67c8d2f3712b45dc516ba333b9c27","title":"OpenAPI/Swagger Documentation (enableSwagger)","url":"/docs/reference/server-configuration#openapiswagger-documentation-enableswagger","content":"When enableSwagger is set to true, the server exposes interactive API documentation endpoints:\n\n| Endpoint | Description |\n| ----------------------------- | ---------------------------------------- |\n| GET {basePath}/openapi.json | OpenAPI 3.1 specification in JSON format |\n| GET {basePath}/openapi.yaml | OpenAPI 3.1 specification in YAML format |\n| GET {basePath}/docs | Interactive Swagger UI documentation |\n\nExample URLs (with default basePath /api):\nhttp://localhost:3000/api/openapi.json\nhttp://localhost:3000/api/openapi.yaml\nhttp://localhost:3000/api/docs\n\nThe Swagger UI provides an interactive interface where you can:\nBrowse all available API endpoints\nView request/response schemas\nTest API calls directly from the browser\nDownload the OpenAPI specification\n\nSecurity Consideration: In production environments, consider disabling enableSwagger to prevent exposing internal API structure. Alternatively, protect the documentation endpoints with authentication middleware.","hierarchy":{"lvl0":"Reference","lvl1":"Srvr Cofiguratio Rfrc []","lvl2":"OpenAPI/Swagger Documentation (enableSwagger)","lvl3":""}},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "12.48.0",
3
+ "version": "12.48.2",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002/call) for routing, tool selection and context budgeting. MCP-native (4 transports), voice TTS/STT/realtime, RAG, agents, memory, compaction, 9 observability exporters. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · TypeSafe Jev and more.",
6
6
  "author": {