@mlx-node/server 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/endpoints/messages.d.ts +13 -0
  2. package/dist/endpoints/messages.d.ts.map +1 -0
  3. package/dist/endpoints/messages.js +511 -0
  4. package/dist/endpoints/models.d.ts +5 -0
  5. package/dist/endpoints/models.d.ts.map +1 -0
  6. package/dist/endpoints/models.js +10 -0
  7. package/dist/endpoints/responses.d.ts +79 -0
  8. package/dist/endpoints/responses.d.ts.map +1 -0
  9. package/dist/endpoints/responses.js +2816 -0
  10. package/dist/errors.d.ts +43 -0
  11. package/dist/errors.d.ts.map +1 -0
  12. package/dist/errors.js +84 -0
  13. package/dist/handler.d.ts +18 -0
  14. package/dist/handler.d.ts.map +1 -0
  15. package/dist/handler.js +35 -0
  16. package/dist/index.d.ts +23 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +16 -0
  19. package/dist/mappers/anthropic-request.d.ts +9 -0
  20. package/dist/mappers/anthropic-request.d.ts.map +1 -0
  21. package/dist/mappers/anthropic-request.js +241 -0
  22. package/dist/mappers/anthropic-response.d.ts +14 -0
  23. package/dist/mappers/anthropic-response.d.ts.map +1 -0
  24. package/dist/mappers/anthropic-response.js +112 -0
  25. package/dist/mappers/request.d.ts +18 -0
  26. package/dist/mappers/request.d.ts.map +1 -0
  27. package/dist/mappers/request.js +206 -0
  28. package/dist/mappers/response.d.ts +13 -0
  29. package/dist/mappers/response.d.ts.map +1 -0
  30. package/dist/mappers/response.js +116 -0
  31. package/dist/pending-writes.d.ts +337 -0
  32. package/dist/pending-writes.d.ts.map +1 -0
  33. package/dist/pending-writes.js +468 -0
  34. package/dist/registry.d.ts +363 -0
  35. package/dist/registry.d.ts.map +1 -0
  36. package/dist/registry.js +497 -0
  37. package/dist/router.d.ts +6 -0
  38. package/dist/router.d.ts.map +1 -0
  39. package/dist/router.js +78 -0
  40. package/dist/server.d.ts +80 -0
  41. package/dist/server.d.ts.map +1 -0
  42. package/dist/server.js +158 -0
  43. package/dist/session-registry.d.ts +297 -0
  44. package/dist/session-registry.d.ts.map +1 -0
  45. package/dist/session-registry.js +403 -0
  46. package/dist/streaming.d.ts +7 -0
  47. package/dist/streaming.d.ts.map +1 -0
  48. package/dist/streaming.js +16 -0
  49. package/dist/tool-call-buffer.d.ts +26 -0
  50. package/dist/tool-call-buffer.d.ts.map +1 -0
  51. package/dist/tool-call-buffer.js +51 -0
  52. package/dist/transport-visibility.d.ts +56 -0
  53. package/dist/transport-visibility.d.ts.map +1 -0
  54. package/dist/transport-visibility.js +161 -0
  55. package/dist/types-anthropic.d.ts +144 -0
  56. package/dist/types-anthropic.d.ts.map +1 -0
  57. package/dist/types-anthropic.js +2 -0
  58. package/dist/types.d.ts +220 -0
  59. package/dist/types.d.ts.map +1 -0
  60. package/dist/types.js +2 -0
  61. package/package.json +36 -0
