@mindees/ai 0.22.4 → 0.22.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mappers.d.ts.map +1 -1
- package/dist/mappers.js +11 -2
- package/dist/mappers.js.map +1 -1
- package/dist/server.js +4 -2
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { Maturity, NotImplementedError, PackageInfo, notImplemented } from "@min
|
|
|
14
14
|
/** The npm package name. */
|
|
15
15
|
declare const name = "@mindees/ai";
|
|
16
16
|
/** The package version. All `@mindees/*` packages share one locked version line. */
|
|
17
|
-
declare const VERSION = "0.22.
|
|
17
|
+
declare const VERSION = "0.22.5";
|
|
18
18
|
/** Current maturity of this package. See the repository `STATUS.md`. */
|
|
19
19
|
declare const maturity: Maturity;
|
|
20
20
|
/**
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import { NotImplementedError, notImplemented } from "@mindees/core";
|
|
|
12
12
|
/** The npm package name. */
|
|
13
13
|
const name = "@mindees/ai";
|
|
14
14
|
/** The package version. All `@mindees/*` packages share one locked version line. */
|
|
15
|
-
const VERSION = "0.22.
|
|
15
|
+
const VERSION = "0.22.5";
|
|
16
16
|
/** Current maturity of this package. See the repository `STATUS.md`. */
|
|
17
17
|
const maturity = "experimental";
|
|
18
18
|
/**
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * `@mindees/ai` (Synapse) — provider-agnostic AI + dev-time intelligence.\n *\n * Phase 11 ships the **contract** ({@link createAi}, {@link AiBackend}, messages,\n * {@link GenerateRequest}/{@link AiResult}/{@link AiChunk}, {@link AiError}) with\n * streaming as `AsyncIterable` only (Node/browser/Hermes-safe), a deterministic\n * {@link createMockBackend mock backend} (the working, offline, no-keys fallback),\n * Standard-Schema structured output, bounded tool calling, an inject-`fetch` server\n * backend on the `@mindees/ai/server` subpath, and a dev-time error explainer on the\n * `@mindees/ai/devtools` subpath. The {@link createOnDeviceBackend on-device seam}\n * throws because on-device LLM inference is inherently native and stays a 🔬 research\n * track.\n *\n * @module\n */\n\nimport type { Maturity, PackageInfo } from '@mindees/core'\nimport { NotImplementedError, notImplemented } from '@mindees/core'\n\n/** The npm package name. */\nexport const name = '@mindees/ai'\n\n/** The package version. All `@mindees/*` packages share one locked version line. */\nexport const VERSION = '0.22.
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * `@mindees/ai` (Synapse) — provider-agnostic AI + dev-time intelligence.\n *\n * Phase 11 ships the **contract** ({@link createAi}, {@link AiBackend}, messages,\n * {@link GenerateRequest}/{@link AiResult}/{@link AiChunk}, {@link AiError}) with\n * streaming as `AsyncIterable` only (Node/browser/Hermes-safe), a deterministic\n * {@link createMockBackend mock backend} (the working, offline, no-keys fallback),\n * Standard-Schema structured output, bounded tool calling, an inject-`fetch` server\n * backend on the `@mindees/ai/server` subpath, and a dev-time error explainer on the\n * `@mindees/ai/devtools` subpath. The {@link createOnDeviceBackend on-device seam}\n * throws because on-device LLM inference is inherently native and stays a 🔬 research\n * track.\n *\n * @module\n */\n\nimport type { Maturity, PackageInfo } from '@mindees/core'\nimport { NotImplementedError, notImplemented } from '@mindees/core'\n\n/** The npm package name. */\nexport const name = '@mindees/ai'\n\n/** The package version. All `@mindees/*` packages share one locked version line. */\nexport const VERSION = '0.22.5'\n\n/** Current maturity of this package. See the repository `STATUS.md`. */\nexport const maturity: Maturity = 'experimental'\n\n/**\n * Static identity + maturity metadata for this package. Frozen so the\n * self-reported identity tooling introspects cannot be mutated at runtime,\n * matching the `readonly` fields of {@link PackageInfo}.\n */\nexport const info: PackageInfo = Object.freeze({ name, version: VERSION, maturity })\n\nexport { type CacheOptions, withCache } from './cache'\nexport {\n type AbortLike,\n type Ai,\n type AiBackend,\n type AiChunk,\n type AiResult,\n createAi,\n type FinishReason,\n type GenerateRequest,\n type Message,\n messageText,\n type Part,\n type Role,\n type TextPart,\n type ToolCallPart,\n type ToolDefinition,\n type ToolResultPart,\n type Usage,\n} from './contract'\nexport { AiError, type AiErrorCode, type AiErrorOptions } from './errors'\nexport {\n containsForbiddenKey,\n DEFAULT_MAX_INPUT_CHARS,\n type ExtractResult,\n extractJson,\n formatIssues,\n lenientParseJson,\n type SanitizeLimits,\n sanitizeJson,\n type ValidationOutcome,\n validateStandard,\n} from './json'\nexport {\n createMockBackend,\n type MockBackendOptions,\n type MockReply,\n type MockResponse,\n} from './mock'\nexport {\n type GenerateObjectOptions,\n type GenerateObjectResult,\n type GeneratingBackend,\n generateObject,\n type StreamingBackend,\n type StreamObjectChunk,\n type StreamObjectOptions,\n streamObject,\n} from './object'\nexport { createOnDeviceBackend } from './on-device'\nexport { type RetryOptions, withRetry } from './retry'\nexport type { StandardSchemaV1 } from './standard-schema'\nexport {\n type RunToolsOptions,\n type RunToolsResult,\n runTools,\n type Tool,\n type ToolContext,\n} from './tools'\n\nexport type { Maturity, PackageInfo }\nexport { NotImplementedError, notImplemented }\n"],"mappings":";;;;;;;;;;;;AAoBA,MAAa,OAAO;;AAGpB,MAAa,UAAU;;AAGvB,MAAa,WAAqB;;;;;;AAOlC,MAAa,OAAoB,OAAO,OAAO;CAAE;CAAM,SAAS;CAAS;AAAS,CAAC"}
|
package/dist/mappers.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mappers.d.ts","names":[],"sources":["../src/mappers.ts"],"mappings":";;;;;;;;;;KA6BY,YAAA,IAAgB,IAAA,uBAA2B,OAAO;;;;;;UAO7C,cAAA;EAaI;;;;;;EAAA,SANV,IAAA;EAcyB;EAZlC,YAAA,CACE,OAAA,EAAS,eAAA,EACT,KAAA,UACA,MAAA;IACG,IAAA;IAAc,IAAA;EAAA;
|
|
1
|
+
{"version":3,"file":"mappers.d.ts","names":[],"sources":["../src/mappers.ts"],"mappings":";;;;;;;;;;KA6BY,YAAA,IAAgB,IAAA,uBAA2B,OAAO;;;;;;UAO7C,cAAA;EAaI;;;;;;EAAA,SANV,IAAA;EAcyB;EAZlC,YAAA,CACE,OAAA,EAAS,eAAA,EACT,KAAA,UACA,MAAA;IACG,IAAA;IAAc,IAAA;EAAA;EA+OpB;EA7OC,aAAA,CAAc,IAAA,YAAgB,QAAA;EAsU/B;;;AAAA;AAGD;EAnUE,kBAAA,IAAsB,YAAA;AAAA;;cAyJX,YAAA,EAAc,cA8E1B;;cAGY,eAAA,EAAiB,cAsF7B;;cAGY,OAAA;EAAA,iBAAuE,cAAA;EAAA,oBAAA,cAAA;AAAA;;KAGxE,WAAA,gBAA2B,OAAO"}
|
package/dist/mappers.js
CHANGED
|
@@ -33,11 +33,20 @@ function parseJsonArgs(value) {
|
|
|
33
33
|
return {};
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
-
/** Serialize a tool result to the string the wire APIs expect (never throws
|
|
36
|
+
/** Serialize a tool result to the string the wire APIs expect (never throws; bigint- + cycle-safe). */
|
|
37
37
|
function toWireString(value) {
|
|
38
38
|
if (typeof value === "string") return value;
|
|
39
|
+
if (value === null || typeof value !== "object") return typeof value === "bigint" ? value.toString() : String(value);
|
|
39
40
|
try {
|
|
40
|
-
|
|
41
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
42
|
+
return JSON.stringify(value, (_key, v) => {
|
|
43
|
+
if (typeof v === "bigint") return v.toString();
|
|
44
|
+
if (v !== null && typeof v === "object") {
|
|
45
|
+
if (seen.has(v)) return "[Circular]";
|
|
46
|
+
seen.add(v);
|
|
47
|
+
}
|
|
48
|
+
return v;
|
|
49
|
+
}) ?? String(value);
|
|
41
50
|
} catch {
|
|
42
51
|
return String(value);
|
|
43
52
|
}
|
package/dist/mappers.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mappers.js","names":[],"sources":["../src/mappers.ts"],"sourcesContent":["/**\n * Per-provider wire mappers (OpenAI-compatible + Anthropic), isolating the HTTP shape so\n * the server backend never imports a vendor SDK and a provider change is a contained\n * edit. All response/stream parsing is defensive — model/server JSON is untrusted. Tool\n * mapping is added in 11C (with tools). See `docs/adr/0018-synapse-server-backend.md`.\n *\n * @module\n */\n\nimport {\n type AiChunk,\n type AiResult,\n type FinishReason,\n type GenerateRequest,\n type Message,\n messageText,\n type Part,\n type ToolCallPart,\n type ToolResultPart,\n type Usage,\n} from './contract'\n\n/**\n * Parse one SSE `data` JSON into zero or more {@link AiChunk}s. Returns an array (empty\n * to skip a non-content event) because a single event can legitimately carry more than\n * one logical chunk — e.g. an OpenAI-compatible server that puts a content delta AND the\n * terminal `finish_reason`/`usage` in the same event (a 1-chunk-per-event parser would\n * drop the finish/usage).\n */\nexport type StreamParser = (data: unknown) => readonly AiChunk[]\n\n/**\n * A provider wire mapper. A custom mapper whose `buildRequest` cannot express\n * `request.tools` MUST throw rather than silently drop them (the built-in openai/anthropic\n * mappers serialize them; the on-device backend throws).\n */\nexport interface ProviderMapper {\n /**\n * How `apiKey` is sent. `'anthropic'` → `x-api-key` + `anthropic-version`; `'bearer'`\n * (the default when omitted) → `Authorization: Bearer`. Declared on the mapper so auth\n * follows the chosen provider whether it is selected by name or by object — and so a\n * custom Anthropic-compatible mapper can opt into `x-api-key` auth.\n */\n readonly auth?: 'bearer' | 'anthropic'\n /** Build the HTTP path + JSON body for a request. */\n buildRequest(\n request: GenerateRequest,\n model: string,\n stream: boolean,\n ): { path: string; body: unknown }\n /** Parse a non-streaming JSON response into an {@link AiResult}. */\n parseResponse(json: unknown): AiResult\n /**\n * Create a {@link StreamParser} for one stream. Returned fresh per stream so it may hold\n * per-stream state (e.g. the last `finish_reason`) without leaking across concurrent\n * streams that share this mapper singleton.\n */\n createStreamParser(): StreamParser\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> | undefined {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : undefined\n}\nfunction asString(value: unknown): string | undefined {\n return typeof value === 'string' ? value : undefined\n}\nfunction asNumber(value: unknown): number | undefined {\n return typeof value === 'number' && Number.isFinite(value) ? value : undefined\n}\n/** Build a {@link Usage} omitting undefined fields (exactOptionalPropertyTypes-safe). */\nfunction usageOf(inputTokens?: number, outputTokens?: number): Usage {\n const usage: { inputTokens?: number; outputTokens?: number } = {}\n if (inputTokens !== undefined) usage.inputTokens = inputTokens\n if (outputTokens !== undefined) usage.outputTokens = outputTokens\n return usage\n}\n\n/** Parse OpenAI's `function.arguments` (a JSON STRING) defensively into a value. */\nfunction parseJsonArgs(value: unknown): unknown {\n if (typeof value !== 'string') return value ?? {}\n try {\n return JSON.parse(value)\n } catch {\n return {} // a malformed arg string becomes empty args — the loop's validator rejects it\n }\n}\n\n/** Serialize a tool result to the string the wire APIs expect (never throws, cycle-safe-ish). */\nfunction toWireString(value: unknown): string {\n if (typeof value === 'string') return value\n try {\n return JSON.stringify(value) ?? String(value)\n } catch {\n return String(value)\n }\n}\n\n/** Split a message's parts (string content has only text). */\nfunction partsOf(content: string | readonly Part[]): {\n text: string\n calls: ToolCallPart[]\n results: ToolResultPart[]\n} {\n if (typeof content === 'string') return { text: content, calls: [], results: [] }\n const text = content\n .filter((p): p is Extract<Part, { type: 'text' }> => p.type === 'text')\n .map((p) => p.text)\n .join('')\n const calls = content.filter((p): p is ToolCallPart => p.type === 'tool-call')\n const results = content.filter((p): p is ToolResultPart => p.type === 'tool-result')\n return { text, calls, results }\n}\n\n/**\n * Serialize messages to the OpenAI chat shape, round-tripping tool turns: an assistant turn\n * with tool calls emits `tool_calls`; a `tool` message emits one `{ role:'tool', tool_call_id }`\n * per result. Without this the tool loop's 2nd turn would lose its tool context.\n */\nfunction openaiMessages(messages: readonly Message[]): unknown[] {\n const out: unknown[] = []\n for (const m of messages) {\n const { text, calls, results } = partsOf(m.content)\n if (results.length > 0) {\n for (const r of results)\n out.push({ role: 'tool', tool_call_id: r.id, content: toWireString(r.result) })\n } else if (calls.length > 0) {\n out.push({\n role: m.role,\n content: text || null,\n tool_calls: calls.map((c) => ({\n id: c.id,\n type: 'function',\n function: { name: c.name, arguments: JSON.stringify(c.args ?? {}) },\n })),\n })\n } else {\n out.push({ role: m.role, content: text })\n }\n }\n return out\n}\n\n/**\n * Serialize non-system messages to the Anthropic shape, round-tripping tool turns: assistant\n * tool calls become `tool_use` blocks; a `tool` message becomes a USER message of `tool_result`\n * blocks (Anthropic carries tool results in the user turn).\n */\nfunction anthropicMessages(messages: readonly Message[]): unknown[] {\n const out: unknown[] = []\n for (const m of messages) {\n if (m.role === 'system') continue // folded into the top-level `system` field\n const { text, calls, results } = partsOf(m.content)\n if (results.length > 0) {\n out.push({\n role: 'user',\n content: results.map((r) => ({\n type: 'tool_result',\n tool_use_id: r.id,\n content: toWireString(r.result),\n })),\n })\n } else if (calls.length > 0) {\n const content: unknown[] = []\n if (text) content.push({ type: 'text', text })\n for (const c of calls)\n content.push({ type: 'tool_use', id: c.id, name: c.name, input: c.args ?? {} })\n out.push({ role: 'assistant', content })\n } else {\n out.push({ role: m.role, content: text })\n }\n }\n return out\n}\n\n/** Add `toolCalls` to an {@link AiResult} only when present (exactOptionalPropertyTypes-safe). */\nfunction withToolCalls(result: AiResult, toolCalls: ToolCallPart[]): AiResult {\n return toolCalls.length > 0 ? { ...result, toolCalls } : result\n}\n\n// null-prototype maps so an untrusted finish_reason like 'toString'/'__proto__' reads as\n// undefined (→ the 'stop' fallback) rather than leaking an inherited member.\nconst OPENAI_FINISH: Record<string, FinishReason> = Object.assign(Object.create(null), {\n stop: 'stop',\n length: 'length',\n tool_calls: 'tool-calls',\n})\nconst ANTHROPIC_FINISH: Record<string, FinishReason> = Object.assign(Object.create(null), {\n end_turn: 'stop',\n max_tokens: 'length',\n tool_use: 'tool-calls',\n})\n\n/** OpenAI-compatible `/chat/completions` mapper (also fits many local/compatible servers). */\nexport const openaiMapper: ProviderMapper = {\n auth: 'bearer',\n buildRequest(request, model, stream) {\n const body: Record<string, unknown> = {\n model,\n stream,\n messages: openaiMessages(request.messages),\n }\n if (request.temperature !== undefined) body.temperature = request.temperature\n if (request.maxOutputTokens !== undefined) body.max_tokens = request.maxOutputTokens\n if (request.tools && request.tools.length > 0) {\n body.tools = request.tools.map((t) => ({\n type: 'function',\n function: {\n name: t.name,\n ...(t.description !== undefined ? { description: t.description } : {}),\n parameters: t.parameters ?? { type: 'object', properties: {} },\n },\n }))\n }\n if (stream) body.stream_options = { include_usage: true }\n return { path: '/chat/completions', body }\n },\n parseResponse(json) {\n const root = asRecord(json)\n const choice = asRecord((root?.choices as unknown[] | undefined)?.[0])\n const message = asRecord(choice?.message)\n const usage = asRecord(root?.usage)\n const toolCalls: ToolCallPart[] = []\n for (const raw of (message?.tool_calls as unknown[] | undefined) ?? []) {\n const tc = asRecord(raw)\n const fn = asRecord(tc?.function)\n const name = asString(fn?.name)\n if (!name) continue\n toolCalls.push({\n type: 'tool-call',\n id: asString(tc?.id) ?? name,\n name,\n args: parseJsonArgs(fn?.arguments),\n })\n }\n return withToolCalls(\n {\n text: asString(message?.content) ?? '',\n finishReason: OPENAI_FINISH[asString(choice?.finish_reason) ?? ''] ?? 'stop',\n usage: usageOf(asNumber(usage?.prompt_tokens), asNumber(usage?.completion_tokens)),\n },\n toolCalls,\n )\n },\n createStreamParser() {\n // With stream_options.include_usage, OpenAI sends usage on a trailing choices:[] chunk\n // that carries NO finish_reason. Remember the last real finish_reason so the\n // usage-bearing finish reports the true reason instead of a fabricated 'stop'.\n let lastReason: FinishReason = 'stop'\n return (data) => {\n const root = asRecord(data)\n const choice = asRecord((root?.choices as unknown[] | undefined)?.[0])\n const out: AiChunk[] = []\n const delta = asString(asRecord(choice?.delta)?.content)\n if (delta) out.push({ type: 'text-delta', delta })\n const finish = asString(choice?.finish_reason)\n if (finish) lastReason = OPENAI_FINISH[finish] ?? 'stop'\n const usage = asRecord(root?.usage)\n // Emit a finish for a finish_reason chunk OR the trailing usage-only chunk, so\n // streamed usage isn't lost — both carry the last-seen reason. Emitted IN ADDITION\n // to any content delta in the same event (servers that bundle them are common), so\n // the terminal finish/usage is never dropped.\n if (finish || usage) {\n out.push({\n type: 'finish',\n finishReason: lastReason,\n usage: usageOf(asNumber(usage?.prompt_tokens), asNumber(usage?.completion_tokens)),\n })\n }\n return out\n }\n },\n}\n\n/** Anthropic `/v1/messages` mapper. */\nexport const anthropicMapper: ProviderMapper = {\n auth: 'anthropic',\n buildRequest(request, model, stream) {\n const system = request.messages\n .filter((m) => m.role === 'system')\n .map((m) => messageText(m))\n .join('\\n')\n const messages = anthropicMessages(request.messages)\n const body: Record<string, unknown> = {\n model,\n stream,\n messages,\n max_tokens: request.maxOutputTokens ?? 1024, // required by Anthropic\n }\n if (system) body.system = system\n if (request.temperature !== undefined) body.temperature = request.temperature\n if (request.tools && request.tools.length > 0) {\n body.tools = request.tools.map((t) => ({\n name: t.name,\n ...(t.description !== undefined ? { description: t.description } : {}),\n input_schema: t.parameters ?? { type: 'object', properties: {} },\n }))\n }\n return { path: '/v1/messages', body }\n },\n parseResponse(json) {\n const root = asRecord(json)\n const content = (root?.content as unknown[] | undefined) ?? []\n let text = ''\n const toolCalls: ToolCallPart[] = []\n for (const part of content) {\n const block = asRecord(part)\n if (asString(block?.type) === 'tool_use') {\n const name = asString(block?.name)\n if (!name) continue\n toolCalls.push({\n type: 'tool-call',\n id: asString(block?.id) ?? name,\n name,\n args: block?.input ?? {},\n })\n } else {\n text += asString(block?.text) ?? ''\n }\n }\n const usage = asRecord(root?.usage)\n return withToolCalls(\n {\n text,\n finishReason: ANTHROPIC_FINISH[asString(root?.stop_reason) ?? ''] ?? 'stop',\n usage: usageOf(asNumber(usage?.input_tokens), asNumber(usage?.output_tokens)),\n },\n toolCalls,\n )\n },\n createStreamParser() {\n // Anthropic reports input_tokens once on `message_start` and the (cumulative)\n // output_tokens on `message_delta`. They arrive in SEPARATE events, so the parser\n // must remember input_tokens from the start event to report it on the finish chunk —\n // otherwise every streamed response loses its prompt-token count.\n let inputTokens: number | undefined\n return (data) => {\n const root = asRecord(data)\n const type = asString(root?.type)\n if (type === 'message_start') {\n inputTokens = asNumber(asRecord(asRecord(root?.message)?.usage)?.input_tokens)\n return []\n }\n if (type === 'content_block_delta') {\n const delta = asString(asRecord(root?.delta)?.text)\n return delta ? [{ type: 'text-delta', delta }] : []\n }\n if (type === 'message_delta') {\n const stop = asString(asRecord(root?.delta)?.stop_reason)\n const usage = asRecord(root?.usage)\n return [\n {\n type: 'finish',\n finishReason: ANTHROPIC_FINISH[stop ?? ''] ?? 'stop',\n usage: usageOf(inputTokens, asNumber(usage?.output_tokens)),\n },\n ]\n }\n return []\n }\n },\n}\n\n/** The built-in mappers by adapter name. */\nexport const MAPPERS = { openai: openaiMapper, anthropic: anthropicMapper } as const\n\n/** A built-in provider adapter name. */\nexport type AdapterName = keyof typeof MAPPERS\n"],"mappings":";;;;;;;;;;AA4DA,SAAS,SAAS,OAAqD;CACrE,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK,IACrE,QACD,KAAA;AACN;AACA,SAAS,SAAS,OAAoC;CACpD,OAAO,OAAO,UAAU,WAAW,QAAQ,KAAA;AAC7C;AACA,SAAS,SAAS,OAAoC;CACpD,OAAO,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,IAAI,QAAQ,KAAA;AACvE;;AAEA,SAAS,QAAQ,aAAsB,cAA8B;CACnE,MAAM,QAAyD,CAAC;CAChE,IAAI,gBAAgB,KAAA,GAAW,MAAM,cAAc;CACnD,IAAI,iBAAiB,KAAA,GAAW,MAAM,eAAe;CACrD,OAAO;AACT;;AAGA,SAAS,cAAc,OAAyB;CAC9C,IAAI,OAAO,UAAU,UAAU,OAAO,SAAS,CAAC;CAChD,IAAI;EACF,OAAO,KAAK,MAAM,KAAK;CACzB,QAAQ;EACN,OAAO,CAAC;CACV;AACF;;AAGA,SAAS,aAAa,OAAwB;CAC5C,IAAI,OAAO,UAAU,UAAU,OAAO;CACtC,IAAI;EACF,OAAO,KAAK,UAAU,KAAK,KAAK,OAAO,KAAK;CAC9C,QAAQ;EACN,OAAO,OAAO,KAAK;CACrB;AACF;;AAGA,SAAS,QAAQ,SAIf;CACA,IAAI,OAAO,YAAY,UAAU,OAAO;EAAE,MAAM;EAAS,OAAO,CAAC;EAAG,SAAS,CAAC;CAAE;CAOhF,OAAO;EAAE,MANI,QACV,QAAQ,MAA4C,EAAE,SAAS,MAAM,EACrE,KAAK,MAAM,EAAE,IAAI,EACjB,KAAK,EAGI;EAAG,OAFD,QAAQ,QAAQ,MAAyB,EAAE,SAAS,WAE/C;EAAG,SADN,QAAQ,QAAQ,MAA2B,EAAE,SAAS,aAC1C;CAAE;AAChC;;;;;;AAOA,SAAS,eAAe,UAAyC;CAC/D,MAAM,MAAiB,CAAC;CACxB,KAAK,MAAM,KAAK,UAAU;EACxB,MAAM,EAAE,MAAM,OAAO,YAAY,QAAQ,EAAE,OAAO;EAClD,IAAI,QAAQ,SAAS,GACnB,KAAK,MAAM,KAAK,SACd,IAAI,KAAK;GAAE,MAAM;GAAQ,cAAc,EAAE;GAAI,SAAS,aAAa,EAAE,MAAM;EAAE,CAAC;OAC3E,IAAI,MAAM,SAAS,GACxB,IAAI,KAAK;GACP,MAAM,EAAE;GACR,SAAS,QAAQ;GACjB,YAAY,MAAM,KAAK,OAAO;IAC5B,IAAI,EAAE;IACN,MAAM;IACN,UAAU;KAAE,MAAM,EAAE;KAAM,WAAW,KAAK,UAAU,EAAE,QAAQ,CAAC,CAAC;IAAE;GACpE,EAAE;EACJ,CAAC;OAED,IAAI,KAAK;GAAE,MAAM,EAAE;GAAM,SAAS;EAAK,CAAC;CAE5C;CACA,OAAO;AACT;;;;;;AAOA,SAAS,kBAAkB,UAAyC;CAClE,MAAM,MAAiB,CAAC;CACxB,KAAK,MAAM,KAAK,UAAU;EACxB,IAAI,EAAE,SAAS,UAAU;EACzB,MAAM,EAAE,MAAM,OAAO,YAAY,QAAQ,EAAE,OAAO;EAClD,IAAI,QAAQ,SAAS,GACnB,IAAI,KAAK;GACP,MAAM;GACN,SAAS,QAAQ,KAAK,OAAO;IAC3B,MAAM;IACN,aAAa,EAAE;IACf,SAAS,aAAa,EAAE,MAAM;GAChC,EAAE;EACJ,CAAC;OACI,IAAI,MAAM,SAAS,GAAG;GAC3B,MAAM,UAAqB,CAAC;GAC5B,IAAI,MAAM,QAAQ,KAAK;IAAE,MAAM;IAAQ;GAAK,CAAC;GAC7C,KAAK,MAAM,KAAK,OACd,QAAQ,KAAK;IAAE,MAAM;IAAY,IAAI,EAAE;IAAI,MAAM,EAAE;IAAM,OAAO,EAAE,QAAQ,CAAC;GAAE,CAAC;GAChF,IAAI,KAAK;IAAE,MAAM;IAAa;GAAQ,CAAC;EACzC,OACE,IAAI,KAAK;GAAE,MAAM,EAAE;GAAM,SAAS;EAAK,CAAC;CAE5C;CACA,OAAO;AACT;;AAGA,SAAS,cAAc,QAAkB,WAAqC;CAC5E,OAAO,UAAU,SAAS,IAAI;EAAE,GAAG;EAAQ;CAAU,IAAI;AAC3D;AAIA,MAAM,gBAA8C,OAAO,OAAO,OAAO,OAAO,IAAI,GAAG;CACrF,MAAM;CACN,QAAQ;CACR,YAAY;AACd,CAAC;AACD,MAAM,mBAAiD,OAAO,OAAO,OAAO,OAAO,IAAI,GAAG;CACxF,UAAU;CACV,YAAY;CACZ,UAAU;AACZ,CAAC;;AAGD,MAAa,eAA+B;CAC1C,MAAM;CACN,aAAa,SAAS,OAAO,QAAQ;EACnC,MAAM,OAAgC;GACpC;GACA;GACA,UAAU,eAAe,QAAQ,QAAQ;EAC3C;EACA,IAAI,QAAQ,gBAAgB,KAAA,GAAW,KAAK,cAAc,QAAQ;EAClE,IAAI,QAAQ,oBAAoB,KAAA,GAAW,KAAK,aAAa,QAAQ;EACrE,IAAI,QAAQ,SAAS,QAAQ,MAAM,SAAS,GAC1C,KAAK,QAAQ,QAAQ,MAAM,KAAK,OAAO;GACrC,MAAM;GACN,UAAU;IACR,MAAM,EAAE;IACR,GAAI,EAAE,gBAAgB,KAAA,IAAY,EAAE,aAAa,EAAE,YAAY,IAAI,CAAC;IACpE,YAAY,EAAE,cAAc;KAAE,MAAM;KAAU,YAAY,CAAC;IAAE;GAC/D;EACF,EAAE;EAEJ,IAAI,QAAQ,KAAK,iBAAiB,EAAE,eAAe,KAAK;EACxD,OAAO;GAAE,MAAM;GAAqB;EAAK;CAC3C;CACA,cAAc,MAAM;EAClB,MAAM,OAAO,SAAS,IAAI;EAC1B,MAAM,SAAS,UAAU,MAAM,WAAoC,EAAE;EACrE,MAAM,UAAU,SAAS,QAAQ,OAAO;EACxC,MAAM,QAAQ,SAAS,MAAM,KAAK;EAClC,MAAM,YAA4B,CAAC;EACnC,KAAK,MAAM,OAAQ,SAAS,cAAwC,CAAC,GAAG;GACtE,MAAM,KAAK,SAAS,GAAG;GACvB,MAAM,KAAK,SAAS,IAAI,QAAQ;GAChC,MAAM,OAAO,SAAS,IAAI,IAAI;GAC9B,IAAI,CAAC,MAAM;GACX,UAAU,KAAK;IACb,MAAM;IACN,IAAI,SAAS,IAAI,EAAE,KAAK;IACxB;IACA,MAAM,cAAc,IAAI,SAAS;GACnC,CAAC;EACH;EACA,OAAO,cACL;GACE,MAAM,SAAS,SAAS,OAAO,KAAK;GACpC,cAAc,cAAc,SAAS,QAAQ,aAAa,KAAK,OAAO;GACtE,OAAO,QAAQ,SAAS,OAAO,aAAa,GAAG,SAAS,OAAO,iBAAiB,CAAC;EACnF,GACA,SACF;CACF;CACA,qBAAqB;EAInB,IAAI,aAA2B;EAC/B,QAAQ,SAAS;GACf,MAAM,OAAO,SAAS,IAAI;GAC1B,MAAM,SAAS,UAAU,MAAM,WAAoC,EAAE;GACrE,MAAM,MAAiB,CAAC;GACxB,MAAM,QAAQ,SAAS,SAAS,QAAQ,KAAK,GAAG,OAAO;GACvD,IAAI,OAAO,IAAI,KAAK;IAAE,MAAM;IAAc;GAAM,CAAC;GACjD,MAAM,SAAS,SAAS,QAAQ,aAAa;GAC7C,IAAI,QAAQ,aAAa,cAAc,WAAW;GAClD,MAAM,QAAQ,SAAS,MAAM,KAAK;GAKlC,IAAI,UAAU,OACZ,IAAI,KAAK;IACP,MAAM;IACN,cAAc;IACd,OAAO,QAAQ,SAAS,OAAO,aAAa,GAAG,SAAS,OAAO,iBAAiB,CAAC;GACnF,CAAC;GAEH,OAAO;EACT;CACF;AACF;;AAGA,MAAa,kBAAkC;CAC7C,MAAM;CACN,aAAa,SAAS,OAAO,QAAQ;EACnC,MAAM,SAAS,QAAQ,SACpB,QAAQ,MAAM,EAAE,SAAS,QAAQ,EACjC,KAAK,MAAM,YAAY,CAAC,CAAC,EACzB,KAAK,IAAI;EAEZ,MAAM,OAAgC;GACpC;GACA;GACA,UAJe,kBAAkB,QAAQ,QAIlC;GACP,YAAY,QAAQ,mBAAmB;EACzC;EACA,IAAI,QAAQ,KAAK,SAAS;EAC1B,IAAI,QAAQ,gBAAgB,KAAA,GAAW,KAAK,cAAc,QAAQ;EAClE,IAAI,QAAQ,SAAS,QAAQ,MAAM,SAAS,GAC1C,KAAK,QAAQ,QAAQ,MAAM,KAAK,OAAO;GACrC,MAAM,EAAE;GACR,GAAI,EAAE,gBAAgB,KAAA,IAAY,EAAE,aAAa,EAAE,YAAY,IAAI,CAAC;GACpE,cAAc,EAAE,cAAc;IAAE,MAAM;IAAU,YAAY,CAAC;GAAE;EACjE,EAAE;EAEJ,OAAO;GAAE,MAAM;GAAgB;EAAK;CACtC;CACA,cAAc,MAAM;EAClB,MAAM,OAAO,SAAS,IAAI;EAC1B,MAAM,UAAW,MAAM,WAAqC,CAAC;EAC7D,IAAI,OAAO;EACX,MAAM,YAA4B,CAAC;EACnC,KAAK,MAAM,QAAQ,SAAS;GAC1B,MAAM,QAAQ,SAAS,IAAI;GAC3B,IAAI,SAAS,OAAO,IAAI,MAAM,YAAY;IACxC,MAAM,OAAO,SAAS,OAAO,IAAI;IACjC,IAAI,CAAC,MAAM;IACX,UAAU,KAAK;KACb,MAAM;KACN,IAAI,SAAS,OAAO,EAAE,KAAK;KAC3B;KACA,MAAM,OAAO,SAAS,CAAC;IACzB,CAAC;GACH,OACE,QAAQ,SAAS,OAAO,IAAI,KAAK;EAErC;EACA,MAAM,QAAQ,SAAS,MAAM,KAAK;EAClC,OAAO,cACL;GACE;GACA,cAAc,iBAAiB,SAAS,MAAM,WAAW,KAAK,OAAO;GACrE,OAAO,QAAQ,SAAS,OAAO,YAAY,GAAG,SAAS,OAAO,aAAa,CAAC;EAC9E,GACA,SACF;CACF;CACA,qBAAqB;EAKnB,IAAI;EACJ,QAAQ,SAAS;GACf,MAAM,OAAO,SAAS,IAAI;GAC1B,MAAM,OAAO,SAAS,MAAM,IAAI;GAChC,IAAI,SAAS,iBAAiB;IAC5B,cAAc,SAAS,SAAS,SAAS,MAAM,OAAO,GAAG,KAAK,GAAG,YAAY;IAC7E,OAAO,CAAC;GACV;GACA,IAAI,SAAS,uBAAuB;IAClC,MAAM,QAAQ,SAAS,SAAS,MAAM,KAAK,GAAG,IAAI;IAClD,OAAO,QAAQ,CAAC;KAAE,MAAM;KAAc;IAAM,CAAC,IAAI,CAAC;GACpD;GACA,IAAI,SAAS,iBAAiB;IAC5B,MAAM,OAAO,SAAS,SAAS,MAAM,KAAK,GAAG,WAAW;IACxD,MAAM,QAAQ,SAAS,MAAM,KAAK;IAClC,OAAO,CACL;KACE,MAAM;KACN,cAAc,iBAAiB,QAAQ,OAAO;KAC9C,OAAO,QAAQ,aAAa,SAAS,OAAO,aAAa,CAAC;IAC5D,CACF;GACF;GACA,OAAO,CAAC;EACV;CACF;AACF;;AAGA,MAAa,UAAU;CAAE,QAAQ;CAAc,WAAW;AAAgB"}
|
|
1
|
+
{"version":3,"file":"mappers.js","names":[],"sources":["../src/mappers.ts"],"sourcesContent":["/**\n * Per-provider wire mappers (OpenAI-compatible + Anthropic), isolating the HTTP shape so\n * the server backend never imports a vendor SDK and a provider change is a contained\n * edit. All response/stream parsing is defensive — model/server JSON is untrusted. Tool\n * mapping is added in 11C (with tools). See `docs/adr/0018-synapse-server-backend.md`.\n *\n * @module\n */\n\nimport {\n type AiChunk,\n type AiResult,\n type FinishReason,\n type GenerateRequest,\n type Message,\n messageText,\n type Part,\n type ToolCallPart,\n type ToolResultPart,\n type Usage,\n} from './contract'\n\n/**\n * Parse one SSE `data` JSON into zero or more {@link AiChunk}s. Returns an array (empty\n * to skip a non-content event) because a single event can legitimately carry more than\n * one logical chunk — e.g. an OpenAI-compatible server that puts a content delta AND the\n * terminal `finish_reason`/`usage` in the same event (a 1-chunk-per-event parser would\n * drop the finish/usage).\n */\nexport type StreamParser = (data: unknown) => readonly AiChunk[]\n\n/**\n * A provider wire mapper. A custom mapper whose `buildRequest` cannot express\n * `request.tools` MUST throw rather than silently drop them (the built-in openai/anthropic\n * mappers serialize them; the on-device backend throws).\n */\nexport interface ProviderMapper {\n /**\n * How `apiKey` is sent. `'anthropic'` → `x-api-key` + `anthropic-version`; `'bearer'`\n * (the default when omitted) → `Authorization: Bearer`. Declared on the mapper so auth\n * follows the chosen provider whether it is selected by name or by object — and so a\n * custom Anthropic-compatible mapper can opt into `x-api-key` auth.\n */\n readonly auth?: 'bearer' | 'anthropic'\n /** Build the HTTP path + JSON body for a request. */\n buildRequest(\n request: GenerateRequest,\n model: string,\n stream: boolean,\n ): { path: string; body: unknown }\n /** Parse a non-streaming JSON response into an {@link AiResult}. */\n parseResponse(json: unknown): AiResult\n /**\n * Create a {@link StreamParser} for one stream. Returned fresh per stream so it may hold\n * per-stream state (e.g. the last `finish_reason`) without leaking across concurrent\n * streams that share this mapper singleton.\n */\n createStreamParser(): StreamParser\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> | undefined {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : undefined\n}\nfunction asString(value: unknown): string | undefined {\n return typeof value === 'string' ? value : undefined\n}\nfunction asNumber(value: unknown): number | undefined {\n return typeof value === 'number' && Number.isFinite(value) ? value : undefined\n}\n/** Build a {@link Usage} omitting undefined fields (exactOptionalPropertyTypes-safe). */\nfunction usageOf(inputTokens?: number, outputTokens?: number): Usage {\n const usage: { inputTokens?: number; outputTokens?: number } = {}\n if (inputTokens !== undefined) usage.inputTokens = inputTokens\n if (outputTokens !== undefined) usage.outputTokens = outputTokens\n return usage\n}\n\n/** Parse OpenAI's `function.arguments` (a JSON STRING) defensively into a value. */\nfunction parseJsonArgs(value: unknown): unknown {\n if (typeof value !== 'string') return value ?? {}\n try {\n return JSON.parse(value)\n } catch {\n return {} // a malformed arg string becomes empty args — the loop's validator rejects it\n }\n}\n\n/** Serialize a tool result to the string the wire APIs expect (never throws; bigint- + cycle-safe). */\nfunction toWireString(value: unknown): string {\n if (typeof value === 'string') return value\n if (value === null || typeof value !== 'object') {\n return typeof value === 'bigint' ? value.toString() : String(value)\n }\n try {\n // bigint → decimal string (plain JSON.stringify throws on it); re-seen objects → \"[Circular]\"\n // (a true cycle would throw too) — so objects/arrays serialize losslessly instead of \"[object Object]\".\n const seen = new WeakSet<object>()\n return (\n JSON.stringify(value, (_key, v) => {\n if (typeof v === 'bigint') return v.toString()\n if (v !== null && typeof v === 'object') {\n if (seen.has(v)) return '[Circular]'\n seen.add(v)\n }\n return v\n }) ?? String(value)\n )\n } catch {\n return String(value)\n }\n}\n\n/** Split a message's parts (string content has only text). */\nfunction partsOf(content: string | readonly Part[]): {\n text: string\n calls: ToolCallPart[]\n results: ToolResultPart[]\n} {\n if (typeof content === 'string') return { text: content, calls: [], results: [] }\n const text = content\n .filter((p): p is Extract<Part, { type: 'text' }> => p.type === 'text')\n .map((p) => p.text)\n .join('')\n const calls = content.filter((p): p is ToolCallPart => p.type === 'tool-call')\n const results = content.filter((p): p is ToolResultPart => p.type === 'tool-result')\n return { text, calls, results }\n}\n\n/**\n * Serialize messages to the OpenAI chat shape, round-tripping tool turns: an assistant turn\n * with tool calls emits `tool_calls`; a `tool` message emits one `{ role:'tool', tool_call_id }`\n * per result. Without this the tool loop's 2nd turn would lose its tool context.\n */\nfunction openaiMessages(messages: readonly Message[]): unknown[] {\n const out: unknown[] = []\n for (const m of messages) {\n const { text, calls, results } = partsOf(m.content)\n if (results.length > 0) {\n for (const r of results)\n out.push({ role: 'tool', tool_call_id: r.id, content: toWireString(r.result) })\n } else if (calls.length > 0) {\n out.push({\n role: m.role,\n content: text || null,\n tool_calls: calls.map((c) => ({\n id: c.id,\n type: 'function',\n function: { name: c.name, arguments: JSON.stringify(c.args ?? {}) },\n })),\n })\n } else {\n out.push({ role: m.role, content: text })\n }\n }\n return out\n}\n\n/**\n * Serialize non-system messages to the Anthropic shape, round-tripping tool turns: assistant\n * tool calls become `tool_use` blocks; a `tool` message becomes a USER message of `tool_result`\n * blocks (Anthropic carries tool results in the user turn).\n */\nfunction anthropicMessages(messages: readonly Message[]): unknown[] {\n const out: unknown[] = []\n for (const m of messages) {\n if (m.role === 'system') continue // folded into the top-level `system` field\n const { text, calls, results } = partsOf(m.content)\n if (results.length > 0) {\n out.push({\n role: 'user',\n content: results.map((r) => ({\n type: 'tool_result',\n tool_use_id: r.id,\n content: toWireString(r.result),\n })),\n })\n } else if (calls.length > 0) {\n const content: unknown[] = []\n if (text) content.push({ type: 'text', text })\n for (const c of calls)\n content.push({ type: 'tool_use', id: c.id, name: c.name, input: c.args ?? {} })\n out.push({ role: 'assistant', content })\n } else {\n out.push({ role: m.role, content: text })\n }\n }\n return out\n}\n\n/** Add `toolCalls` to an {@link AiResult} only when present (exactOptionalPropertyTypes-safe). */\nfunction withToolCalls(result: AiResult, toolCalls: ToolCallPart[]): AiResult {\n return toolCalls.length > 0 ? { ...result, toolCalls } : result\n}\n\n// null-prototype maps so an untrusted finish_reason like 'toString'/'__proto__' reads as\n// undefined (→ the 'stop' fallback) rather than leaking an inherited member.\nconst OPENAI_FINISH: Record<string, FinishReason> = Object.assign(Object.create(null), {\n stop: 'stop',\n length: 'length',\n tool_calls: 'tool-calls',\n})\nconst ANTHROPIC_FINISH: Record<string, FinishReason> = Object.assign(Object.create(null), {\n end_turn: 'stop',\n max_tokens: 'length',\n tool_use: 'tool-calls',\n})\n\n/** OpenAI-compatible `/chat/completions` mapper (also fits many local/compatible servers). */\nexport const openaiMapper: ProviderMapper = {\n auth: 'bearer',\n buildRequest(request, model, stream) {\n const body: Record<string, unknown> = {\n model,\n stream,\n messages: openaiMessages(request.messages),\n }\n if (request.temperature !== undefined) body.temperature = request.temperature\n if (request.maxOutputTokens !== undefined) body.max_tokens = request.maxOutputTokens\n if (request.tools && request.tools.length > 0) {\n body.tools = request.tools.map((t) => ({\n type: 'function',\n function: {\n name: t.name,\n ...(t.description !== undefined ? { description: t.description } : {}),\n parameters: t.parameters ?? { type: 'object', properties: {} },\n },\n }))\n }\n if (stream) body.stream_options = { include_usage: true }\n return { path: '/chat/completions', body }\n },\n parseResponse(json) {\n const root = asRecord(json)\n const choice = asRecord((root?.choices as unknown[] | undefined)?.[0])\n const message = asRecord(choice?.message)\n const usage = asRecord(root?.usage)\n const toolCalls: ToolCallPart[] = []\n for (const raw of (message?.tool_calls as unknown[] | undefined) ?? []) {\n const tc = asRecord(raw)\n const fn = asRecord(tc?.function)\n const name = asString(fn?.name)\n if (!name) continue\n toolCalls.push({\n type: 'tool-call',\n id: asString(tc?.id) ?? name,\n name,\n args: parseJsonArgs(fn?.arguments),\n })\n }\n return withToolCalls(\n {\n text: asString(message?.content) ?? '',\n finishReason: OPENAI_FINISH[asString(choice?.finish_reason) ?? ''] ?? 'stop',\n usage: usageOf(asNumber(usage?.prompt_tokens), asNumber(usage?.completion_tokens)),\n },\n toolCalls,\n )\n },\n createStreamParser() {\n // With stream_options.include_usage, OpenAI sends usage on a trailing choices:[] chunk\n // that carries NO finish_reason. Remember the last real finish_reason so the\n // usage-bearing finish reports the true reason instead of a fabricated 'stop'.\n let lastReason: FinishReason = 'stop'\n return (data) => {\n const root = asRecord(data)\n const choice = asRecord((root?.choices as unknown[] | undefined)?.[0])\n const out: AiChunk[] = []\n const delta = asString(asRecord(choice?.delta)?.content)\n if (delta) out.push({ type: 'text-delta', delta })\n const finish = asString(choice?.finish_reason)\n if (finish) lastReason = OPENAI_FINISH[finish] ?? 'stop'\n const usage = asRecord(root?.usage)\n // Emit a finish for a finish_reason chunk OR the trailing usage-only chunk, so\n // streamed usage isn't lost — both carry the last-seen reason. Emitted IN ADDITION\n // to any content delta in the same event (servers that bundle them are common), so\n // the terminal finish/usage is never dropped.\n if (finish || usage) {\n out.push({\n type: 'finish',\n finishReason: lastReason,\n usage: usageOf(asNumber(usage?.prompt_tokens), asNumber(usage?.completion_tokens)),\n })\n }\n return out\n }\n },\n}\n\n/** Anthropic `/v1/messages` mapper. */\nexport const anthropicMapper: ProviderMapper = {\n auth: 'anthropic',\n buildRequest(request, model, stream) {\n const system = request.messages\n .filter((m) => m.role === 'system')\n .map((m) => messageText(m))\n .join('\\n')\n const messages = anthropicMessages(request.messages)\n const body: Record<string, unknown> = {\n model,\n stream,\n messages,\n max_tokens: request.maxOutputTokens ?? 1024, // required by Anthropic\n }\n if (system) body.system = system\n if (request.temperature !== undefined) body.temperature = request.temperature\n if (request.tools && request.tools.length > 0) {\n body.tools = request.tools.map((t) => ({\n name: t.name,\n ...(t.description !== undefined ? { description: t.description } : {}),\n input_schema: t.parameters ?? { type: 'object', properties: {} },\n }))\n }\n return { path: '/v1/messages', body }\n },\n parseResponse(json) {\n const root = asRecord(json)\n const content = (root?.content as unknown[] | undefined) ?? []\n let text = ''\n const toolCalls: ToolCallPart[] = []\n for (const part of content) {\n const block = asRecord(part)\n if (asString(block?.type) === 'tool_use') {\n const name = asString(block?.name)\n if (!name) continue\n toolCalls.push({\n type: 'tool-call',\n id: asString(block?.id) ?? name,\n name,\n args: block?.input ?? {},\n })\n } else {\n text += asString(block?.text) ?? ''\n }\n }\n const usage = asRecord(root?.usage)\n return withToolCalls(\n {\n text,\n finishReason: ANTHROPIC_FINISH[asString(root?.stop_reason) ?? ''] ?? 'stop',\n usage: usageOf(asNumber(usage?.input_tokens), asNumber(usage?.output_tokens)),\n },\n toolCalls,\n )\n },\n createStreamParser() {\n // Anthropic reports input_tokens once on `message_start` and the (cumulative)\n // output_tokens on `message_delta`. They arrive in SEPARATE events, so the parser\n // must remember input_tokens from the start event to report it on the finish chunk —\n // otherwise every streamed response loses its prompt-token count.\n let inputTokens: number | undefined\n return (data) => {\n const root = asRecord(data)\n const type = asString(root?.type)\n if (type === 'message_start') {\n inputTokens = asNumber(asRecord(asRecord(root?.message)?.usage)?.input_tokens)\n return []\n }\n if (type === 'content_block_delta') {\n const delta = asString(asRecord(root?.delta)?.text)\n return delta ? [{ type: 'text-delta', delta }] : []\n }\n if (type === 'message_delta') {\n const stop = asString(asRecord(root?.delta)?.stop_reason)\n const usage = asRecord(root?.usage)\n return [\n {\n type: 'finish',\n finishReason: ANTHROPIC_FINISH[stop ?? ''] ?? 'stop',\n usage: usageOf(inputTokens, asNumber(usage?.output_tokens)),\n },\n ]\n }\n return []\n }\n },\n}\n\n/** The built-in mappers by adapter name. */\nexport const MAPPERS = { openai: openaiMapper, anthropic: anthropicMapper } as const\n\n/** A built-in provider adapter name. */\nexport type AdapterName = keyof typeof MAPPERS\n"],"mappings":"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"}
|
package/dist/server.js
CHANGED
|
@@ -46,14 +46,16 @@ function createServerBackend(options) {
|
|
|
46
46
|
const parseChunk = mapper.createStreamParser();
|
|
47
47
|
for await (const message of parseSse(decodeChunks(res.body))) {
|
|
48
48
|
if (message.data.trim() === "[DONE]") return;
|
|
49
|
-
if (
|
|
49
|
+
if (message.data.trim() === "") continue;
|
|
50
50
|
let parsed;
|
|
51
51
|
try {
|
|
52
52
|
parsed = JSON.parse(message.data);
|
|
53
53
|
} catch {
|
|
54
54
|
throw new AiError("STREAM_PARSE", `malformed SSE data: ${message.data.slice(0, 80)}`);
|
|
55
55
|
}
|
|
56
|
-
|
|
56
|
+
const chunks = [...parseChunk(parsed)];
|
|
57
|
+
if (!chunks.some((c) => c.type === "finish") && request.signal?.aborted) throw new AiError("ABORTED", "request aborted");
|
|
58
|
+
for (const chunk of chunks) yield chunk;
|
|
57
59
|
}
|
|
58
60
|
}
|
|
59
61
|
};
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","names":[],"sources":["../src/server.ts"],"sourcesContent":["/**\n * The Synapse server/HTTP backend — a real {@link AiBackend} that talks to a hosted\n * model API over an **injected `fetch`** (capability injection, like Pulse's\n * `fetchManifest`). Provider-agnostic via a {@link ProviderMapper}; streaming via the\n * pure-TS {@link parseSse} parser → `AsyncIterable<AiChunk>` (no Web/Node streams in the\n * public surface). Exported from the `@mindees/ai/server` subpath. See\n * `docs/adr/0018-synapse-server-backend.md`.\n *\n * @module\n */\n\nimport type { AiBackend, AiChunk, AiResult, GenerateRequest } from './contract'\nimport { AiError } from './errors'\nimport { type AdapterName, MAPPERS, type ProviderMapper } from './mappers'\nimport { decodeChunks, parseSse } from './sse'\n\nexport {\n type AdapterName,\n anthropicMapper,\n openaiMapper,\n type ProviderMapper,\n type StreamParser,\n} from './mappers'\nexport { decodeChunks, parseSse, type SseMessage } from './sse'\n\n/** A minimal `Response` shape (no DOM lib). A real `Response` is structurally compatible. */\nexport interface ResponseLike {\n readonly ok: boolean\n readonly status: number\n json(): Promise<unknown>\n text(): Promise<string>\n readonly body?: AsyncIterable<Uint8Array> | null\n}\n\n/** A minimal request init (no DOM lib). */\nexport interface RequestInitLike {\n readonly method: string\n readonly headers: Record<string, string>\n readonly body: string\n readonly signal?: unknown\n}\n\n/** A minimal `fetch` (no DOM lib). The global `fetch` is structurally compatible. */\nexport type FetchLike = (url: string, init: RequestInitLike) => Promise<ResponseLike>\n\n/** Options for {@link createServerBackend}. */\nexport interface ServerBackendOptions {\n /** Injected transport (the global `fetch`, or a fake in tests). */\n readonly fetch: FetchLike\n /** Base URL of the model API (no trailing slash). */\n readonly baseUrl: string\n /** Model id to request. */\n readonly model: string\n /** API key — sent as `Authorization: Bearer` (or Anthropic's `x-api-key`). */\n readonly apiKey?: string\n /** Provider adapter name or a custom {@link ProviderMapper}. Default `'openai'`. */\n readonly adapter?: AdapterName | ProviderMapper\n /** Extra headers merged over the defaults. */\n readonly headers?: Record<string, string>\n}\n\n/** Create a server/HTTP {@link AiBackend}. */\nexport function createServerBackend(options: ServerBackendOptions): AiBackend {\n const { baseUrl, model } = options\n const doFetch = options.fetch\n if (typeof doFetch !== 'function') {\n throw new AiError('NO_TRANSPORT', 'createServerBackend requires a `fetch`')\n }\n const mapper: ProviderMapper =\n typeof options.adapter === 'object' ? options.adapter : MAPPERS[options.adapter ?? 'openai']\n\n const buildHeaders = (): Record<string, string> => {\n const headers: Record<string, string> = {\n 'content-type': 'application/json',\n ...options.headers,\n }\n if (options.apiKey) {\n // Auth scheme follows the MAPPER (not the string-vs-object form the adapter was\n // supplied in), so `adapter: anthropicMapper` authenticates like `adapter: 'anthropic'`.\n if (mapper.auth === 'anthropic') {\n headers['x-api-key'] = options.apiKey\n headers['anthropic-version'] = '2023-06-01'\n } else {\n headers.authorization = `Bearer ${options.apiKey}`\n }\n }\n return headers\n }\n\n const send = async (request: GenerateRequest, stream: boolean): Promise<ResponseLike> => {\n if (request.signal?.aborted) throw new AiError('ABORTED', 'request aborted')\n const { path, body } = mapper.buildRequest(request, model, stream)\n const res = await doFetch(`${baseUrl}${path}`, {\n method: 'POST',\n headers: buildHeaders(),\n body: JSON.stringify(body),\n signal: request.signal,\n })\n if (!res.ok) {\n const detail = await res.text().catch(() => '')\n throw new AiError('HTTP_STATUS', `model API returned ${res.status}: ${detail.slice(0, 200)}`)\n }\n return res\n }\n\n return {\n async generate(request): Promise<AiResult> {\n const res = await send(request, false)\n const json = await res.json()\n // Re-check after the round-trip: an abort during the fetch/parse must surface,\n // matching stream()'s and runTools' polling model (an injected fetch may ignore\n // the forwarded signal).\n if (request.signal?.aborted) throw new AiError('ABORTED', 'request aborted')\n return mapper.parseResponse(json)\n },\n\n async *stream(request): AsyncIterable<AiChunk> {\n const res = await send(request, true)\n if (!res.body) throw new AiError('STREAM_PARSE', 'streaming response has no body')\n const parseChunk = mapper.createStreamParser()\n for await (const message of parseSse(decodeChunks(res.body))) {\n // Terminal sentinel first: a completed stream must resolve normally even if the\n // signal flips on this exact iteration (no spurious ABORTED on a done stream).\n if (message.data.trim() === '[DONE]') return\n
|
|
1
|
+
{"version":3,"file":"server.js","names":[],"sources":["../src/server.ts"],"sourcesContent":["/**\n * The Synapse server/HTTP backend — a real {@link AiBackend} that talks to a hosted\n * model API over an **injected `fetch`** (capability injection, like Pulse's\n * `fetchManifest`). Provider-agnostic via a {@link ProviderMapper}; streaming via the\n * pure-TS {@link parseSse} parser → `AsyncIterable<AiChunk>` (no Web/Node streams in the\n * public surface). Exported from the `@mindees/ai/server` subpath. See\n * `docs/adr/0018-synapse-server-backend.md`.\n *\n * @module\n */\n\nimport type { AiBackend, AiChunk, AiResult, GenerateRequest } from './contract'\nimport { AiError } from './errors'\nimport { type AdapterName, MAPPERS, type ProviderMapper } from './mappers'\nimport { decodeChunks, parseSse } from './sse'\n\nexport {\n type AdapterName,\n anthropicMapper,\n openaiMapper,\n type ProviderMapper,\n type StreamParser,\n} from './mappers'\nexport { decodeChunks, parseSse, type SseMessage } from './sse'\n\n/** A minimal `Response` shape (no DOM lib). A real `Response` is structurally compatible. */\nexport interface ResponseLike {\n readonly ok: boolean\n readonly status: number\n json(): Promise<unknown>\n text(): Promise<string>\n readonly body?: AsyncIterable<Uint8Array> | null\n}\n\n/** A minimal request init (no DOM lib). */\nexport interface RequestInitLike {\n readonly method: string\n readonly headers: Record<string, string>\n readonly body: string\n readonly signal?: unknown\n}\n\n/** A minimal `fetch` (no DOM lib). The global `fetch` is structurally compatible. */\nexport type FetchLike = (url: string, init: RequestInitLike) => Promise<ResponseLike>\n\n/** Options for {@link createServerBackend}. */\nexport interface ServerBackendOptions {\n /** Injected transport (the global `fetch`, or a fake in tests). */\n readonly fetch: FetchLike\n /** Base URL of the model API (no trailing slash). */\n readonly baseUrl: string\n /** Model id to request. */\n readonly model: string\n /** API key — sent as `Authorization: Bearer` (or Anthropic's `x-api-key`). */\n readonly apiKey?: string\n /** Provider adapter name or a custom {@link ProviderMapper}. Default `'openai'`. */\n readonly adapter?: AdapterName | ProviderMapper\n /** Extra headers merged over the defaults. */\n readonly headers?: Record<string, string>\n}\n\n/** Create a server/HTTP {@link AiBackend}. */\nexport function createServerBackend(options: ServerBackendOptions): AiBackend {\n const { baseUrl, model } = options\n const doFetch = options.fetch\n if (typeof doFetch !== 'function') {\n throw new AiError('NO_TRANSPORT', 'createServerBackend requires a `fetch`')\n }\n const mapper: ProviderMapper =\n typeof options.adapter === 'object' ? options.adapter : MAPPERS[options.adapter ?? 'openai']\n\n const buildHeaders = (): Record<string, string> => {\n const headers: Record<string, string> = {\n 'content-type': 'application/json',\n ...options.headers,\n }\n if (options.apiKey) {\n // Auth scheme follows the MAPPER (not the string-vs-object form the adapter was\n // supplied in), so `adapter: anthropicMapper` authenticates like `adapter: 'anthropic'`.\n if (mapper.auth === 'anthropic') {\n headers['x-api-key'] = options.apiKey\n headers['anthropic-version'] = '2023-06-01'\n } else {\n headers.authorization = `Bearer ${options.apiKey}`\n }\n }\n return headers\n }\n\n const send = async (request: GenerateRequest, stream: boolean): Promise<ResponseLike> => {\n if (request.signal?.aborted) throw new AiError('ABORTED', 'request aborted')\n const { path, body } = mapper.buildRequest(request, model, stream)\n const res = await doFetch(`${baseUrl}${path}`, {\n method: 'POST',\n headers: buildHeaders(),\n body: JSON.stringify(body),\n signal: request.signal,\n })\n if (!res.ok) {\n const detail = await res.text().catch(() => '')\n throw new AiError('HTTP_STATUS', `model API returned ${res.status}: ${detail.slice(0, 200)}`)\n }\n return res\n }\n\n return {\n async generate(request): Promise<AiResult> {\n const res = await send(request, false)\n const json = await res.json()\n // Re-check after the round-trip: an abort during the fetch/parse must surface,\n // matching stream()'s and runTools' polling model (an injected fetch may ignore\n // the forwarded signal).\n if (request.signal?.aborted) throw new AiError('ABORTED', 'request aborted')\n return mapper.parseResponse(json)\n },\n\n async *stream(request): AsyncIterable<AiChunk> {\n const res = await send(request, true)\n if (!res.body) throw new AiError('STREAM_PARSE', 'streaming response has no body')\n const parseChunk = mapper.createStreamParser()\n for await (const message of parseSse(decodeChunks(res.body))) {\n // Terminal sentinel first: a completed stream must resolve normally even if the\n // signal flips on this exact iteration (no spurious ABORTED on a done stream).\n if (message.data.trim() === '[DONE]') return\n // Skip empty keep-alive/heartbeat events (a gateway/proxy may emit a bare `data:`) — JSON.parse('')\n // would otherwise throw STREAM_PARSE and abort the whole stream.\n if (message.data.trim() === '') continue\n let parsed: unknown\n try {\n parsed = JSON.parse(message.data)\n } catch {\n throw new AiError('STREAM_PARSE', `malformed SSE data: ${message.data.slice(0, 80)}`)\n }\n const chunks = [...parseChunk(parsed)]\n // A terminal (finish) event completes the stream — deliver it even if the signal just flipped\n // (servers that omit [DONE] end with this event); only a MID-stream event honors a fresh abort.\n if (!chunks.some((c) => c.type === 'finish') && request.signal?.aborted) {\n throw new AiError('ABORTED', 'request aborted')\n }\n for (const chunk of chunks) yield chunk\n }\n },\n }\n}\n"],"mappings":";;;;;AA8DA,SAAgB,oBAAoB,SAA0C;CAC5E,MAAM,EAAE,SAAS,UAAU;CAC3B,MAAM,UAAU,QAAQ;CACxB,IAAI,OAAO,YAAY,YACrB,MAAM,IAAI,QAAQ,gBAAgB,wCAAwC;CAE5E,MAAM,SACJ,OAAO,QAAQ,YAAY,WAAW,QAAQ,UAAU,QAAQ,QAAQ,WAAW;CAErF,MAAM,qBAA6C;EACjD,MAAM,UAAkC;GACtC,gBAAgB;GAChB,GAAG,QAAQ;EACb;EACA,IAAI,QAAQ,QAGV,IAAI,OAAO,SAAS,aAAa;GAC/B,QAAQ,eAAe,QAAQ;GAC/B,QAAQ,uBAAuB;EACjC,OACE,QAAQ,gBAAgB,UAAU,QAAQ;EAG9C,OAAO;CACT;CAEA,MAAM,OAAO,OAAO,SAA0B,WAA2C;EACvF,IAAI,QAAQ,QAAQ,SAAS,MAAM,IAAI,QAAQ,WAAW,iBAAiB;EAC3E,MAAM,EAAE,MAAM,SAAS,OAAO,aAAa,SAAS,OAAO,MAAM;EACjE,MAAM,MAAM,MAAM,QAAQ,GAAG,UAAU,QAAQ;GAC7C,QAAQ;GACR,SAAS,aAAa;GACtB,MAAM,KAAK,UAAU,IAAI;GACzB,QAAQ,QAAQ;EAClB,CAAC;EACD,IAAI,CAAC,IAAI,IAAI;GACX,MAAM,SAAS,MAAM,IAAI,KAAK,EAAE,YAAY,EAAE;GAC9C,MAAM,IAAI,QAAQ,eAAe,sBAAsB,IAAI,OAAO,IAAI,OAAO,MAAM,GAAG,GAAG,GAAG;EAC9F;EACA,OAAO;CACT;CAEA,OAAO;EACL,MAAM,SAAS,SAA4B;GAEzC,MAAM,OAAO,OAAM,MADD,KAAK,SAAS,KAAK,GACd,KAAK;GAI5B,IAAI,QAAQ,QAAQ,SAAS,MAAM,IAAI,QAAQ,WAAW,iBAAiB;GAC3E,OAAO,OAAO,cAAc,IAAI;EAClC;EAEA,OAAO,OAAO,SAAiC;GAC7C,MAAM,MAAM,MAAM,KAAK,SAAS,IAAI;GACpC,IAAI,CAAC,IAAI,MAAM,MAAM,IAAI,QAAQ,gBAAgB,gCAAgC;GACjF,MAAM,aAAa,OAAO,mBAAmB;GAC7C,WAAW,MAAM,WAAW,SAAS,aAAa,IAAI,IAAI,CAAC,GAAG;IAG5D,IAAI,QAAQ,KAAK,KAAK,MAAM,UAAU;IAGtC,IAAI,QAAQ,KAAK,KAAK,MAAM,IAAI;IAChC,IAAI;IACJ,IAAI;KACF,SAAS,KAAK,MAAM,QAAQ,IAAI;IAClC,QAAQ;KACN,MAAM,IAAI,QAAQ,gBAAgB,uBAAuB,QAAQ,KAAK,MAAM,GAAG,EAAE,GAAG;IACtF;IACA,MAAM,SAAS,CAAC,GAAG,WAAW,MAAM,CAAC;IAGrC,IAAI,CAAC,OAAO,MAAM,MAAM,EAAE,SAAS,QAAQ,KAAK,QAAQ,QAAQ,SAC9D,MAAM,IAAI,QAAQ,WAAW,iBAAiB;IAEhD,KAAK,MAAM,SAAS,QAAQ,MAAM;GACpC;EACF;CACF;AACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mindees/ai",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.5",
|
|
4
4
|
"description": "MindeesNative Synapse - provider-agnostic AI: a pure-TS contract with mock + server backends, streaming via async iterables, structured output, and tool calling (on-device runtime is a research track).",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"directory": "packages/ai"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@mindees/core": "0.22.
|
|
34
|
+
"@mindees/core": "0.22.5"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
37
|
"build": "tsdown",
|