@@ -0,0 +1,43 @@
1
+ /** OpenAI- and Anthropic-compatible JSON error responses. */
2
+ import type { ServerResponse } from 'node:http';
3
+ export interface APIError {
4
+ type: string;
5
+ message: string;
6
+ code: string | null;
7
+ param: string | null;
8
+ }
9
+ export declare function sendError(res: ServerResponse, status: number, type: string, message: string, param?: string | null): void;
10
+ export declare function sendBadRequest(res: ServerResponse, message: string, param?: string): void;
11
+ export declare function sendNotFound(res: ServerResponse, message: string): void;
12
+ export declare function sendMethodNotAllowed(res: ServerResponse, allowed: string): void;
13
+ export declare function sendInternalError(res: ServerResponse, message: string): void;
14
+ /**
15
+ * 503 with `type: 'storage_timeout'`. Emitted by the responses endpoint when
16
+ * an in-flight `store.store(...)` gating a `previous_response_id` continuation
17
+ * fails to settle within `CHAIN_WRITE_WAIT_TIMEOUT_MS` and the final `getChain`
18
+ * probe still misses. 503 (not 404) because the write may yet land, so the
19
+ * same id can be retried — a 404 would wrongly mark it permanently invalid.
20
+ */
21
+ export declare function sendStorageTimeout(res: ServerResponse, message: string): void;
22
+ /**
23
+ * 429 with `type: 'rate_limit_error'` and `code: 'queue_full'`. Emitted by
24
+ * `/v1/responses` when the per-model execution queue is already holding
25
+ * `maxQueueDepth` waiters behind the current dispatch. Always sets
26
+ * `Retry-After: 1` (string seconds) so clients back off briefly before
27
+ * retrying — short enough to encourage a retry, long enough to avoid
28
+ * busy-looping the server.
29
+ */
30
+ export declare function sendRateLimit(res: ServerResponse, message: string): void;
31
+ export declare function sendAnthropicError(res: ServerResponse, status: number, type: string, message: string): void;
32
+ export declare function sendAnthropicBadRequest(res: ServerResponse, message: string): void;
33
+ export declare function sendAnthropicNotFound(res: ServerResponse, message: string): void;
34
+ export declare function sendAnthropicInternalError(res: ServerResponse, message: string): void;
35
+ export declare function sendAnthropicMethodNotAllowed(res: ServerResponse, allowed: string): void;
36
+ /**
37
+ * 429 Anthropic-shape rate-limit response. Mirror of {@link sendRateLimit}
38
+ * for `/v1/messages`. Body uses the `{ type: 'error', error: { type, message } }`
39
+ * envelope the rest of the Anthropic error helpers use; `Retry-After: 1`
40
+ * is set verbatim so clients can wait one second before retrying.
41
+ */
42
+ export declare function sendAnthropicRateLimit(res: ServerResponse, message: string): void;
43
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAE7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEhD,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,wBAAgB,SAAS,CACvB,GAAG,EAAE,cAAc,EACnB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GACpB,IAAI,CAWN;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAEzF;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAEvE;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAO/E;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAE5E;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAE7E;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAYxE;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAG3G;AAED,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAElF;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAEhF;AAED,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAErF;AAED,wBAAgB,6BAA6B,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAGxF;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAGjF"}
package/dist/errors.js ADDED
@@ -0,0 +1,84 @@
1
+ /** OpenAI- and Anthropic-compatible JSON error responses. */
2
+ export function sendError(res, status, type, message, param) {
3
+ const body = {
4
+ error: {
5
+ type,
6
+ message,
7
+ code: null,
8
+ param: param ?? null,
9
+ },
10
+ };
11
+ res.writeHead(status, { 'Content-Type': 'application/json' });
12
+ res.end(JSON.stringify(body));
13
+ }
14
+ export function sendBadRequest(res, message, param) {
15
+ sendError(res, 400, 'invalid_request_error', message, param);
16
+ }
17
+ export function sendNotFound(res, message) {
18
+ sendError(res, 404, 'not_found_error', message);
19
+ }
20
+ export function sendMethodNotAllowed(res, allowed) {
21
+ res.writeHead(405, { Allow: allowed, 'Content-Type': 'application/json' });
22
+ res.end(JSON.stringify({
23
+ error: { type: 'invalid_request_error', message: 'Method not allowed', code: null, param: null },
24
+ }));
25
+ }
26
+ export function sendInternalError(res, message) {
27
+ sendError(res, 500, 'server_error', message);
28
+ }
29
+ /**
30
+ * 503 with `type: 'storage_timeout'`. Emitted by the responses endpoint when
31
+ * an in-flight `store.store(...)` gating a `previous_response_id` continuation
32
+ * fails to settle within `CHAIN_WRITE_WAIT_TIMEOUT_MS` and the final `getChain`
33
+ * probe still misses. 503 (not 404) because the write may yet land, so the
34
+ * same id can be retried — a 404 would wrongly mark it permanently invalid.
35
+ */
36
+ export function sendStorageTimeout(res, message) {
37
+ sendError(res, 503, 'storage_timeout', message);
38
+ }
39
+ /**
40
+ * 429 with `type: 'rate_limit_error'` and `code: 'queue_full'`. Emitted by
41
+ * `/v1/responses` when the per-model execution queue is already holding
42
+ * `maxQueueDepth` waiters behind the current dispatch. Always sets
43
+ * `Retry-After: 1` (string seconds) so clients back off briefly before
44
+ * retrying — short enough to encourage a retry, long enough to avoid
45
+ * busy-looping the server.
46
+ */
47
+ export function sendRateLimit(res, message) {
48
+ res.writeHead(429, { 'Retry-After': '1', 'Content-Type': 'application/json' });
49
+ res.end(JSON.stringify({
50
+ error: {
51
+ type: 'rate_limit_error',
52
+ message,
53
+ code: 'queue_full',
54
+ param: null,
55
+ },
56
+ }));
57
+ }
58
+ export function sendAnthropicError(res, status, type, message) {
59
+ res.writeHead(status, { 'Content-Type': 'application/json' });
60
+ res.end(JSON.stringify({ type: 'error', error: { type, message } }));
61
+ }
62
+ export function sendAnthropicBadRequest(res, message) {
63
+ sendAnthropicError(res, 400, 'invalid_request_error', message);
64
+ }
65
+ export function sendAnthropicNotFound(res, message) {
66
+ sendAnthropicError(res, 404, 'not_found_error', message);
67
+ }
68
+ export function sendAnthropicInternalError(res, message) {
69
+ sendAnthropicError(res, 500, 'api_error', message);
70
+ }
71
+ export function sendAnthropicMethodNotAllowed(res, allowed) {
72
+ res.writeHead(405, { Allow: allowed, 'Content-Type': 'application/json' });
73
+ res.end(JSON.stringify({ type: 'error', error: { type: 'invalid_request_error', message: 'Method not allowed' } }));
74
+ }
75
+ /**
76
+ * 429 Anthropic-shape rate-limit response. Mirror of {@link sendRateLimit}
77
+ * for `/v1/messages`. Body uses the `{ type: 'error', error: { type, message } }`
78
+ * envelope the rest of the Anthropic error helpers use; `Retry-After: 1`
79
+ * is set verbatim so clients can wait one second before retrying.
80
+ */
81
+ export function sendAnthropicRateLimit(res, message) {
82
+ res.writeHead(429, { 'Retry-After': '1', 'Content-Type': 'application/json' });
83
+ res.end(JSON.stringify({ type: 'error', error: { type: 'rate_limit_error', message } }));
84
+ }
@@ -0,0 +1,18 @@
1
+ /** Composable `(req, res)` handler for node:http — usable standalone or mounted into an existing server. */
2
+ import type { IncomingMessage, ServerResponse } from 'node:http';
3
+ import type { ResponseStore } from '@mlx-node/core';
4
+ import type { ModelRegistry } from './registry.js';
5
+ export interface HandlerOptions {
6
+ /** Enable CORS headers (default: true). */
7
+ cors?: boolean;
8
+ /** Response store for previous_response_id support. */
9
+ store?: ResponseStore | null;
10
+ /**
11
+ * Retention (seconds) stamped as `expires_at` when committing a response row.
12
+ * When omitted, the endpoint falls back to its own default (see `responses.ts`
13
+ * and `ServerConfig.responseRetentionSec`).
14
+ */
15
+ responseRetentionSec?: number;
16
+ }
17
+ export declare function createHandler(registry: ModelRegistry, options?: HandlerOptions): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
18
+ //# sourceMappingURL=handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"AAAA,4GAA4G;AAE5G,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEjE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAGpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAGnD,MAAM,WAAW,cAAc;IAC7B,2CAA2C;IAC3C,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,uDAAuD;IACvD,KAAK,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IAC7B;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,aAAa,EACvB,OAAO,CAAC,EAAE,cAAc,GACvB,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAgC9D"}
@@ -0,0 +1,35 @@
1
+ /** Composable `(req, res)` handler for node:http — usable standalone or mounted into an existing server. */
2
+ import { sendInternalError } from './errors.js';
3
+ import { routeRequest } from './router.js';
4
+ export function createHandler(registry, options) {
5
+ const cors = options?.cors ?? true;
6
+ const store = options?.store ?? null;
7
+ const responseRetentionSec = options?.responseRetentionSec;
8
+ return async (req, res) => {
9
+ if (cors) {
10
+ res.setHeader('Access-Control-Allow-Origin', '*');
11
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
12
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, x-api-key, anthropic-version');
13
+ if (req.method === 'OPTIONS') {
14
+ res.writeHead(204);
15
+ res.end();
16
+ return;
17
+ }
18
+ }
19
+ // Returning the promise lets tests await the full lifecycle including
20
+ // post-`res.end()` bookkeeping (e.g. `SessionRegistry.adopt`). `http.createServer`
21
+ // ignores the return value, so this is transparent to production callers.
22
+ try {
23
+ await routeRequest(req, res, registry, store, responseRetentionSec);
24
+ }
25
+ catch (err) {
26
+ const message = err instanceof Error ? err.message : 'Internal server error';
27
+ if (!res.headersSent) {
28
+ sendInternalError(res, message);
29
+ }
30
+ else {
31
+ res.end();
32
+ }
33
+ }
34
+ };
35
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @mlx-node/server -- OpenAI Responses + Anthropic Messages server for MLX models.
3
+ *
4
+ * Exposes loaded models via `POST /v1/responses`, `POST /v1/messages`, and
5
+ * `GET /v1/models`, in both streaming (SSE) and non-streaming modes.
6
+ */
7
+ export { createServer } from './server.js';
8
+ export type { ServerConfig, ServerInstance } from './server.js';
9
+ /**
10
+ * Internal helpers re-exported for unit testing only. Not part of the
11
+ * supported public API — names may change without notice.
12
+ */
13
+ export { parseEnvSeconds as __parseEnvSeconds, parseEnvPositiveInt as __parseEnvPositiveInt } from './server.js';
14
+ export { createHandler } from './handler.js';
15
+ export type { HandlerOptions } from './handler.js';
16
+ export { ModelRegistry } from './registry.js';
17
+ export type { ServableModel, ModelEntry, ModelRegistryOptions } from './registry.js';
18
+ export { QueueFullError, SessionRegistry } from './session-registry.js';
19
+ export type { SessionLookupResult, SessionRegistryOptions } from './session-registry.js';
20
+ export type { ResponsesAPIRequest, ResponseObject, ResponseUsage, ResponseError, InputItem, InputMessage, InputFunctionCall, InputFunctionCallOutput, OutputItem, MessageOutputItem, ReasoningOutputItem, FunctionCallOutputItem, OutputTextPart, SummaryTextPart, ResponsesToolDefinition, ContentPart, InputTextPart, StreamEvent, } from './types.js';
21
+ export type { AnthropicMessagesRequest, AnthropicMessagesResponse, AnthropicMessage, AnthropicContentBlock, AnthropicTextContentBlock, AnthropicImageContentBlock, AnthropicToolResultContentBlock, AnthropicToolUseContentBlock, AnthropicThinkingContentBlock, AnthropicToolDefinition, AnthropicToolChoice, AnthropicResponseContent, AnthropicResponseTextBlock, AnthropicResponseThinkingBlock, AnthropicResponseToolUseBlock, AnthropicUsage, AnthropicStreamEvent, AnthropicMessageStartEvent, AnthropicContentBlockStartEvent, AnthropicContentBlockDeltaEvent, AnthropicContentBlockStopEvent, AnthropicMessageDeltaEvent, AnthropicMessageStopEvent, AnthropicDelta, AnthropicTextDelta, AnthropicThinkingDelta, AnthropicInputJsonDelta, SystemBlock, } from './types-anthropic.js';
22
+ export { writeSSEEvent, beginSSE, endSSE } from './streaming.js';
23
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAEhE;;;GAGG;AACH,OAAO,EAAE,eAAe,IAAI,iBAAiB,EAAE,mBAAmB,IAAI,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAEjH,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAErF,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxE,YAAY,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAEzF,YAAY,EACV,mBAAmB,EACnB,cAAc,EACd,aAAa,EACb,aAAa,EACb,SAAS,EACT,YAAY,EACZ,iBAAiB,EACjB,uBAAuB,EACvB,UAAU,EACV,iBAAiB,EACjB,mBAAmB,EACnB,sBAAsB,EACtB,cAAc,EACd,eAAe,EACf,uBAAuB,EACvB,WAAW,EACX,aAAa,EACb,WAAW,GACZ,MAAM,YAAY,CAAC;AAEpB,YAAY,EACV,wBAAwB,EACxB,yBAAyB,EACzB,gBAAgB,EAChB,qBAAqB,EACrB,yBAAyB,EACzB,0BAA0B,EAC1B,+BAA+B,EAC/B,4BAA4B,EAC5B,6BAA6B,EAC7B,uBAAuB,EACvB,mBAAmB,EACnB,wBAAwB,EACxB,0BAA0B,EAC1B,8BAA8B,EAC9B,6BAA6B,EAC7B,cAAc,EACd,oBAAoB,EACpB,0BAA0B,EAC1B,+BAA+B,EAC/B,+BAA+B,EAC/B,8BAA8B,EAC9B,0BAA0B,EAC1B,yBAAyB,EACzB,cAAc,EACd,kBAAkB,EAClB,sBAAsB,EACtB,uBAAuB,EACvB,WAAW,GACZ,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @mlx-node/server -- OpenAI Responses + Anthropic Messages server for MLX models.
3
+ *
4
+ * Exposes loaded models via `POST /v1/responses`, `POST /v1/messages`, and
5
+ * `GET /v1/models`, in both streaming (SSE) and non-streaming modes.
6
+ */
7
+ export { createServer } from './server.js';
8
+ /**
9
+ * Internal helpers re-exported for unit testing only. Not part of the
10
+ * supported public API — names may change without notice.
11
+ */
12
+ export { parseEnvSeconds as __parseEnvSeconds, parseEnvPositiveInt as __parseEnvPositiveInt } from './server.js';
13
+ export { createHandler } from './handler.js';
14
+ export { ModelRegistry } from './registry.js';
15
+ export { QueueFullError, SessionRegistry } from './session-registry.js';
16
+ export { writeSSEEvent, beginSSE, endSSE } from './streaming.js';
@@ -0,0 +1,9 @@
1
+ /** Anthropic Messages API request → internal `ChatMessage[]` + `ChatConfig`. */
2
+ import type { ChatConfig, ChatMessage } from '@mlx-node/core';
3
+ import type { AnthropicMessagesRequest } from '../types-anthropic.js';
4
+ export interface MappedAnthropicRequest {
5
+ messages: ChatMessage[];
6
+ config: ChatConfig;
7
+ }
8
+ export declare function mapAnthropicRequest(req: AnthropicMessagesRequest): MappedAnthropicRequest;
9
+ //# sourceMappingURL=anthropic-request.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anthropic-request.d.ts","sourceRoot":"","sources":["../../src/mappers/anthropic-request.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAEhF,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAkB,MAAM,gBAAgB,CAAC;AAE9E,OAAO,KAAK,EAGV,wBAAwB,EAIzB,MAAM,uBAAuB,CAAC;AAE/B,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,MAAM,EAAE,UAAU,CAAC;CACpB;AAiDD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,wBAAwB,GAAG,sBAAsB,CAyMzF"}
@@ -0,0 +1,241 @@
1
+ /** Anthropic Messages API request → internal `ChatMessage[]` + `ChatConfig`. */
2
+ /**
3
+ * Resolve the text content of a `tool_result` block. The internal `ChatMessage`
4
+ * shape (NAPI-generated) has no `images` field on `role: 'tool'`, so nested
5
+ * images are rejected outright — any hoist-to-trailing-user workaround loses
6
+ * both declared order and per-tool association once downstream canonicalization
7
+ * reorders the tool rows. Callers must send images as a top-level image block
8
+ * in a separate user turn.
9
+ */
10
+ function resolveToolResultContent(content) {
11
+ if (content == null)
12
+ return { text: '' };
13
+ if (typeof content === 'string')
14
+ return { text: content };
15
+ const parts = [];
16
+ for (const b of content) {
17
+ if (b.type === 'text') {
18
+ parts.push(b.text);
19
+ }
20
+ else if (b.type === 'image') {
21
+ throw new Error('Unsupported: nested image content in tool_result blocks is not representable in the internal ' +
22
+ 'message model. Send the image as a top-level image block in a separate user turn, and reference ' +
23
+ 'it from the tool_result via text.');
24
+ }
25
+ else {
26
+ throw new Error(`Unsupported tool_result content type: "${b.type}"`);
27
+ }
28
+ }
29
+ return { text: parts.join('') };
30
+ }
31
+ /** NAPI `ToolDefinition` requires `parameters.properties` to be a JSON string. */
32
+ function mapTool(tool) {
33
+ const schema = tool.input_schema;
34
+ return {
35
+ type: 'function',
36
+ function: {
37
+ name: tool.name,
38
+ description: tool.description,
39
+ parameters: {
40
+ type: typeof schema['type'] === 'string' ? schema['type'] : 'object',
41
+ properties: JSON.stringify(schema['properties'] ?? {}),
42
+ required: Array.isArray(schema['required']) ? schema['required'] : undefined,
43
+ },
44
+ },
45
+ };
46
+ }
47
+ export function mapAnthropicRequest(req) {
48
+ const messages = [];
49
+ if (req.system != null) {
50
+ if (typeof req.system === 'string') {
51
+ messages.push({ role: 'system', content: req.system });
52
+ }
53
+ else {
54
+ const systemParts = [];
55
+ for (const b of req.system) {
56
+ if (b.type === 'text') {
57
+ systemParts.push(b.text);
58
+ }
59
+ else {
60
+ throw new Error(`Unsupported system block type: "${b.type}"`);
61
+ }
62
+ }
63
+ messages.push({ role: 'system', content: systemParts.join('') });
64
+ }
65
+ }
66
+ for (const msg of req.messages) {
67
+ const { role, content } = msg;
68
+ if (role === 'user') {
69
+ if (typeof content === 'string') {
70
+ messages.push({ role: 'user', content });
71
+ }
72
+ else {
73
+ // An Anthropic user turn may carry either pure text/image blocks,
74
+ // or a contiguous prefix of `tool_result` blocks optionally followed
75
+ // by trailing text/image blocks. Interleaving text/image BEFORE a
76
+ // tool_result is rejected — we cannot preserve author intent and
77
+ // fan-out ordering without silently reordering the caller's blocks.
78
+ // Caller-relative order within a tool_result prefix is preserved;
79
+ // `validateAndCanonicalizeHistoryToolOrder` reorders later if needed.
80
+ const toolResults = [];
81
+ const trailingText = [];
82
+ const trailingImages = [];
83
+ let seenNonToolResult = false;
84
+ let seenToolResult = false;
85
+ for (const block of content) {
86
+ if (block.type === 'tool_result') {
87
+ if (seenNonToolResult) {
88
+ throw new Error('Unsupported: tool_result blocks must appear as a contiguous prefix of the user ' +
89
+ 'turn, before any text or image blocks. Interleaving a text/image block before a ' +
90
+ 'tool_result would require reordering the caller-supplied blocks and silently ' +
91
+ 'changing authorship.');
92
+ }
93
+ seenToolResult = true;
94
+ const resolved = resolveToolResultContent(block.content);
95
+ toolResults.push({
96
+ toolCallId: block.tool_use_id,
97
+ content: resolved.text,
98
+ isError: block.is_error === true,
99
+ });
100
+ }
101
+ else if (block.type === 'text') {
102
+ seenNonToolResult = true;
103
+ trailingText.push(block.text);
104
+ }
105
+ else if (block.type === 'image' && block.source.type === 'base64') {
106
+ seenNonToolResult = true;
107
+ trailingImages.push(Buffer.from(block.source.data, 'base64'));
108
+ }
109
+ else {
110
+ throw new Error(`Unsupported content block type: "${block.type}"`);
111
+ }
112
+ }
113
+ if (seenToolResult) {
114
+ // `ChatMessage` (NAPI-generated) has no `isError` field, so
115
+ // Anthropic's `tool_result.is_error=true` is encoded as a JSON
116
+ // envelope `{ "is_error": true, "content": <original> }`. The
117
+ // envelope preserves the raw payload verbatim (unlike a text
118
+ // prefix, which would corrupt JSON payloads and collide with
119
+ // strings that legitimately start with the prefix). Every other
120
+ // wire shape is a successful tool result.
121
+ for (const tr of toolResults) {
122
+ const encoded = tr.isError ? JSON.stringify({ is_error: true, content: tr.content }) : tr.content;
123
+ messages.push({
124
+ role: 'tool',
125
+ content: encoded,
126
+ toolCallId: tr.toolCallId,
127
+ });
128
+ }
129
+ // Trailing suffix after a tool_result prefix: accept either
130
+ // (a) text-only (concatenated) or (b) exactly one image block.
131
+ // Mixing text+image or multiple images would silently reorder
132
+ // content in the flat NAPI `ChatMessage` shape.
133
+ const hasTrailingText = trailingText.length > 0;
134
+ const hasTrailingImages = trailingImages.length > 0;
135
+ if (hasTrailingText && hasTrailingImages) {
136
+ throw new Error('Unsupported: mixing trailing text and image blocks after a tool_result prefix is not ' +
137
+ 'representable in the internal message model. The flat ChatMessage shape cannot preserve ' +
138
+ 'the caller-declared relative order of interleaved text and images, so any mapping would ' +
139
+ 'silently reorder your content. Send any commentary as part of the tool_result text, and ' +
140
+ 'deliver additional images in a separate follow-up user turn.');
141
+ }
142
+ if (hasTrailingImages && trailingImages.length > 1) {
143
+ throw new Error('Unsupported: multiple trailing image blocks after a tool_result prefix are not ' +
144
+ 'representable in the internal message model without silently reordering the images ' +
145
+ 'relative to any surrounding text. Send at most one trailing image block, and deliver ' +
146
+ 'additional images in a separate follow-up user turn.');
147
+ }
148
+ if (hasTrailingText || hasTrailingImages) {
149
+ const trailingMsg = { role: 'user', content: trailingText.join('') };
150
+ if (hasTrailingImages) {
151
+ trailingMsg.images = trailingImages;
152
+ }
153
+ messages.push(trailingMsg);
154
+ }
155
+ }
156
+ else {
157
+ // Pure text/image user turn — always emit exactly one `user` message, even if empty.
158
+ const userMsg = { role: 'user', content: trailingText.join('') };
159
+ if (trailingImages.length > 0) {
160
+ userMsg.images = trailingImages;
161
+ }
162
+ messages.push(userMsg);
163
+ }
164
+ }
165
+ }
166
+ else if (role === 'assistant') {
167
+ if (typeof content === 'string') {
168
+ messages.push({ role: 'assistant', content });
169
+ }
170
+ else {
171
+ // Collapse into a single assistant message. The internal shape does not
172
+ // support text-after-tool_use ordering, so interleaved shapes are rejected.
173
+ let text = '';
174
+ let reasoningContent;
175
+ const toolCalls = [];
176
+ let seenToolUse = false;
177
+ for (const block of content) {
178
+ if (block.type === 'text') {
179
+ if (seenToolUse) {
180
+ throw new Error('Text blocks after tool_use blocks are not supported in assistant messages');
181
+ }
182
+ text += block.text;
183
+ }
184
+ else if (block.type === 'thinking') {
185
+ reasoningContent = (reasoningContent ?? '') + block.thinking;
186
+ }
187
+ else if (block.type === 'tool_use') {
188
+ seenToolUse = true;
189
+ toolCalls.push({
190
+ id: block.id,
191
+ name: block.name,
192
+ arguments: JSON.stringify(block.input),
193
+ });
194
+ }
195
+ else {
196
+ throw new Error(`Unsupported assistant content block type: "${block.type}"`);
197
+ }
198
+ }
199
+ const assistantMsg = { role: 'assistant', content: text };
200
+ if (reasoningContent != null) {
201
+ assistantMsg.reasoningContent = reasoningContent;
202
+ }
203
+ if (toolCalls.length > 0) {
204
+ assistantMsg.toolCalls = toolCalls;
205
+ }
206
+ messages.push(assistantMsg);
207
+ }
208
+ }
209
+ else {
210
+ throw new Error(`Unsupported message role: "${role}"`);
211
+ }
212
+ }
213
+ const config = {
214
+ reportPerformance: true,
215
+ };
216
+ if (req.max_tokens != null) {
217
+ config.maxNewTokens = req.max_tokens;
218
+ }
219
+ if (req.temperature != null) {
220
+ config.temperature = req.temperature;
221
+ }
222
+ if (req.top_p != null) {
223
+ config.topP = req.top_p;
224
+ }
225
+ if (req.top_k != null) {
226
+ config.topK = req.top_k;
227
+ }
228
+ if (req.tools && req.tools.length > 0) {
229
+ const toolChoice = req.tool_choice;
230
+ if (toolChoice?.type === 'tool' && toolChoice.name) {
231
+ const matched = req.tools.filter((t) => t.name === toolChoice.name);
232
+ if (matched.length > 0) {
233
+ config.tools = matched.map(mapTool);
234
+ }
235
+ }
236
+ else {
237
+ config.tools = req.tools.map(mapTool);
238
+ }
239
+ }
240
+ return { messages, config };
241
+ }
@@ -0,0 +1,14 @@
1
+ /** ChatResult / ChatStreamEvent → Anthropic Messages API output. */
2
+ import type { ChatResult } from '@mlx-node/core';
3
+ import type { AnthropicContentBlockDeltaEvent, AnthropicContentBlockStartEvent, AnthropicContentBlockStopEvent, AnthropicDelta, AnthropicMessageDeltaEvent, AnthropicMessageStartEvent, AnthropicMessageStopEvent, AnthropicMessagesRequest, AnthropicMessagesResponse, AnthropicResponseContent } from '../types-anthropic.js';
4
+ export declare function mapStopReason(finishReason: string, hasToolCalls: boolean): 'end_turn' | 'max_tokens' | 'tool_use';
5
+ export declare function buildAnthropicContent(result: ChatResult): AnthropicResponseContent[];
6
+ export declare function buildAnthropicResponse(result: ChatResult, req: AnthropicMessagesRequest, messageId: string): AnthropicMessagesResponse;
7
+ /** Embedded message has empty content and zero output_tokens at start. */
8
+ export declare function buildMessageStartEvent(req: AnthropicMessagesRequest, messageId: string, inputTokens: number): AnthropicMessageStartEvent;
9
+ export declare function buildContentBlockStart(index: number, block: AnthropicResponseContent): AnthropicContentBlockStartEvent;
10
+ export declare function buildContentBlockDelta(index: number, delta: AnthropicDelta): AnthropicContentBlockDeltaEvent;
11
+ export declare function buildContentBlockStop(index: number): AnthropicContentBlockStopEvent;
12
+ export declare function buildMessageDelta(stopReason: string, outputTokens: number, inputTokens?: number): AnthropicMessageDeltaEvent;
13
+ export declare function buildMessageStop(): AnthropicMessageStopEvent;
14
+ //# sourceMappingURL=anthropic-response.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anthropic-response.d.ts","sourceRoot":"","sources":["../../src/mappers/anthropic-response.ts"],"names":[],"mappings":"AAAA,oEAAoE;AAEpE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD,OAAO,KAAK,EACV,+BAA+B,EAC/B,+BAA+B,EAC/B,8BAA8B,EAC9B,cAAc,EACd,0BAA0B,EAC1B,0BAA0B,EAC1B,yBAAyB,EACzB,wBAAwB,EACxB,yBAAyB,EACzB,wBAAwB,EACzB,MAAM,uBAAuB,CAAC;AAU/B,wBAAgB,aAAa,CAAC,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,GAAG,UAAU,GAAG,YAAY,GAAG,UAAU,CAQjH;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,UAAU,GAAG,wBAAwB,EAAE,CAwBpF;AAED,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,wBAAwB,EAC7B,SAAS,EAAE,MAAM,GAChB,yBAAyB,CAiB3B;AAID,0EAA0E;AAC1E,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,wBAAwB,EAC7B,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,GAClB,0BAA0B,CAiB5B;AAED,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,wBAAwB,GAC9B,+BAA+B,CAMjC;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,GAAG,+BAA+B,CAM5G;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,8BAA8B,CAKnF;AAED,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,EACpB,WAAW,CAAC,EAAE,MAAM,GACnB,0BAA0B,CAY5B;AAED,wBAAgB,gBAAgB,IAAI,yBAAyB,CAI5D"}
@@ -0,0 +1,112 @@
1
+ /** ChatResult / ChatStreamEvent → Anthropic Messages API output. */
2
+ import { genId } from './response.js';
3
+ function parseArguments(args) {
4
+ if (typeof args === 'string') {
5
+ return JSON.parse(args);
6
+ }
7
+ return args;
8
+ }
9
+ export function mapStopReason(finishReason, hasToolCalls) {
10
+ if (finishReason === 'length') {
11
+ return 'max_tokens';
12
+ }
13
+ if (hasToolCalls) {
14
+ return 'tool_use';
15
+ }
16
+ return 'end_turn';
17
+ }
18
+ export function buildAnthropicContent(result) {
19
+ const content = [];
20
+ if (result.thinking) {
21
+ content.push({ type: 'thinking', thinking: result.thinking });
22
+ }
23
+ const okToolCalls = result.toolCalls.filter((t) => t.status === 'ok');
24
+ // Emit a text block unless tool calls exist and there is no text.
25
+ if (result.text || okToolCalls.length === 0) {
26
+ content.push({ type: 'text', text: result.text });
27
+ }
28
+ for (const tc of okToolCalls) {
29
+ content.push({
30
+ type: 'tool_use',
31
+ id: tc.id ?? genId('toolu_'),
32
+ name: tc.name,
33
+ input: parseArguments(tc.arguments),
34
+ });
35
+ }
36
+ return content;
37
+ }
38
+ export function buildAnthropicResponse(result, req, messageId) {
39
+ const okToolCalls = result.toolCalls.filter((t) => t.status === 'ok');
40
+ const hasToolCalls = okToolCalls.length > 0;
41
+ return {
42
+ id: messageId,
43
+ type: 'message',
44
+ role: 'assistant',
45
+ model: req.model,
46
+ content: buildAnthropicContent(result),
47
+ stop_reason: mapStopReason(result.finishReason, hasToolCalls),
48
+ stop_sequence: null,
49
+ usage: {
50
+ input_tokens: result.promptTokens,
51
+ output_tokens: result.numTokens,
52
+ },
53
+ };
54
+ }
55
+ // Streaming helpers
56
+ /** Embedded message has empty content and zero output_tokens at start. */
57
+ export function buildMessageStartEvent(req, messageId, inputTokens) {
58
+ return {
59
+ type: 'message_start',
60
+ message: {
61
+ id: messageId,
62
+ type: 'message',
63
+ role: 'assistant',
64
+ model: req.model,
65
+ content: [],
66
+ stop_reason: null,
67
+ stop_sequence: null,
68
+ usage: {
69
+ input_tokens: inputTokens,
70
+ output_tokens: 0,
71
+ },
72
+ },
73
+ };
74
+ }
75
+ export function buildContentBlockStart(index, block) {
76
+ return {
77
+ type: 'content_block_start',
78
+ index,
79
+ content_block: block,
80
+ };
81
+ }
82
+ export function buildContentBlockDelta(index, delta) {
83
+ return {
84
+ type: 'content_block_delta',
85
+ index,
86
+ delta,
87
+ };
88
+ }
89
+ export function buildContentBlockStop(index) {
90
+ return {
91
+ type: 'content_block_stop',
92
+ index,
93
+ };
94
+ }
95
+ export function buildMessageDelta(stopReason, outputTokens, inputTokens) {
96
+ return {
97
+ type: 'message_delta',
98
+ delta: {
99
+ stop_reason: stopReason,
100
+ stop_sequence: null,
101
+ },
102
+ usage: {
103
+ ...(inputTokens != null ? { input_tokens: inputTokens } : {}),
104
+ output_tokens: outputTokens,
105
+ },
106
+ };
107
+ }
108
+ export function buildMessageStop() {
109
+ return {
110
+ type: 'message_stop',
111
+ };
112
+ }
@@ -0,0 +1,18 @@
1
+ /** OpenAI Responses API request → internal `ChatMessage[]` + `ChatConfig`. */
2
+ import type { ChatConfig, ChatMessage } from '@mlx-node/core';
3
+ import type { ResponsesAPIRequest } from '../types.js';
4
+ export interface MappedRequest {
5
+ messages: ChatMessage[];
6
+ config: ChatConfig;
7
+ }
8
+ export declare function mapRequest(req: ResponsesAPIRequest, priorMessages?: ChatMessage[]): MappedRequest;
9
+ /**
10
+ * Reconstruct `ChatMessage[]` from a stored response chain. Each record
11
+ * stores `inputJson` (messages sent) and `outputJson` (output items); we
12
+ * interleave them.
13
+ */
14
+ export declare function reconstructMessagesFromChain(chain: {
15
+ inputJson: string;
16
+ outputJson: string;
17
+ }[]): ChatMessage[];
18
+ //# sourceMappingURL=request.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../../src/mappers/request.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAE9E,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAkB,MAAM,gBAAgB,CAAC;AAE9E,OAAO,KAAK,EAAe,mBAAmB,EAA2B,MAAM,aAAa,CAAC;AAqC7F,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,MAAM,EAAE,UAAU,CAAC;CACpB;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,mBAAmB,EAAE,aAAa,CAAC,EAAE,WAAW,EAAE,GAAG,aAAa,CA4GjG;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,EAAE,GAAG,WAAW,EAAE,CAoE9G"}