@graphorin/provider 0.5.0 → 0.6.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 (80) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +7 -7
  3. package/dist/adapters/llamacpp-server.d.ts +1 -1
  4. package/dist/adapters/llamacpp-server.js +1 -1
  5. package/dist/adapters/llamacpp-server.js.map +1 -1
  6. package/dist/adapters/ollama.d.ts.map +1 -1
  7. package/dist/adapters/ollama.js +3 -2
  8. package/dist/adapters/ollama.js.map +1 -1
  9. package/dist/adapters/openai-compatible.d.ts +14 -1
  10. package/dist/adapters/openai-compatible.d.ts.map +1 -1
  11. package/dist/adapters/openai-compatible.js +2 -0
  12. package/dist/adapters/openai-compatible.js.map +1 -1
  13. package/dist/adapters/vercel-messages.js +245 -0
  14. package/dist/adapters/vercel-messages.js.map +1 -0
  15. package/dist/adapters/vercel.d.ts +10 -8
  16. package/dist/adapters/vercel.d.ts.map +1 -1
  17. package/dist/adapters/vercel.js +54 -14
  18. package/dist/adapters/vercel.js.map +1 -1
  19. package/dist/counters/anthropic-wire.js +149 -0
  20. package/dist/counters/anthropic-wire.js.map +1 -0
  21. package/dist/counters/anthropic.d.ts +6 -1
  22. package/dist/counters/anthropic.d.ts.map +1 -1
  23. package/dist/counters/anthropic.js +25 -4
  24. package/dist/counters/anthropic.js.map +1 -1
  25. package/dist/counters/bedrock.js.map +1 -1
  26. package/dist/counters/dispatcher.d.ts.map +1 -1
  27. package/dist/counters/dispatcher.js +6 -4
  28. package/dist/counters/dispatcher.js.map +1 -1
  29. package/dist/counters/google.js.map +1 -1
  30. package/dist/counters/js-tiktoken.js.map +1 -1
  31. package/dist/errors/errors.d.ts +39 -11
  32. package/dist/errors/errors.d.ts.map +1 -1
  33. package/dist/errors/errors.js +54 -11
  34. package/dist/errors/errors.js.map +1 -1
  35. package/dist/errors/index.d.ts +2 -2
  36. package/dist/errors/index.js +2 -2
  37. package/dist/index.d.ts +5 -4
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +5 -5
  40. package/dist/index.js.map +1 -1
  41. package/dist/internal/abort.js +2 -2
  42. package/dist/internal/abort.js.map +1 -1
  43. package/dist/internal/http.js +19 -4
  44. package/dist/internal/http.js.map +1 -1
  45. package/dist/internal/openai-shaped.js +8 -3
  46. package/dist/internal/openai-shaped.js.map +1 -1
  47. package/dist/middleware/compose.d.ts +6 -6
  48. package/dist/middleware/compose.js +7 -7
  49. package/dist/middleware/compose.js.map +1 -1
  50. package/dist/middleware/production-hook.js.map +1 -1
  51. package/dist/middleware/with-cost-limit.d.ts +1 -1
  52. package/dist/middleware/with-cost-limit.js.map +1 -1
  53. package/dist/middleware/with-cost-tracking.d.ts +17 -4
  54. package/dist/middleware/with-cost-tracking.d.ts.map +1 -1
  55. package/dist/middleware/with-cost-tracking.js +12 -2
  56. package/dist/middleware/with-cost-tracking.js.map +1 -1
  57. package/dist/middleware/with-fallback.js +4 -2
  58. package/dist/middleware/with-fallback.js.map +1 -1
  59. package/dist/middleware/with-rate-limit.d.ts +2 -2
  60. package/dist/middleware/with-rate-limit.js.map +1 -1
  61. package/dist/middleware/with-redaction.d.ts +1 -1
  62. package/dist/middleware/with-redaction.js.map +1 -1
  63. package/dist/middleware/with-retry.js +3 -1
  64. package/dist/middleware/with-retry.js.map +1 -1
  65. package/dist/middleware/with-tracing.js +27 -6
  66. package/dist/middleware/with-tracing.js.map +1 -1
  67. package/dist/model-tier/classify.d.ts +1 -1
  68. package/dist/model-tier/classify.js +13 -5
  69. package/dist/model-tier/classify.js.map +1 -1
  70. package/dist/provider.js.map +1 -1
  71. package/dist/reasoning/apply-policy.js.map +1 -1
  72. package/dist/reasoning/classify-contract.js +14 -5
  73. package/dist/reasoning/classify-contract.js.map +1 -1
  74. package/dist/reasoning/retention.js.map +1 -1
  75. package/dist/tool-examples.d.ts +1 -1
  76. package/dist/tool-examples.js +1 -1
  77. package/dist/tool-examples.js.map +1 -1
  78. package/dist/trust/classify-local-provider.d.ts +1 -1
  79. package/dist/trust/classify-local-provider.js.map +1 -1
  80. package/package.json +6 -4
@@ -1 +1 @@
1
- {"version":3,"file":"http.js","names":["out: Record<string, unknown>","parsed: unknown","buffer: string[]","resp: Response"],"sources":["../../src/internal/http.ts"],"sourcesContent":["/**\n * Internal HTTP helpers shared across the local-LLM adapters.\n *\n * @internal\n */\n\nimport type { ProviderEvent } from '@graphorin/core';\n\nimport { ProviderHttpError } from '../errors/errors.js';\n\n/**\n * Convert a graphorin `Message` to the OpenAI-compatible chat-completion\n * shape. The shape is the lingua-franca of the bundled local adapters\n * (`llamaCppServerAdapter` and `openAICompatibleAdapter`); the\n * native-Ollama path uses its own conversion.\n *\n * @internal\n */\nexport function toOpenAIChatMessages(\n messages: ReadonlyArray<{\n readonly role: 'system' | 'user' | 'assistant' | 'tool';\n readonly content: string | ReadonlyArray<unknown>;\n readonly toolCalls?: ReadonlyArray<{\n readonly toolCallId: string;\n readonly toolName: string;\n readonly args: unknown;\n }>;\n readonly toolCallId?: string;\n }>,\n): ReadonlyArray<Record<string, unknown>> {\n return messages.map((msg) => {\n const out: Record<string, unknown> = {\n role: msg.role,\n content: typeof msg.content === 'string' ? msg.content : flattenContent(msg.content),\n };\n if (msg.toolCalls !== undefined && msg.toolCalls.length > 0) {\n out.tool_calls = msg.toolCalls.map((tc) => ({\n id: tc.toolCallId,\n type: 'function',\n function: {\n name: tc.toolName,\n arguments: typeof tc.args === 'string' ? tc.args : JSON.stringify(tc.args),\n },\n }));\n }\n if (msg.role === 'tool' && msg.toolCallId !== undefined) {\n out.tool_call_id = msg.toolCallId;\n }\n return out;\n });\n}\n\n/**\n * Convert a graphorin `Message` to Ollama's **native** `/api/chat` shape\n * (PS-13). Unlike the OpenAI form, Ollama's Go server expects `tool_calls`\n * with object `arguments` (a `map[string]any`, never a JSON string) and no\n * `id` / `type` fields — sending those breaks its unmarshaller and any\n * multi-turn replay of assistant tool calls.\n *\n * @internal\n */\nexport function toOllamaChatMessages(\n messages: ReadonlyArray<{\n readonly role: 'system' | 'user' | 'assistant' | 'tool';\n readonly content: string | ReadonlyArray<unknown>;\n readonly toolCalls?: ReadonlyArray<{\n readonly toolCallId: string;\n readonly toolName: string;\n readonly args: unknown;\n }>;\n readonly toolCallId?: string;\n }>,\n): ReadonlyArray<Record<string, unknown>> {\n return messages.map((msg) => {\n const out: Record<string, unknown> = {\n role: msg.role,\n content: typeof msg.content === 'string' ? msg.content : flattenContent(msg.content),\n };\n if (msg.toolCalls !== undefined && msg.toolCalls.length > 0) {\n out.tool_calls = msg.toolCalls.map((tc) => ({\n function: {\n name: tc.toolName,\n arguments: toArgsObject(tc.args),\n },\n }));\n }\n return out;\n });\n}\n\n/**\n * Coerce a tool-call `args` value into the object map Ollama expects. Strings\n * (e.g. an OpenAI-style JSON blob produced upstream) are parsed leniently;\n * anything that isn't a JSON object becomes `{}`.\n */\nfunction toArgsObject(args: unknown): Record<string, unknown> {\n if (typeof args === 'string') {\n try {\n const parsed: unknown = JSON.parse(args);\n return typeof parsed === 'object' && parsed !== null\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n }\n return typeof args === 'object' && args !== null ? (args as Record<string, unknown>) : {};\n}\n\nfunction flattenContent(parts: ReadonlyArray<unknown>): string {\n const buffer: string[] = [];\n for (const part of parts) {\n if (typeof part === 'string') {\n buffer.push(part);\n continue;\n }\n if (typeof part === 'object' && part !== null) {\n const obj = part as { type?: string; text?: string };\n if (obj.type === 'text' && typeof obj.text === 'string') {\n buffer.push(obj.text);\n }\n }\n }\n return buffer.join('');\n}\n\n/**\n * Wrap a `fetch` call with HTTP error mapping. The helper does not\n * assume any particular streaming format — callers receive the raw\n * `Response` and dispatch on its body.\n *\n * @internal\n */\n/**\n * Default per-request timeout for the baseUrl adapters (PS-24). Scoped\n * to time-to-response (headers): the timer is cleared the moment the\n * server answers, so long streaming bodies are never killed — only a\n * hung server that never responds. Generous because a cold local\n * llama-server can take tens of seconds to load a model.\n *\n * @stable\n */\nexport const DEFAULT_REQUEST_TIMEOUT_MS = 120_000;\n\nexport async function callJsonHttp(args: {\n readonly providerName: string;\n readonly url: string;\n readonly headers: Record<string, string>;\n readonly body: unknown;\n readonly signal?: AbortSignal;\n readonly fetchImpl?: typeof fetch;\n /**\n * Time-to-response budget (PS-24). Default\n * {@link DEFAULT_REQUEST_TIMEOUT_MS}; `0` disables.\n */\n readonly timeoutMs?: number;\n}): Promise<Response> {\n const fetchImpl = args.fetchImpl ?? globalThis.fetch.bind(globalThis);\n const timeoutMs = args.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n const timeoutCtl = timeoutMs > 0 ? new AbortController() : undefined;\n const timer =\n timeoutCtl !== undefined ? setTimeout(() => timeoutCtl.abort(), timeoutMs) : undefined;\n const signal =\n args.signal !== undefined && timeoutCtl !== undefined\n ? AbortSignal.any([args.signal, timeoutCtl.signal])\n : (args.signal ?? timeoutCtl?.signal);\n let resp: Response;\n try {\n resp = await fetchImpl(args.url, {\n method: 'POST',\n headers: args.headers,\n body: JSON.stringify(args.body),\n ...(signal !== undefined ? { signal } : {}),\n });\n } catch (cause) {\n if (timeoutCtl?.signal.aborted === true && args.signal?.aborted !== true) {\n throw new ProviderHttpError({\n providerName: args.providerName,\n status: 0,\n message: `request timed out after ${timeoutMs}ms reaching ${args.url}`,\n cause,\n });\n }\n throw new ProviderHttpError({\n providerName: args.providerName,\n status: 0,\n message: `network error reaching ${args.url}`,\n cause,\n });\n } finally {\n if (timer !== undefined) clearTimeout(timer);\n }\n if (!resp.ok) {\n const detail = await safeReadText(resp);\n throw new ProviderHttpError({\n providerName: args.providerName,\n status: resp.status,\n message: detail.length > 0 ? detail : resp.statusText,\n });\n }\n return resp;\n}\n\nasync function safeReadText(resp: Response): Promise<string> {\n try {\n return await resp.text();\n } catch {\n return '';\n }\n}\n\n/**\n * Yield a `stream-start` `ProviderEvent` for an HTTP adapter.\n *\n * @internal\n */\nexport function makeStreamStartEvent(args: {\n readonly providerName: string;\n readonly modelId: string;\n readonly responseId?: string;\n}): ProviderEvent {\n return {\n type: 'stream-start',\n metadata: {\n providerName: args.providerName,\n modelId: args.modelId,\n ...(args.responseId !== undefined ? { responseId: args.responseId } : {}),\n createdAt: new Date().toISOString(),\n },\n };\n}\n"],"mappings":";;;;;;;;;;;AAkBA,SAAgB,qBACd,UAUwC;AACxC,QAAO,SAAS,KAAK,QAAQ;EAC3B,MAAMA,MAA+B;GACnC,MAAM,IAAI;GACV,SAAS,OAAO,IAAI,YAAY,WAAW,IAAI,UAAU,eAAe,IAAI,QAAQ;GACrF;AACD,MAAI,IAAI,cAAc,UAAa,IAAI,UAAU,SAAS,EACxD,KAAI,aAAa,IAAI,UAAU,KAAK,QAAQ;GAC1C,IAAI,GAAG;GACP,MAAM;GACN,UAAU;IACR,MAAM,GAAG;IACT,WAAW,OAAO,GAAG,SAAS,WAAW,GAAG,OAAO,KAAK,UAAU,GAAG,KAAK;IAC3E;GACF,EAAE;AAEL,MAAI,IAAI,SAAS,UAAU,IAAI,eAAe,OAC5C,KAAI,eAAe,IAAI;AAEzB,SAAO;GACP;;;;;;;;;;;AAYJ,SAAgB,qBACd,UAUwC;AACxC,QAAO,SAAS,KAAK,QAAQ;EAC3B,MAAMA,MAA+B;GACnC,MAAM,IAAI;GACV,SAAS,OAAO,IAAI,YAAY,WAAW,IAAI,UAAU,eAAe,IAAI,QAAQ;GACrF;AACD,MAAI,IAAI,cAAc,UAAa,IAAI,UAAU,SAAS,EACxD,KAAI,aAAa,IAAI,UAAU,KAAK,QAAQ,EAC1C,UAAU;GACR,MAAM,GAAG;GACT,WAAW,aAAa,GAAG,KAAK;GACjC,EACF,EAAE;AAEL,SAAO;GACP;;;;;;;AAQJ,SAAS,aAAa,MAAwC;AAC5D,KAAI,OAAO,SAAS,SAClB,KAAI;EACF,MAAMC,SAAkB,KAAK,MAAM,KAAK;AACxC,SAAO,OAAO,WAAW,YAAY,WAAW,OAC3C,SACD,EAAE;SACA;AACN,SAAO,EAAE;;AAGb,QAAO,OAAO,SAAS,YAAY,SAAS,OAAQ,OAAmC,EAAE;;AAG3F,SAAS,eAAe,OAAuC;CAC7D,MAAMC,SAAmB,EAAE;AAC3B,MAAK,MAAM,QAAQ,OAAO;AACxB,MAAI,OAAO,SAAS,UAAU;AAC5B,UAAO,KAAK,KAAK;AACjB;;AAEF,MAAI,OAAO,SAAS,YAAY,SAAS,MAAM;GAC7C,MAAM,MAAM;AACZ,OAAI,IAAI,SAAS,UAAU,OAAO,IAAI,SAAS,SAC7C,QAAO,KAAK,IAAI,KAAK;;;AAI3B,QAAO,OAAO,KAAK,GAAG;;;;;;;;;;;;;;;;;;AAmBxB,MAAa,6BAA6B;AAE1C,eAAsB,aAAa,MAYb;CACpB,MAAM,YAAY,KAAK,aAAa,WAAW,MAAM,KAAK,WAAW;CACrE,MAAM,YAAY,KAAK,aAAa;CACpC,MAAM,aAAa,YAAY,IAAI,IAAI,iBAAiB,GAAG;CAC3D,MAAM,QACJ,eAAe,SAAY,iBAAiB,WAAW,OAAO,EAAE,UAAU,GAAG;CAC/E,MAAM,SACJ,KAAK,WAAW,UAAa,eAAe,SACxC,YAAY,IAAI,CAAC,KAAK,QAAQ,WAAW,OAAO,CAAC,GAChD,KAAK,UAAU,YAAY;CAClC,IAAIC;AACJ,KAAI;AACF,SAAO,MAAM,UAAU,KAAK,KAAK;GAC/B,QAAQ;GACR,SAAS,KAAK;GACd,MAAM,KAAK,UAAU,KAAK,KAAK;GAC/B,GAAI,WAAW,SAAY,EAAE,QAAQ,GAAG,EAAE;GAC3C,CAAC;UACK,OAAO;AACd,MAAI,YAAY,OAAO,YAAY,QAAQ,KAAK,QAAQ,YAAY,KAClE,OAAM,IAAI,kBAAkB;GAC1B,cAAc,KAAK;GACnB,QAAQ;GACR,SAAS,2BAA2B,UAAU,cAAc,KAAK;GACjE;GACD,CAAC;AAEJ,QAAM,IAAI,kBAAkB;GAC1B,cAAc,KAAK;GACnB,QAAQ;GACR,SAAS,0BAA0B,KAAK;GACxC;GACD,CAAC;WACM;AACR,MAAI,UAAU,OAAW,cAAa,MAAM;;AAE9C,KAAI,CAAC,KAAK,IAAI;EACZ,MAAM,SAAS,MAAM,aAAa,KAAK;AACvC,QAAM,IAAI,kBAAkB;GAC1B,cAAc,KAAK;GACnB,QAAQ,KAAK;GACb,SAAS,OAAO,SAAS,IAAI,SAAS,KAAK;GAC5C,CAAC;;AAEJ,QAAO;;AAGT,eAAe,aAAa,MAAiC;AAC3D,KAAI;AACF,SAAO,MAAM,KAAK,MAAM;SAClB;AACN,SAAO;;;;;;;;AASX,SAAgB,qBAAqB,MAInB;AAChB,QAAO;EACL,MAAM;EACN,UAAU;GACR,cAAc,KAAK;GACnB,SAAS,KAAK;GACd,GAAI,KAAK,eAAe,SAAY,EAAE,YAAY,KAAK,YAAY,GAAG,EAAE;GACxE,4BAAW,IAAI,MAAM,EAAC,aAAa;GACpC;EACF"}
1
+ {"version":3,"file":"http.js","names":["out: Record<string, unknown>","parsed: unknown","buffer: string[]","resp: Response","picked: Record<string, string>"],"sources":["../../src/internal/http.ts"],"sourcesContent":["/**\n * Internal HTTP helpers shared across the local-LLM adapters.\n *\n * @internal\n */\n\nimport type { ProviderEvent } from '@graphorin/core';\n\nimport { ProviderHttpError } from '../errors/errors.js';\n\n/**\n * Convert a graphorin `Message` to the OpenAI-compatible chat-completion\n * shape. The shape is the lingua-franca of the bundled local adapters\n * (`llamaCppServerAdapter` and `openAICompatibleAdapter`); the\n * native-Ollama path uses its own conversion.\n *\n * @internal\n */\nexport function toOpenAIChatMessages(\n messages: ReadonlyArray<{\n readonly role: 'system' | 'user' | 'assistant' | 'tool';\n readonly content: string | ReadonlyArray<unknown>;\n readonly toolCalls?: ReadonlyArray<{\n readonly toolCallId: string;\n readonly toolName: string;\n readonly args: unknown;\n }>;\n readonly toolCallId?: string;\n }>,\n): ReadonlyArray<Record<string, unknown>> {\n return messages.map((msg) => {\n const out: Record<string, unknown> = {\n role: msg.role,\n content: typeof msg.content === 'string' ? msg.content : flattenContent(msg.content),\n };\n if (msg.toolCalls !== undefined && msg.toolCalls.length > 0) {\n out.tool_calls = msg.toolCalls.map((tc) => ({\n id: tc.toolCallId,\n type: 'function',\n function: {\n name: tc.toolName,\n arguments: typeof tc.args === 'string' ? tc.args : JSON.stringify(tc.args),\n },\n }));\n }\n if (msg.role === 'tool' && msg.toolCallId !== undefined) {\n out.tool_call_id = msg.toolCallId;\n }\n return out;\n });\n}\n\n/**\n * Convert a graphorin `Message` to Ollama's **native** `/api/chat` shape\n * (PS-13). Unlike the OpenAI form, Ollama's Go server expects `tool_calls`\n * with object `arguments` (a `map[string]any`, never a JSON string) and no\n * `id` / `type` fields - sending those breaks its unmarshaller and any\n * multi-turn replay of assistant tool calls.\n *\n * @internal\n */\nexport function toOllamaChatMessages(\n messages: ReadonlyArray<{\n readonly role: 'system' | 'user' | 'assistant' | 'tool';\n readonly content: string | ReadonlyArray<unknown>;\n readonly toolCalls?: ReadonlyArray<{\n readonly toolCallId: string;\n readonly toolName: string;\n readonly args: unknown;\n }>;\n readonly toolCallId?: string;\n }>,\n): ReadonlyArray<Record<string, unknown>> {\n return messages.map((msg) => {\n const out: Record<string, unknown> = {\n role: msg.role,\n content: typeof msg.content === 'string' ? msg.content : flattenContent(msg.content),\n };\n if (msg.toolCalls !== undefined && msg.toolCalls.length > 0) {\n out.tool_calls = msg.toolCalls.map((tc) => ({\n function: {\n name: tc.toolName,\n arguments: toArgsObject(tc.args),\n },\n }));\n }\n return out;\n });\n}\n\n/**\n * Coerce a tool-call `args` value into the object map Ollama expects. Strings\n * (e.g. an OpenAI-style JSON blob produced upstream) are parsed leniently;\n * anything that isn't a JSON object becomes `{}`.\n */\nfunction toArgsObject(args: unknown): Record<string, unknown> {\n if (typeof args === 'string') {\n try {\n const parsed: unknown = JSON.parse(args);\n return typeof parsed === 'object' && parsed !== null\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n }\n return typeof args === 'object' && args !== null ? (args as Record<string, unknown>) : {};\n}\n\nfunction flattenContent(parts: ReadonlyArray<unknown>): string {\n const buffer: string[] = [];\n for (const part of parts) {\n if (typeof part === 'string') {\n buffer.push(part);\n continue;\n }\n if (typeof part === 'object' && part !== null) {\n const obj = part as { type?: string; text?: string };\n if (obj.type === 'text' && typeof obj.text === 'string') {\n buffer.push(obj.text);\n }\n }\n }\n return buffer.join('');\n}\n\n/**\n * Wrap a `fetch` call with HTTP error mapping. The helper does not\n * assume any particular streaming format - callers receive the raw\n * `Response` and dispatch on its body.\n *\n * @internal\n */\n/**\n * Default per-request timeout for the baseUrl adapters (PS-24). Scoped\n * to time-to-response (headers): the timer is cleared the moment the\n * server answers, so long streaming bodies are never killed - only a\n * hung server that never responds. Generous because a cold local\n * llama-server can take tens of seconds to load a model.\n *\n * @stable\n */\nexport const DEFAULT_REQUEST_TIMEOUT_MS = 120_000;\n\nexport async function callJsonHttp(args: {\n readonly providerName: string;\n readonly url: string;\n readonly headers: Record<string, string>;\n readonly body: unknown;\n readonly signal?: AbortSignal;\n readonly fetchImpl?: typeof fetch;\n /**\n * Time-to-response budget (PS-24). Default\n * {@link DEFAULT_REQUEST_TIMEOUT_MS}; `0` disables.\n */\n readonly timeoutMs?: number;\n}): Promise<Response> {\n const fetchImpl = args.fetchImpl ?? globalThis.fetch.bind(globalThis);\n const timeoutMs = args.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n const timeoutCtl = timeoutMs > 0 ? new AbortController() : undefined;\n const timer =\n timeoutCtl !== undefined ? setTimeout(() => timeoutCtl.abort(), timeoutMs) : undefined;\n const signal =\n args.signal !== undefined && timeoutCtl !== undefined\n ? AbortSignal.any([args.signal, timeoutCtl.signal])\n : (args.signal ?? timeoutCtl?.signal);\n let resp: Response;\n try {\n resp = await fetchImpl(args.url, {\n method: 'POST',\n headers: args.headers,\n body: JSON.stringify(args.body),\n ...(signal !== undefined ? { signal } : {}),\n });\n } catch (cause) {\n if (timeoutCtl?.signal.aborted === true && args.signal?.aborted !== true) {\n throw new ProviderHttpError({\n providerName: args.providerName,\n status: 0,\n message: `request timed out after ${timeoutMs}ms reaching ${args.url}`,\n cause,\n });\n }\n throw new ProviderHttpError({\n providerName: args.providerName,\n status: 0,\n message: `network error reaching ${args.url}`,\n cause,\n });\n } finally {\n if (timer !== undefined) clearTimeout(timer);\n }\n if (!resp.ok) {\n const detail = await safeReadText(resp);\n const headers = pickBackoffHeaders(resp.headers);\n throw new ProviderHttpError({\n providerName: args.providerName,\n status: resp.status,\n message: detail.length > 0 ? detail : resp.statusText,\n ...(headers !== undefined ? { headers } : {}),\n });\n }\n return resp;\n}\n\n/**\n * Capture the backoff-relevant response headers (`retry-after`,\n * `x-ratelimit-*`) so `withRetry`'s Retry-After hint reader can honour\n * server-provided delays. Returns `undefined` when none are present.\n */\nfunction pickBackoffHeaders(headers: Headers): Readonly<Record<string, string>> | undefined {\n const picked: Record<string, string> = {};\n headers.forEach((value, key) => {\n const lower = key.toLowerCase();\n if (lower === 'retry-after' || lower.startsWith('x-ratelimit-')) {\n picked[lower] = value;\n }\n });\n return Object.keys(picked).length > 0 ? picked : undefined;\n}\n\nasync function safeReadText(resp: Response): Promise<string> {\n try {\n return await resp.text();\n } catch {\n return '';\n }\n}\n\n/**\n * Yield a `stream-start` `ProviderEvent` for an HTTP adapter.\n *\n * @internal\n */\nexport function makeStreamStartEvent(args: {\n readonly providerName: string;\n readonly modelId: string;\n readonly responseId?: string;\n}): ProviderEvent {\n return {\n type: 'stream-start',\n metadata: {\n providerName: args.providerName,\n modelId: args.modelId,\n ...(args.responseId !== undefined ? { responseId: args.responseId } : {}),\n createdAt: new Date().toISOString(),\n },\n };\n}\n"],"mappings":"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"}
@@ -1,6 +1,7 @@
1
1
  import { LocalProviderInsecureTransportError, ProviderStreamParseError } from "../errors/errors.js";
2
2
  import { applyReasoningPolicy } from "../reasoning/apply-policy.js";
3
3
  import { resolveReasoningRetention } from "../reasoning/retention.js";
4
+ import { foldToolExamples } from "../tool-examples.js";
4
5
  import { classifyLocalProvider } from "../trust/classify-local-provider.js";
5
6
  import { callJsonHttp, makeStreamStartEvent, toOpenAIChatMessages } from "./http.js";
6
7
  import { parseEventStream } from "./sse.js";
@@ -220,9 +221,10 @@ function buildBody(model, req, stream, structuredOutput) {
220
221
  }, ...req.messages] : req.messages),
221
222
  stream
222
223
  };
224
+ if (stream) body.stream_options = { include_usage: true };
223
225
  if (req.temperature !== void 0) body.temperature = req.temperature;
224
226
  if (req.maxTokens !== void 0) body.max_tokens = req.maxTokens;
225
- if (req.tools !== void 0 && req.tools.length > 0) body.tools = req.tools.map((t) => ({
227
+ if (req.tools !== void 0 && req.tools.length > 0) body.tools = foldToolExamples(req.tools).map((t) => ({
226
228
  type: "function",
227
229
  function: {
228
230
  name: t.name,
@@ -277,11 +279,14 @@ function mapOpenAIUsage(input) {
277
279
  const u = input;
278
280
  const promptTokens = u.prompt_tokens ?? 0;
279
281
  const completionTokens = u.completion_tokens ?? 0;
282
+ const totalTokens = u.total_tokens ?? promptTokens + completionTokens;
283
+ const cachedTokens = u.prompt_tokens_details?.cached_tokens;
280
284
  return {
281
285
  promptTokens,
282
286
  completionTokens,
283
- totalTokens: u.total_tokens ?? promptTokens + completionTokens,
284
- ...u.reasoning_tokens !== void 0 ? { reasoningTokens: u.reasoning_tokens } : {}
287
+ totalTokens,
288
+ ...u.reasoning_tokens !== void 0 ? { reasoningTokens: u.reasoning_tokens } : {},
289
+ ...typeof cachedTokens === "number" && Number.isFinite(cachedTokens) && cachedTokens > 0 ? { cachedReadTokens: cachedTokens } : {}
285
290
  };
286
291
  }
287
292
  function parseFinalArgs(raw) {
@@ -1 +1 @@
1
- {"version":3,"file":"openai-shaped.js","names":["DEFAULT_CAPABILITIES: ProviderCapabilities","capabilities: ProviderCapabilities","finishReason: FinishReason","usage: Usage | undefined","parsed: OpenAIChunk","json: OpenAIChunk","body: Record<string, unknown>","headers: Record<string, string>"],"sources":["../../src/internal/openai-shaped.ts"],"sourcesContent":["/**\n * Shared implementation backing the two OpenAI-compatible adapters\n * (`llamaCppServerAdapter` and `openAICompatibleAdapter`). The wire\n * shape is identical for the `/v1/chat/completions` endpoint, so we\n * factor the streaming + one-shot logic here and the adapters become\n * thin shells declaring trust class, defaults, and capability flags.\n *\n * @internal\n */\n\nimport type {\n FinishReason,\n Provider,\n ProviderCapabilities,\n ProviderEvent,\n ProviderRequest,\n ProviderResponse,\n Sensitivity,\n Usage,\n} from '@graphorin/core';\n\nimport { LocalProviderInsecureTransportError, ProviderStreamParseError } from '../errors/errors.js';\nimport { applyReasoningPolicy } from '../reasoning/apply-policy.js';\nimport { resolveReasoningRetention } from '../reasoning/retention.js';\nimport {\n classifyLocalProvider,\n type LocalProviderClassification,\n} from '../trust/classify-local-provider.js';\nimport { callJsonHttp, makeStreamStartEvent, toOpenAIChatMessages } from './http.js';\nimport { parseEventStream } from './sse.js';\nimport { stripTrailingSlashes } from './url-utils.js';\n\n/**\n * Options shared by every OpenAI-compatible adapter.\n *\n * @internal\n */\nexport interface OpenAIShapedOptions {\n readonly providerName: string;\n readonly model: string;\n readonly baseUrl: string;\n readonly chatPath?: string;\n readonly apiKey?: string;\n readonly headers?: Readonly<Record<string, string>>;\n readonly fetchImpl?: typeof fetch;\n readonly allowInsecureTransport?: boolean;\n readonly capabilities?: Partial<ProviderCapabilities>;\n /**\n * Time-to-response budget per request (PS-24). Default\n * `DEFAULT_REQUEST_TIMEOUT_MS` (120s); `0` disables.\n */\n readonly timeoutMs?: number;\n /**\n * Overrides the default `acceptsSensitivity` value derived from the\n * trust classifier. Loud users can opt out of the conservative\n * default, but the framework still emits one WARN per process so\n * the override is visible.\n */\n readonly acceptsSensitivity?: ReadonlyArray<Sensitivity>;\n /**\n * Optional log sink. When unset, the adapter falls back to\n * `console.warn`. Tests pass a fixture-driven sink to silence the\n * console.\n */\n readonly logger?: (level: 'warn' | 'info', message: string, meta?: object) => void;\n}\n\nconst DEFAULT_CAPABILITIES: ProviderCapabilities = {\n streaming: true,\n toolCalling: true,\n parallelToolCalls: false,\n multimodal: false,\n structuredOutput: true,\n reasoning: false,\n contextWindow: 8_192,\n maxOutput: 4_096,\n reasoningContract: 'optional',\n};\n\nconst KNOWN_LOOPBACK_OVERRIDES_WARNED = new Set<string>();\n\n/**\n * Build a Graphorin `Provider` backed by an OpenAI-compatible HTTP\n * server. Returns the wrapped object plus the resolved trust\n * classification so the caller can attach it to span attributes if\n * desired.\n *\n * @internal\n */\nexport function buildOpenAIShapedProvider(opts: OpenAIShapedOptions): {\n readonly provider: Provider;\n readonly classification: LocalProviderClassification;\n} {\n const classification = classifyLocalProvider(opts.baseUrl);\n if (classification.trust === 'public-cleartext' && opts.allowInsecureTransport !== true) {\n throw new LocalProviderInsecureTransportError(opts.baseUrl);\n }\n const log = opts.logger ?? defaultLogger;\n if (classification.trust === 'public-cleartext') {\n log('warn', `[${opts.providerName}] allowInsecureTransport=true accepted for ${opts.baseUrl}`, {\n baseUrl: opts.baseUrl,\n });\n } else if (classification.trust === 'public-tls') {\n log('warn', `[${opts.providerName}] public-TLS endpoint; treating as cloud-tier`, {\n baseUrl: opts.baseUrl,\n });\n } else if (classification.trust === 'private') {\n log(\n 'warn',\n `[${opts.providerName}] private-network endpoint detected (${classification.reason})`,\n {\n baseUrl: opts.baseUrl,\n acceptsSensitivity: classification.acceptsSensitivity,\n },\n );\n }\n\n const acceptsSensitivity = opts.acceptsSensitivity ?? classification.acceptsSensitivity;\n if (\n opts.acceptsSensitivity !== undefined &&\n !sameSensitivity(opts.acceptsSensitivity, classification.acceptsSensitivity) &&\n !KNOWN_LOOPBACK_OVERRIDES_WARNED.has(opts.baseUrl)\n ) {\n KNOWN_LOOPBACK_OVERRIDES_WARNED.add(opts.baseUrl);\n log(\n 'info',\n `[${opts.providerName}] sensitivity override accepted; default would be [${classification.acceptsSensitivity.join(', ')}]`,\n {\n baseUrl: opts.baseUrl,\n },\n );\n }\n\n const capabilities: ProviderCapabilities = {\n ...DEFAULT_CAPABILITIES,\n ...opts.capabilities,\n };\n const chatPath = opts.chatPath ?? '/v1/chat/completions';\n const url = `${stripTrailingSlashes(opts.baseUrl)}${chatPath}`;\n\n const provider: Provider = {\n name: opts.providerName,\n modelId: opts.model,\n capabilities,\n acceptsSensitivity,\n stream(req) {\n return streamOpenAIShaped(opts, url, applyOpenAIShapedPreflight(req, capabilities));\n },\n async generate(req) {\n return generateOpenAIShaped(opts, url, applyOpenAIShapedPreflight(req, capabilities));\n },\n };\n\n return { provider, classification };\n}\n\nfunction applyOpenAIShapedPreflight(\n req: ProviderRequest,\n capabilities: ProviderCapabilities,\n): ProviderRequest {\n const retention = resolveReasoningRetention({\n ...(req.reasoningRetention !== undefined ? { requested: req.reasoningRetention } : {}),\n ...(capabilities.reasoningContract !== undefined\n ? { contract: capabilities.reasoningContract }\n : {}),\n });\n if (retention === 'pass-through-all') return req;\n const filtered = applyReasoningPolicy({ messages: req.messages, retention });\n if (filtered === req.messages) return req;\n return { ...req, messages: filtered };\n}\n\nasync function* streamOpenAIShaped(\n opts: OpenAIShapedOptions,\n url: string,\n req: ProviderRequest,\n): AsyncIterable<ProviderEvent> {\n const body = buildBody(opts.model, req, true, opts.capabilities?.structuredOutput ?? true);\n const resp = await callJsonHttp({\n providerName: opts.providerName,\n url,\n headers: buildHeaders(opts),\n body,\n ...(req.signal !== undefined ? { signal: req.signal } : {}),\n ...(opts.fetchImpl !== undefined ? { fetchImpl: opts.fetchImpl } : {}),\n ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),\n });\n\n yield makeStreamStartEvent({ providerName: opts.providerName, modelId: opts.model });\n\n let finishReason: FinishReason = 'stop';\n let usage: Usage | undefined;\n const toolCallBuffer = new Map<\n number,\n { id: string; name: string; args: string; emittedStart: boolean }\n >();\n\n for await (const payload of parseEventStream(\n resp.body,\n req.signal !== undefined ? { signal: req.signal } : {},\n )) {\n if (req.signal?.aborted) {\n // PS-12: report the abort honestly instead of the default 'stop'.\n finishReason = 'aborted';\n break;\n }\n let parsed: OpenAIChunk;\n try {\n parsed = JSON.parse(payload) as OpenAIChunk;\n } catch (cause) {\n throw new ProviderStreamParseError(\n opts.providerName,\n `failed to parse SSE chunk as JSON: ${(cause as Error).message}`,\n cause,\n );\n }\n const choice = parsed.choices?.[0];\n if (choice === undefined) {\n if (parsed.usage !== undefined) usage = mapOpenAIUsage(parsed.usage);\n continue;\n }\n const delta = choice.delta ?? {};\n if (typeof delta.content === 'string' && delta.content.length > 0) {\n yield { type: 'text-delta', delta: delta.content };\n }\n if (typeof (delta as { reasoning_content?: unknown }).reasoning_content === 'string') {\n const r = (delta as { reasoning_content: string }).reasoning_content;\n if (r.length > 0) yield { type: 'reasoning-delta', delta: r };\n }\n if (Array.isArray(delta.tool_calls)) {\n for (const tc of delta.tool_calls) {\n const idx = tc.index ?? 0;\n const slot = toolCallBuffer.get(idx) ?? {\n id: '',\n name: '',\n args: '',\n emittedStart: false,\n };\n if (typeof tc.id === 'string' && tc.id.length > 0) slot.id = tc.id;\n if (tc.function?.name !== undefined) slot.name = tc.function.name;\n if (typeof tc.function?.arguments === 'string') slot.args += tc.function.arguments;\n toolCallBuffer.set(idx, slot);\n if (!slot.emittedStart && slot.id.length > 0 && slot.name.length > 0) {\n yield {\n type: 'tool-call-start',\n toolCallId: slot.id,\n toolName: slot.name,\n };\n slot.emittedStart = true;\n }\n if (typeof tc.function?.arguments === 'string' && tc.function.arguments.length > 0) {\n yield {\n type: 'tool-call-input-delta',\n toolCallId: slot.id,\n argsDelta: tc.function.arguments,\n };\n }\n }\n }\n if (typeof choice.finish_reason === 'string') {\n finishReason = mapFinishReason(choice.finish_reason);\n for (const slot of toolCallBuffer.values()) {\n const finalArgs = parseFinalArgs(slot.args);\n yield {\n type: 'tool-call-end',\n toolCallId: slot.id,\n finalArgs,\n };\n }\n }\n if (parsed.usage !== undefined) usage = mapOpenAIUsage(parsed.usage);\n }\n\n yield {\n type: 'finish',\n finishReason,\n usage: usage ?? { promptTokens: 0, completionTokens: 0, totalTokens: 0 },\n };\n}\n\nasync function generateOpenAIShaped(\n opts: OpenAIShapedOptions,\n url: string,\n req: ProviderRequest,\n): Promise<ProviderResponse> {\n const body = buildBody(opts.model, req, false, opts.capabilities?.structuredOutput ?? true);\n const resp = await callJsonHttp({\n providerName: opts.providerName,\n url,\n headers: buildHeaders(opts),\n body,\n ...(req.signal !== undefined ? { signal: req.signal } : {}),\n ...(opts.fetchImpl !== undefined ? { fetchImpl: opts.fetchImpl } : {}),\n ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),\n });\n let json: OpenAIChunk;\n try {\n json = (await resp.json()) as OpenAIChunk;\n } catch (cause) {\n throw new ProviderStreamParseError(\n opts.providerName,\n 'response body was not valid JSON',\n cause,\n );\n }\n const choice = json.choices?.[0];\n const message = choice?.message ?? {};\n const text = typeof message.content === 'string' ? message.content : undefined;\n const toolCalls = Array.isArray(message.tool_calls)\n ? message.tool_calls.map((tc) => ({\n toolCallId: tc.id ?? '',\n toolName: tc.function?.name ?? '',\n args: parseFinalArgs(\n typeof tc.function?.arguments === 'string' ? tc.function.arguments : '',\n ),\n }))\n : undefined;\n const usage = mapOpenAIUsage(json.usage) ?? {\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n };\n return {\n usage,\n finishReason: mapFinishReason(choice?.finish_reason),\n ...(text !== undefined ? { text } : {}),\n ...(toolCalls !== undefined ? { toolCalls } : {}),\n };\n}\n\nfunction buildBody(\n model: string,\n req: ProviderRequest,\n stream: boolean,\n structuredOutput: boolean,\n): Record<string, unknown> {\n const messages =\n req.systemMessage !== undefined\n ? [{ role: 'system' as const, content: req.systemMessage }, ...req.messages]\n : req.messages;\n const body: Record<string, unknown> = {\n model,\n messages: toOpenAIChatMessages(messages),\n stream,\n };\n if (req.temperature !== undefined) body.temperature = req.temperature;\n if (req.maxTokens !== undefined) body.max_tokens = req.maxTokens;\n if (req.tools !== undefined && req.tools.length > 0) {\n body.tools = req.tools.map((t) => ({\n type: 'function',\n function: {\n name: t.name,\n ...(t.description !== undefined ? { description: t.description } : {}),\n parameters: t.inputSchema,\n },\n }));\n }\n if (req.toolChoice !== undefined) {\n body.tool_choice = mapToolChoice(req.toolChoice);\n }\n // PS-24: structured output finally reaches the wire — gated on the\n // declared capability so a structuredOutput:false override keeps the\n // request clean for servers that reject response_format.\n if (structuredOutput && req.outputType?.kind === 'structured') {\n body.response_format =\n req.outputType.jsonSchema !== undefined\n ? {\n type: 'json_schema',\n json_schema: { name: 'output', schema: req.outputType.jsonSchema, strict: true },\n }\n : { type: 'json_object' };\n }\n if (req.providerOptions !== undefined) {\n Object.assign(body, req.providerOptions);\n }\n return body;\n}\n\nfunction buildHeaders(opts: OpenAIShapedOptions): Record<string, string> {\n const headers: Record<string, string> = {\n 'content-type': 'application/json',\n accept: 'application/json',\n ...opts.headers,\n };\n if (opts.apiKey !== undefined && opts.apiKey.length > 0) {\n headers.authorization = `Bearer ${opts.apiKey}`;\n }\n return headers;\n}\n\nfunction mapToolChoice(value: unknown): unknown {\n if (value === 'auto' || value === 'none' || value === 'required') return value;\n if (typeof value === 'object' && value !== null && 'tool' in value) {\n return { type: 'function', function: { name: (value as { tool: string }).tool } };\n }\n return 'auto';\n}\n\nfunction mapFinishReason(value: string | null | undefined): FinishReason {\n switch (value) {\n case 'stop':\n case 'length':\n case 'content-filter':\n return value;\n // PS-12: the OpenAI wire format spells it with an underscore; the dashed\n // form alone never matched, so content-filtered completions reported 'stop'.\n case 'content_filter':\n return 'content-filter';\n case 'tool_calls':\n case 'function_call':\n return 'tool-calls';\n case null:\n case undefined:\n return 'stop';\n default:\n return 'stop';\n }\n}\n\nfunction mapOpenAIUsage(input: unknown): Usage | undefined {\n if (input === undefined || input === null || typeof input !== 'object') return undefined;\n const u = input as {\n prompt_tokens?: number;\n completion_tokens?: number;\n total_tokens?: number;\n reasoning_tokens?: number;\n };\n const promptTokens = u.prompt_tokens ?? 0;\n const completionTokens = u.completion_tokens ?? 0;\n const totalTokens = u.total_tokens ?? promptTokens + completionTokens;\n const usage: Usage = {\n promptTokens,\n completionTokens,\n totalTokens,\n ...(u.reasoning_tokens !== undefined ? { reasoningTokens: u.reasoning_tokens } : {}),\n };\n return usage;\n}\n\nfunction parseFinalArgs(raw: string): unknown {\n if (raw.length === 0) return {};\n try {\n return JSON.parse(raw);\n } catch {\n return raw;\n }\n}\n\nfunction sameSensitivity(a: ReadonlyArray<Sensitivity>, b: ReadonlyArray<Sensitivity>): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) return false;\n }\n return true;\n}\n\nfunction defaultLogger(level: 'warn' | 'info', message: string, meta?: object): void {\n const fn = level === 'warn' ? console.warn : console.info;\n if (meta !== undefined) {\n fn(`[graphorin/provider] ${message}`, meta);\n } else {\n fn(`[graphorin/provider] ${message}`);\n }\n}\n\ninterface OpenAIChunk {\n readonly id?: string;\n readonly choices?: ReadonlyArray<{\n readonly delta?: {\n readonly content?: string;\n readonly tool_calls?: ReadonlyArray<{\n readonly index?: number;\n readonly id?: string;\n readonly type?: string;\n readonly function?: { readonly name?: string; readonly arguments?: string };\n }>;\n readonly reasoning_content?: string;\n };\n readonly message?: {\n readonly content?: string | null;\n readonly tool_calls?: ReadonlyArray<{\n readonly id?: string;\n readonly type?: string;\n readonly function?: { readonly name?: string; readonly arguments?: string };\n }>;\n };\n readonly finish_reason?: string | null;\n }>;\n readonly usage?: {\n readonly prompt_tokens?: number;\n readonly completion_tokens?: number;\n readonly total_tokens?: number;\n readonly reasoning_tokens?: number;\n };\n}\n"],"mappings":";;;;;;;;;AAmEA,MAAMA,uBAA6C;CACjD,WAAW;CACX,aAAa;CACb,mBAAmB;CACnB,YAAY;CACZ,kBAAkB;CAClB,WAAW;CACX,eAAe;CACf,WAAW;CACX,mBAAmB;CACpB;AAED,MAAM,kDAAkC,IAAI,KAAa;;;;;;;;;AAUzD,SAAgB,0BAA0B,MAGxC;CACA,MAAM,iBAAiB,sBAAsB,KAAK,QAAQ;AAC1D,KAAI,eAAe,UAAU,sBAAsB,KAAK,2BAA2B,KACjF,OAAM,IAAI,oCAAoC,KAAK,QAAQ;CAE7D,MAAM,MAAM,KAAK,UAAU;AAC3B,KAAI,eAAe,UAAU,mBAC3B,KAAI,QAAQ,IAAI,KAAK,aAAa,6CAA6C,KAAK,WAAW,EAC7F,SAAS,KAAK,SACf,CAAC;UACO,eAAe,UAAU,aAClC,KAAI,QAAQ,IAAI,KAAK,aAAa,gDAAgD,EAChF,SAAS,KAAK,SACf,CAAC;UACO,eAAe,UAAU,UAClC,KACE,QACA,IAAI,KAAK,aAAa,uCAAuC,eAAe,OAAO,IACnF;EACE,SAAS,KAAK;EACd,oBAAoB,eAAe;EACpC,CACF;CAGH,MAAM,qBAAqB,KAAK,sBAAsB,eAAe;AACrE,KACE,KAAK,uBAAuB,UAC5B,CAAC,gBAAgB,KAAK,oBAAoB,eAAe,mBAAmB,IAC5E,CAAC,gCAAgC,IAAI,KAAK,QAAQ,EAClD;AACA,kCAAgC,IAAI,KAAK,QAAQ;AACjD,MACE,QACA,IAAI,KAAK,aAAa,qDAAqD,eAAe,mBAAmB,KAAK,KAAK,CAAC,IACxH,EACE,SAAS,KAAK,SACf,CACF;;CAGH,MAAMC,eAAqC;EACzC,GAAG;EACH,GAAG,KAAK;EACT;CACD,MAAM,WAAW,KAAK,YAAY;CAClC,MAAM,MAAM,GAAG,qBAAqB,KAAK,QAAQ,GAAG;AAepD,QAAO;EAAE,UAbkB;GACzB,MAAM,KAAK;GACX,SAAS,KAAK;GACd;GACA;GACA,OAAO,KAAK;AACV,WAAO,mBAAmB,MAAM,KAAK,2BAA2B,KAAK,aAAa,CAAC;;GAErF,MAAM,SAAS,KAAK;AAClB,WAAO,qBAAqB,MAAM,KAAK,2BAA2B,KAAK,aAAa,CAAC;;GAExF;EAEkB;EAAgB;;AAGrC,SAAS,2BACP,KACA,cACiB;CACjB,MAAM,YAAY,0BAA0B;EAC1C,GAAI,IAAI,uBAAuB,SAAY,EAAE,WAAW,IAAI,oBAAoB,GAAG,EAAE;EACrF,GAAI,aAAa,sBAAsB,SACnC,EAAE,UAAU,aAAa,mBAAmB,GAC5C,EAAE;EACP,CAAC;AACF,KAAI,cAAc,mBAAoB,QAAO;CAC7C,MAAM,WAAW,qBAAqB;EAAE,UAAU,IAAI;EAAU;EAAW,CAAC;AAC5E,KAAI,aAAa,IAAI,SAAU,QAAO;AACtC,QAAO;EAAE,GAAG;EAAK,UAAU;EAAU;;AAGvC,gBAAgB,mBACd,MACA,KACA,KAC8B;CAC9B,MAAM,OAAO,UAAU,KAAK,OAAO,KAAK,MAAM,KAAK,cAAc,oBAAoB,KAAK;CAC1F,MAAM,OAAO,MAAM,aAAa;EAC9B,cAAc,KAAK;EACnB;EACA,SAAS,aAAa,KAAK;EAC3B;EACA,GAAI,IAAI,WAAW,SAAY,EAAE,QAAQ,IAAI,QAAQ,GAAG,EAAE;EAC1D,GAAI,KAAK,cAAc,SAAY,EAAE,WAAW,KAAK,WAAW,GAAG,EAAE;EACrE,GAAI,KAAK,cAAc,SAAY,EAAE,WAAW,KAAK,WAAW,GAAG,EAAE;EACtE,CAAC;AAEF,OAAM,qBAAqB;EAAE,cAAc,KAAK;EAAc,SAAS,KAAK;EAAO,CAAC;CAEpF,IAAIC,eAA6B;CACjC,IAAIC;CACJ,MAAM,iCAAiB,IAAI,KAGxB;AAEH,YAAW,MAAM,WAAW,iBAC1B,KAAK,MACL,IAAI,WAAW,SAAY,EAAE,QAAQ,IAAI,QAAQ,GAAG,EAAE,CACvD,EAAE;AACD,MAAI,IAAI,QAAQ,SAAS;AAEvB,kBAAe;AACf;;EAEF,IAAIC;AACJ,MAAI;AACF,YAAS,KAAK,MAAM,QAAQ;WACrB,OAAO;AACd,SAAM,IAAI,yBACR,KAAK,cACL,sCAAuC,MAAgB,WACvD,MACD;;EAEH,MAAM,SAAS,OAAO,UAAU;AAChC,MAAI,WAAW,QAAW;AACxB,OAAI,OAAO,UAAU,OAAW,SAAQ,eAAe,OAAO,MAAM;AACpE;;EAEF,MAAM,QAAQ,OAAO,SAAS,EAAE;AAChC,MAAI,OAAO,MAAM,YAAY,YAAY,MAAM,QAAQ,SAAS,EAC9D,OAAM;GAAE,MAAM;GAAc,OAAO,MAAM;GAAS;AAEpD,MAAI,OAAQ,MAA0C,sBAAsB,UAAU;GACpF,MAAM,IAAK,MAAwC;AACnD,OAAI,EAAE,SAAS,EAAG,OAAM;IAAE,MAAM;IAAmB,OAAO;IAAG;;AAE/D,MAAI,MAAM,QAAQ,MAAM,WAAW,CACjC,MAAK,MAAM,MAAM,MAAM,YAAY;GACjC,MAAM,MAAM,GAAG,SAAS;GACxB,MAAM,OAAO,eAAe,IAAI,IAAI,IAAI;IACtC,IAAI;IACJ,MAAM;IACN,MAAM;IACN,cAAc;IACf;AACD,OAAI,OAAO,GAAG,OAAO,YAAY,GAAG,GAAG,SAAS,EAAG,MAAK,KAAK,GAAG;AAChE,OAAI,GAAG,UAAU,SAAS,OAAW,MAAK,OAAO,GAAG,SAAS;AAC7D,OAAI,OAAO,GAAG,UAAU,cAAc,SAAU,MAAK,QAAQ,GAAG,SAAS;AACzE,kBAAe,IAAI,KAAK,KAAK;AAC7B,OAAI,CAAC,KAAK,gBAAgB,KAAK,GAAG,SAAS,KAAK,KAAK,KAAK,SAAS,GAAG;AACpE,UAAM;KACJ,MAAM;KACN,YAAY,KAAK;KACjB,UAAU,KAAK;KAChB;AACD,SAAK,eAAe;;AAEtB,OAAI,OAAO,GAAG,UAAU,cAAc,YAAY,GAAG,SAAS,UAAU,SAAS,EAC/E,OAAM;IACJ,MAAM;IACN,YAAY,KAAK;IACjB,WAAW,GAAG,SAAS;IACxB;;AAIP,MAAI,OAAO,OAAO,kBAAkB,UAAU;AAC5C,kBAAe,gBAAgB,OAAO,cAAc;AACpD,QAAK,MAAM,QAAQ,eAAe,QAAQ,EAAE;IAC1C,MAAM,YAAY,eAAe,KAAK,KAAK;AAC3C,UAAM;KACJ,MAAM;KACN,YAAY,KAAK;KACjB;KACD;;;AAGL,MAAI,OAAO,UAAU,OAAW,SAAQ,eAAe,OAAO,MAAM;;AAGtE,OAAM;EACJ,MAAM;EACN;EACA,OAAO,SAAS;GAAE,cAAc;GAAG,kBAAkB;GAAG,aAAa;GAAG;EACzE;;AAGH,eAAe,qBACb,MACA,KACA,KAC2B;CAC3B,MAAM,OAAO,UAAU,KAAK,OAAO,KAAK,OAAO,KAAK,cAAc,oBAAoB,KAAK;CAC3F,MAAM,OAAO,MAAM,aAAa;EAC9B,cAAc,KAAK;EACnB;EACA,SAAS,aAAa,KAAK;EAC3B;EACA,GAAI,IAAI,WAAW,SAAY,EAAE,QAAQ,IAAI,QAAQ,GAAG,EAAE;EAC1D,GAAI,KAAK,cAAc,SAAY,EAAE,WAAW,KAAK,WAAW,GAAG,EAAE;EACrE,GAAI,KAAK,cAAc,SAAY,EAAE,WAAW,KAAK,WAAW,GAAG,EAAE;EACtE,CAAC;CACF,IAAIC;AACJ,KAAI;AACF,SAAQ,MAAM,KAAK,MAAM;UAClB,OAAO;AACd,QAAM,IAAI,yBACR,KAAK,cACL,oCACA,MACD;;CAEH,MAAM,SAAS,KAAK,UAAU;CAC9B,MAAM,UAAU,QAAQ,WAAW,EAAE;CACrC,MAAM,OAAO,OAAO,QAAQ,YAAY,WAAW,QAAQ,UAAU;CACrE,MAAM,YAAY,MAAM,QAAQ,QAAQ,WAAW,GAC/C,QAAQ,WAAW,KAAK,QAAQ;EAC9B,YAAY,GAAG,MAAM;EACrB,UAAU,GAAG,UAAU,QAAQ;EAC/B,MAAM,eACJ,OAAO,GAAG,UAAU,cAAc,WAAW,GAAG,SAAS,YAAY,GACtE;EACF,EAAE,GACH;AAMJ,QAAO;EACL,OANY,eAAe,KAAK,MAAM,IAAI;GAC1C,cAAc;GACd,kBAAkB;GAClB,aAAa;GACd;EAGC,cAAc,gBAAgB,QAAQ,cAAc;EACpD,GAAI,SAAS,SAAY,EAAE,MAAM,GAAG,EAAE;EACtC,GAAI,cAAc,SAAY,EAAE,WAAW,GAAG,EAAE;EACjD;;AAGH,SAAS,UACP,OACA,KACA,QACA,kBACyB;CAKzB,MAAMC,OAAgC;EACpC;EACA,UAAU,qBALV,IAAI,kBAAkB,SAClB,CAAC;GAAE,MAAM;GAAmB,SAAS,IAAI;GAAe,EAAE,GAAG,IAAI,SAAS,GAC1E,IAAI,SAGgC;EACxC;EACD;AACD,KAAI,IAAI,gBAAgB,OAAW,MAAK,cAAc,IAAI;AAC1D,KAAI,IAAI,cAAc,OAAW,MAAK,aAAa,IAAI;AACvD,KAAI,IAAI,UAAU,UAAa,IAAI,MAAM,SAAS,EAChD,MAAK,QAAQ,IAAI,MAAM,KAAK,OAAO;EACjC,MAAM;EACN,UAAU;GACR,MAAM,EAAE;GACR,GAAI,EAAE,gBAAgB,SAAY,EAAE,aAAa,EAAE,aAAa,GAAG,EAAE;GACrE,YAAY,EAAE;GACf;EACF,EAAE;AAEL,KAAI,IAAI,eAAe,OACrB,MAAK,cAAc,cAAc,IAAI,WAAW;AAKlD,KAAI,oBAAoB,IAAI,YAAY,SAAS,aAC/C,MAAK,kBACH,IAAI,WAAW,eAAe,SAC1B;EACE,MAAM;EACN,aAAa;GAAE,MAAM;GAAU,QAAQ,IAAI,WAAW;GAAY,QAAQ;GAAM;EACjF,GACD,EAAE,MAAM,eAAe;AAE/B,KAAI,IAAI,oBAAoB,OAC1B,QAAO,OAAO,MAAM,IAAI,gBAAgB;AAE1C,QAAO;;AAGT,SAAS,aAAa,MAAmD;CACvE,MAAMC,UAAkC;EACtC,gBAAgB;EAChB,QAAQ;EACR,GAAG,KAAK;EACT;AACD,KAAI,KAAK,WAAW,UAAa,KAAK,OAAO,SAAS,EACpD,SAAQ,gBAAgB,UAAU,KAAK;AAEzC,QAAO;;AAGT,SAAS,cAAc,OAAyB;AAC9C,KAAI,UAAU,UAAU,UAAU,UAAU,UAAU,WAAY,QAAO;AACzE,KAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,UAAU,MAC3D,QAAO;EAAE,MAAM;EAAY,UAAU,EAAE,MAAO,MAA2B,MAAM;EAAE;AAEnF,QAAO;;AAGT,SAAS,gBAAgB,OAAgD;AACvE,SAAQ,OAAR;EACE,KAAK;EACL,KAAK;EACL,KAAK,iBACH,QAAO;EAGT,KAAK,iBACH,QAAO;EACT,KAAK;EACL,KAAK,gBACH,QAAO;EACT,KAAK;EACL,KAAK,OACH,QAAO;EACT,QACE,QAAO;;;AAIb,SAAS,eAAe,OAAmC;AACzD,KAAI,UAAU,UAAa,UAAU,QAAQ,OAAO,UAAU,SAAU,QAAO;CAC/E,MAAM,IAAI;CAMV,MAAM,eAAe,EAAE,iBAAiB;CACxC,MAAM,mBAAmB,EAAE,qBAAqB;AAQhD,QANqB;EACnB;EACA;EACA,aAJkB,EAAE,gBAAgB,eAAe;EAKnD,GAAI,EAAE,qBAAqB,SAAY,EAAE,iBAAiB,EAAE,kBAAkB,GAAG,EAAE;EACpF;;AAIH,SAAS,eAAe,KAAsB;AAC5C,KAAI,IAAI,WAAW,EAAG,QAAO,EAAE;AAC/B,KAAI;AACF,SAAO,KAAK,MAAM,IAAI;SAChB;AACN,SAAO;;;AAIX,SAAS,gBAAgB,GAA+B,GAAwC;AAC9F,KAAI,EAAE,WAAW,EAAE,OAAQ,QAAO;AAClC,MAAK,IAAI,IAAI,GAAG,IAAI,EAAE,QAAQ,IAC5B,KAAI,EAAE,OAAO,EAAE,GAAI,QAAO;AAE5B,QAAO;;AAGT,SAAS,cAAc,OAAwB,SAAiB,MAAqB;CACnF,MAAM,KAAK,UAAU,SAAS,QAAQ,OAAO,QAAQ;AACrD,KAAI,SAAS,OACX,IAAG,wBAAwB,WAAW,KAAK;KAE3C,IAAG,wBAAwB,UAAU"}
1
+ {"version":3,"file":"openai-shaped.js","names":["DEFAULT_CAPABILITIES: ProviderCapabilities","capabilities: ProviderCapabilities","finishReason: FinishReason","usage: Usage | undefined","parsed: OpenAIChunk","json: OpenAIChunk","body: Record<string, unknown>","headers: Record<string, string>"],"sources":["../../src/internal/openai-shaped.ts"],"sourcesContent":["/**\n * Shared implementation backing the two OpenAI-compatible adapters\n * (`llamaCppServerAdapter` and `openAICompatibleAdapter`). The wire\n * shape is identical for the `/v1/chat/completions` endpoint, so we\n * factor the streaming + one-shot logic here and the adapters become\n * thin shells declaring trust class, defaults, and capability flags.\n *\n * @internal\n */\n\nimport type {\n FinishReason,\n Provider,\n ProviderCapabilities,\n ProviderEvent,\n ProviderRequest,\n ProviderResponse,\n Sensitivity,\n Usage,\n} from '@graphorin/core';\n\nimport { LocalProviderInsecureTransportError, ProviderStreamParseError } from '../errors/errors.js';\nimport { applyReasoningPolicy } from '../reasoning/apply-policy.js';\nimport { resolveReasoningRetention } from '../reasoning/retention.js';\nimport { foldToolExamples } from '../tool-examples.js';\nimport {\n classifyLocalProvider,\n type LocalProviderClassification,\n} from '../trust/classify-local-provider.js';\nimport { callJsonHttp, makeStreamStartEvent, toOpenAIChatMessages } from './http.js';\nimport { parseEventStream } from './sse.js';\nimport { stripTrailingSlashes } from './url-utils.js';\n\n/**\n * Options shared by every OpenAI-compatible adapter.\n *\n * @internal\n */\nexport interface OpenAIShapedOptions {\n readonly providerName: string;\n readonly model: string;\n readonly baseUrl: string;\n readonly chatPath?: string;\n readonly apiKey?: string;\n readonly headers?: Readonly<Record<string, string>>;\n readonly fetchImpl?: typeof fetch;\n readonly allowInsecureTransport?: boolean;\n readonly capabilities?: Partial<ProviderCapabilities>;\n /**\n * Time-to-response budget per request (PS-24). Default\n * `DEFAULT_REQUEST_TIMEOUT_MS` (120s); `0` disables.\n */\n readonly timeoutMs?: number;\n /**\n * Overrides the default `acceptsSensitivity` value derived from the\n * trust classifier. Loud users can opt out of the conservative\n * default, but the framework still emits one WARN per process so\n * the override is visible.\n */\n readonly acceptsSensitivity?: ReadonlyArray<Sensitivity>;\n /**\n * Optional log sink. When unset, the adapter falls back to\n * `console.warn`. Tests pass a fixture-driven sink to silence the\n * console.\n */\n readonly logger?: (level: 'warn' | 'info', message: string, meta?: object) => void;\n}\n\nconst DEFAULT_CAPABILITIES: ProviderCapabilities = {\n streaming: true,\n toolCalling: true,\n parallelToolCalls: false,\n multimodal: false,\n structuredOutput: true,\n reasoning: false,\n contextWindow: 8_192,\n maxOutput: 4_096,\n reasoningContract: 'optional',\n};\n\nconst KNOWN_LOOPBACK_OVERRIDES_WARNED = new Set<string>();\n\n/**\n * Build a Graphorin `Provider` backed by an OpenAI-compatible HTTP\n * server. Returns the wrapped object plus the resolved trust\n * classification so the caller can attach it to span attributes if\n * desired.\n *\n * @internal\n */\nexport function buildOpenAIShapedProvider(opts: OpenAIShapedOptions): {\n readonly provider: Provider;\n readonly classification: LocalProviderClassification;\n} {\n const classification = classifyLocalProvider(opts.baseUrl);\n if (classification.trust === 'public-cleartext' && opts.allowInsecureTransport !== true) {\n throw new LocalProviderInsecureTransportError(opts.baseUrl);\n }\n const log = opts.logger ?? defaultLogger;\n if (classification.trust === 'public-cleartext') {\n log('warn', `[${opts.providerName}] allowInsecureTransport=true accepted for ${opts.baseUrl}`, {\n baseUrl: opts.baseUrl,\n });\n } else if (classification.trust === 'public-tls') {\n log('warn', `[${opts.providerName}] public-TLS endpoint; treating as cloud-tier`, {\n baseUrl: opts.baseUrl,\n });\n } else if (classification.trust === 'private') {\n log(\n 'warn',\n `[${opts.providerName}] private-network endpoint detected (${classification.reason})`,\n {\n baseUrl: opts.baseUrl,\n acceptsSensitivity: classification.acceptsSensitivity,\n },\n );\n }\n\n const acceptsSensitivity = opts.acceptsSensitivity ?? classification.acceptsSensitivity;\n if (\n opts.acceptsSensitivity !== undefined &&\n !sameSensitivity(opts.acceptsSensitivity, classification.acceptsSensitivity) &&\n !KNOWN_LOOPBACK_OVERRIDES_WARNED.has(opts.baseUrl)\n ) {\n KNOWN_LOOPBACK_OVERRIDES_WARNED.add(opts.baseUrl);\n log(\n 'info',\n `[${opts.providerName}] sensitivity override accepted; default would be [${classification.acceptsSensitivity.join(', ')}]`,\n {\n baseUrl: opts.baseUrl,\n },\n );\n }\n\n const capabilities: ProviderCapabilities = {\n ...DEFAULT_CAPABILITIES,\n ...opts.capabilities,\n };\n const chatPath = opts.chatPath ?? '/v1/chat/completions';\n const url = `${stripTrailingSlashes(opts.baseUrl)}${chatPath}`;\n\n const provider: Provider = {\n name: opts.providerName,\n modelId: opts.model,\n capabilities,\n acceptsSensitivity,\n stream(req) {\n return streamOpenAIShaped(opts, url, applyOpenAIShapedPreflight(req, capabilities));\n },\n async generate(req) {\n return generateOpenAIShaped(opts, url, applyOpenAIShapedPreflight(req, capabilities));\n },\n };\n\n return { provider, classification };\n}\n\nfunction applyOpenAIShapedPreflight(\n req: ProviderRequest,\n capabilities: ProviderCapabilities,\n): ProviderRequest {\n const retention = resolveReasoningRetention({\n ...(req.reasoningRetention !== undefined ? { requested: req.reasoningRetention } : {}),\n ...(capabilities.reasoningContract !== undefined\n ? { contract: capabilities.reasoningContract }\n : {}),\n });\n if (retention === 'pass-through-all') return req;\n const filtered = applyReasoningPolicy({ messages: req.messages, retention });\n if (filtered === req.messages) return req;\n return { ...req, messages: filtered };\n}\n\nasync function* streamOpenAIShaped(\n opts: OpenAIShapedOptions,\n url: string,\n req: ProviderRequest,\n): AsyncIterable<ProviderEvent> {\n const body = buildBody(opts.model, req, true, opts.capabilities?.structuredOutput ?? true);\n const resp = await callJsonHttp({\n providerName: opts.providerName,\n url,\n headers: buildHeaders(opts),\n body,\n ...(req.signal !== undefined ? { signal: req.signal } : {}),\n ...(opts.fetchImpl !== undefined ? { fetchImpl: opts.fetchImpl } : {}),\n ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),\n });\n\n yield makeStreamStartEvent({ providerName: opts.providerName, modelId: opts.model });\n\n let finishReason: FinishReason = 'stop';\n let usage: Usage | undefined;\n const toolCallBuffer = new Map<\n number,\n { id: string; name: string; args: string; emittedStart: boolean }\n >();\n\n for await (const payload of parseEventStream(\n resp.body,\n req.signal !== undefined ? { signal: req.signal } : {},\n )) {\n if (req.signal?.aborted) {\n // PS-12: report the abort honestly instead of the default 'stop'.\n finishReason = 'aborted';\n break;\n }\n let parsed: OpenAIChunk;\n try {\n parsed = JSON.parse(payload) as OpenAIChunk;\n } catch (cause) {\n throw new ProviderStreamParseError(\n opts.providerName,\n `failed to parse SSE chunk as JSON: ${(cause as Error).message}`,\n cause,\n );\n }\n const choice = parsed.choices?.[0];\n if (choice === undefined) {\n if (parsed.usage !== undefined) usage = mapOpenAIUsage(parsed.usage);\n continue;\n }\n const delta = choice.delta ?? {};\n if (typeof delta.content === 'string' && delta.content.length > 0) {\n yield { type: 'text-delta', delta: delta.content };\n }\n if (typeof (delta as { reasoning_content?: unknown }).reasoning_content === 'string') {\n const r = (delta as { reasoning_content: string }).reasoning_content;\n if (r.length > 0) yield { type: 'reasoning-delta', delta: r };\n }\n if (Array.isArray(delta.tool_calls)) {\n for (const tc of delta.tool_calls) {\n const idx = tc.index ?? 0;\n const slot = toolCallBuffer.get(idx) ?? {\n id: '',\n name: '',\n args: '',\n emittedStart: false,\n };\n if (typeof tc.id === 'string' && tc.id.length > 0) slot.id = tc.id;\n if (tc.function?.name !== undefined) slot.name = tc.function.name;\n if (typeof tc.function?.arguments === 'string') slot.args += tc.function.arguments;\n toolCallBuffer.set(idx, slot);\n if (!slot.emittedStart && slot.id.length > 0 && slot.name.length > 0) {\n yield {\n type: 'tool-call-start',\n toolCallId: slot.id,\n toolName: slot.name,\n };\n slot.emittedStart = true;\n }\n if (typeof tc.function?.arguments === 'string' && tc.function.arguments.length > 0) {\n yield {\n type: 'tool-call-input-delta',\n toolCallId: slot.id,\n argsDelta: tc.function.arguments,\n };\n }\n }\n }\n if (typeof choice.finish_reason === 'string') {\n finishReason = mapFinishReason(choice.finish_reason);\n for (const slot of toolCallBuffer.values()) {\n const finalArgs = parseFinalArgs(slot.args);\n yield {\n type: 'tool-call-end',\n toolCallId: slot.id,\n finalArgs,\n };\n }\n }\n if (parsed.usage !== undefined) usage = mapOpenAIUsage(parsed.usage);\n }\n\n yield {\n type: 'finish',\n finishReason,\n usage: usage ?? { promptTokens: 0, completionTokens: 0, totalTokens: 0 },\n };\n}\n\nasync function generateOpenAIShaped(\n opts: OpenAIShapedOptions,\n url: string,\n req: ProviderRequest,\n): Promise<ProviderResponse> {\n const body = buildBody(opts.model, req, false, opts.capabilities?.structuredOutput ?? true);\n const resp = await callJsonHttp({\n providerName: opts.providerName,\n url,\n headers: buildHeaders(opts),\n body,\n ...(req.signal !== undefined ? { signal: req.signal } : {}),\n ...(opts.fetchImpl !== undefined ? { fetchImpl: opts.fetchImpl } : {}),\n ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),\n });\n let json: OpenAIChunk;\n try {\n json = (await resp.json()) as OpenAIChunk;\n } catch (cause) {\n throw new ProviderStreamParseError(\n opts.providerName,\n 'response body was not valid JSON',\n cause,\n );\n }\n const choice = json.choices?.[0];\n const message = choice?.message ?? {};\n const text = typeof message.content === 'string' ? message.content : undefined;\n const toolCalls = Array.isArray(message.tool_calls)\n ? message.tool_calls.map((tc) => ({\n toolCallId: tc.id ?? '',\n toolName: tc.function?.name ?? '',\n args: parseFinalArgs(\n typeof tc.function?.arguments === 'string' ? tc.function.arguments : '',\n ),\n }))\n : undefined;\n const usage = mapOpenAIUsage(json.usage) ?? {\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n };\n return {\n usage,\n finishReason: mapFinishReason(choice?.finish_reason),\n ...(text !== undefined ? { text } : {}),\n ...(toolCalls !== undefined ? { toolCalls } : {}),\n };\n}\n\nfunction buildBody(\n model: string,\n req: ProviderRequest,\n stream: boolean,\n structuredOutput: boolean,\n): Record<string, unknown> {\n const messages =\n req.systemMessage !== undefined\n ? [{ role: 'system' as const, content: req.systemMessage }, ...req.messages]\n : req.messages;\n const body: Record<string, unknown> = {\n model,\n messages: toOpenAIChatMessages(messages),\n stream,\n };\n if (stream) {\n // core-provider-09: vanilla OpenAI-shaped servers (vLLM, Together,\n // OpenAI proper) only emit the final usage chunk when asked;\n // without it streamed calls finish with {0,0,0} usage and cost\n // tracking silently zeroes. llama.cpp accepts the flag too.\n // `providerOptions` is merged last, so callers can override it for\n // servers that reject the field.\n body.stream_options = { include_usage: true };\n }\n if (req.temperature !== undefined) body.temperature = req.temperature;\n if (req.maxTokens !== undefined) body.max_tokens = req.maxTokens;\n if (req.tools !== undefined && req.tools.length > 0) {\n // C2: fold worked examples in the adapter itself (idempotent when an\n // upstream createProvider fold already ran).\n body.tools = foldToolExamples(req.tools).map((t) => ({\n type: 'function',\n function: {\n name: t.name,\n ...(t.description !== undefined ? { description: t.description } : {}),\n parameters: t.inputSchema,\n },\n }));\n }\n if (req.toolChoice !== undefined) {\n body.tool_choice = mapToolChoice(req.toolChoice);\n }\n // PS-24: structured output finally reaches the wire - gated on the\n // declared capability so a structuredOutput:false override keeps the\n // request clean for servers that reject response_format.\n if (structuredOutput && req.outputType?.kind === 'structured') {\n body.response_format =\n req.outputType.jsonSchema !== undefined\n ? {\n type: 'json_schema',\n json_schema: { name: 'output', schema: req.outputType.jsonSchema, strict: true },\n }\n : { type: 'json_object' };\n }\n if (req.providerOptions !== undefined) {\n Object.assign(body, req.providerOptions);\n }\n return body;\n}\n\nfunction buildHeaders(opts: OpenAIShapedOptions): Record<string, string> {\n const headers: Record<string, string> = {\n 'content-type': 'application/json',\n accept: 'application/json',\n ...opts.headers,\n };\n if (opts.apiKey !== undefined && opts.apiKey.length > 0) {\n headers.authorization = `Bearer ${opts.apiKey}`;\n }\n return headers;\n}\n\nfunction mapToolChoice(value: unknown): unknown {\n if (value === 'auto' || value === 'none' || value === 'required') return value;\n if (typeof value === 'object' && value !== null && 'tool' in value) {\n return { type: 'function', function: { name: (value as { tool: string }).tool } };\n }\n return 'auto';\n}\n\nfunction mapFinishReason(value: string | null | undefined): FinishReason {\n switch (value) {\n case 'stop':\n case 'length':\n case 'content-filter':\n return value;\n // PS-12: the OpenAI wire format spells it with an underscore; the dashed\n // form alone never matched, so content-filtered completions reported 'stop'.\n case 'content_filter':\n return 'content-filter';\n case 'tool_calls':\n case 'function_call':\n return 'tool-calls';\n case null:\n case undefined:\n return 'stop';\n default:\n return 'stop';\n }\n}\n\nfunction mapOpenAIUsage(input: unknown): Usage | undefined {\n if (input === undefined || input === null || typeof input !== 'object') return undefined;\n const u = input as {\n prompt_tokens?: number;\n completion_tokens?: number;\n total_tokens?: number;\n reasoning_tokens?: number;\n // OpenAI reports cache reads as a SUBSET of prompt_tokens\n // (core-provider-02); there is no cache-write leg on this wire.\n prompt_tokens_details?: { cached_tokens?: number };\n };\n const promptTokens = u.prompt_tokens ?? 0;\n const completionTokens = u.completion_tokens ?? 0;\n const totalTokens = u.total_tokens ?? promptTokens + completionTokens;\n const cachedTokens = u.prompt_tokens_details?.cached_tokens;\n const usage: Usage = {\n promptTokens,\n completionTokens,\n totalTokens,\n ...(u.reasoning_tokens !== undefined ? { reasoningTokens: u.reasoning_tokens } : {}),\n ...(typeof cachedTokens === 'number' && Number.isFinite(cachedTokens) && cachedTokens > 0\n ? { cachedReadTokens: cachedTokens }\n : {}),\n };\n return usage;\n}\n\nfunction parseFinalArgs(raw: string): unknown {\n if (raw.length === 0) return {};\n try {\n return JSON.parse(raw);\n } catch {\n return raw;\n }\n}\n\nfunction sameSensitivity(a: ReadonlyArray<Sensitivity>, b: ReadonlyArray<Sensitivity>): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) return false;\n }\n return true;\n}\n\nfunction defaultLogger(level: 'warn' | 'info', message: string, meta?: object): void {\n const fn = level === 'warn' ? console.warn : console.info;\n if (meta !== undefined) {\n fn(`[graphorin/provider] ${message}`, meta);\n } else {\n fn(`[graphorin/provider] ${message}`);\n }\n}\n\ninterface OpenAIChunk {\n readonly id?: string;\n readonly choices?: ReadonlyArray<{\n readonly delta?: {\n readonly content?: string;\n readonly tool_calls?: ReadonlyArray<{\n readonly index?: number;\n readonly id?: string;\n readonly type?: string;\n readonly function?: { readonly name?: string; readonly arguments?: string };\n }>;\n readonly reasoning_content?: string;\n };\n readonly message?: {\n readonly content?: string | null;\n readonly tool_calls?: ReadonlyArray<{\n readonly id?: string;\n readonly type?: string;\n readonly function?: { readonly name?: string; readonly arguments?: string };\n }>;\n };\n readonly finish_reason?: string | null;\n }>;\n readonly usage?: {\n readonly prompt_tokens?: number;\n readonly completion_tokens?: number;\n readonly total_tokens?: number;\n readonly reasoning_tokens?: number;\n readonly prompt_tokens_details?: { readonly cached_tokens?: number };\n };\n}\n"],"mappings":"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"}
@@ -11,7 +11,7 @@ import { Provider, ProviderMiddleware } from "@graphorin/core";
11
11
  */
12
12
  declare const MIDDLEWARE_KIND: unique symbol;
13
13
  /**
14
- * Canonical middleware ordering outermost → innermost. The table
14
+ * Canonical middleware ordering - outermost → innermost. The table
15
15
  * is enforced by {@link composeProviderMiddleware} and is part of the
16
16
  * provider layer's public contract (DEC-145 / ADR-039).
17
17
  *
@@ -20,8 +20,8 @@ declare const MIDDLEWARE_KIND: unique symbol;
20
20
  * Composition is outermost-first, so a request flows top→bottom and a
21
21
  * response flows bottom→top:
22
22
  *
23
- * - `withTracing` is outermost so the span wraps everything below
24
- * including retries and records true end-to-end latency.
23
+ * - `withTracing` is outermost so the span wraps everything below -
24
+ * including retries - and records true end-to-end latency.
25
25
  * - `withRetry` sits above the rate/cost limiters so each retry
26
26
  * attempt is independently counted and throttled.
27
27
  * - `withRateLimit` → `withCostLimit` → `withCostTracking` form the
@@ -31,7 +31,7 @@ declare const MIDDLEWARE_KIND: unique symbol;
31
31
  * secondary provider still passes through the redactor.
32
32
  * - `withRedaction` is **innermost** (closest to the provider) so it
33
33
  * is the last thing to touch the outbound payload and the first to
34
- * touch the inbound stream guaranteeing every retry, fallback, and
34
+ * touch the inbound stream - guaranteeing every retry, fallback, and
35
35
  * cost-tracked request sees an already-redacted payload and no
36
36
  * secret can bypass it.
37
37
  *
@@ -56,13 +56,13 @@ declare function providerHasMiddleware(provider: Provider, name: string): boolea
56
56
  /**
57
57
  * Wrap an adapter in a middleware chain whose order is validated
58
58
  * against {@link CANONICAL_MIDDLEWARE_ORDER}. The argument array MUST
59
- * be ordered outermost → innermost the same way the layers appear
59
+ * be ordered outermost → innermost - the same way the layers appear
60
60
  * in the documented composition example. The composer validates that
61
61
  * every kind known to the canonical order is monotonically non-
62
62
  * decreasing in index, throws otherwise.
63
63
  *
64
64
  * Custom middleware whose kind is NOT in the canonical order is
65
- * silently allowed at any position operators registering bespoke
65
+ * silently allowed at any position - operators registering bespoke
66
66
  * layers via {@link defineProviderMiddleware} carry the
67
67
  * responsibility of placing them sensibly.
68
68
  *
@@ -10,7 +10,7 @@ import { MiddlewareOrderingError } from "../errors/errors.js";
10
10
  */
11
11
  const MIDDLEWARE_KIND = Symbol.for("graphorin.provider.middleware.kind");
12
12
  /**
13
- * Canonical middleware ordering outermost → innermost. The table
13
+ * Canonical middleware ordering - outermost → innermost. The table
14
14
  * is enforced by {@link composeProviderMiddleware} and is part of the
15
15
  * provider layer's public contract (DEC-145 / ADR-039).
16
16
  *
@@ -19,8 +19,8 @@ const MIDDLEWARE_KIND = Symbol.for("graphorin.provider.middleware.kind");
19
19
  * Composition is outermost-first, so a request flows top→bottom and a
20
20
  * response flows bottom→top:
21
21
  *
22
- * - `withTracing` is outermost so the span wraps everything below
23
- * including retries and records true end-to-end latency.
22
+ * - `withTracing` is outermost so the span wraps everything below -
23
+ * including retries - and records true end-to-end latency.
24
24
  * - `withRetry` sits above the rate/cost limiters so each retry
25
25
  * attempt is independently counted and throttled.
26
26
  * - `withRateLimit` → `withCostLimit` → `withCostTracking` form the
@@ -30,7 +30,7 @@ const MIDDLEWARE_KIND = Symbol.for("graphorin.provider.middleware.kind");
30
30
  * secondary provider still passes through the redactor.
31
31
  * - `withRedaction` is **innermost** (closest to the provider) so it
32
32
  * is the last thing to touch the outbound payload and the first to
33
- * touch the inbound stream guaranteeing every retry, fallback, and
33
+ * touch the inbound stream - guaranteeing every retry, fallback, and
34
34
  * cost-tracked request sees an already-redacted payload and no
35
35
  * secret can bypass it.
36
36
  *
@@ -50,7 +50,7 @@ const CANONICAL_INDEX = Object.freeze(CANONICAL_MIDDLEWARE_ORDER.reduce((acc, na
50
50
  return acc;
51
51
  }, {}));
52
52
  /**
53
- * Symbol used to walk the chain every wrapper exposes the inner
53
+ * Symbol used to walk the chain - every wrapper exposes the inner
54
54
  * provider so the composer can introspect the full stack at startup.
55
55
  *
56
56
  * @stable
@@ -94,13 +94,13 @@ function providerHasMiddleware(provider, name) {
94
94
  /**
95
95
  * Wrap an adapter in a middleware chain whose order is validated
96
96
  * against {@link CANONICAL_MIDDLEWARE_ORDER}. The argument array MUST
97
- * be ordered outermost → innermost the same way the layers appear
97
+ * be ordered outermost → innermost - the same way the layers appear
98
98
  * in the documented composition example. The composer validates that
99
99
  * every kind known to the canonical order is monotonically non-
100
100
  * decreasing in index, throws otherwise.
101
101
  *
102
102
  * Custom middleware whose kind is NOT in the canonical order is
103
- * silently allowed at any position operators registering bespoke
103
+ * silently allowed at any position - operators registering bespoke
104
104
  * layers via {@link defineProviderMiddleware} carry the
105
105
  * responsibility of placing them sensibly.
106
106
  *
@@ -1 +1 @@
1
- {"version":3,"file":"compose.js","names":["MIDDLEWARE_KIND: unique symbol","CANONICAL_MIDDLEWARE_ORDER: readonly string[]","CANONICAL_INDEX: Readonly<Record<string, number>>","INNER_PROVIDER: unique symbol","kinds: string[]","cur: KindedProvider | undefined","chain: Provider","recognised: { kind: string; index: number }[]","branded: Provider"],"sources":["../../src/middleware/compose.ts"],"sourcesContent":["/**\n * Canonical-order middleware composer. The composer enforces the\n * documented ordering at startup and throws\n * {@link MiddlewareOrderingError} on violation. Manual\n * `withRetry(withRedaction(p))` style still works but emits a one-time\n * WARN unless the consumer flips `strict: true`, in which case the\n * composer also throws on manual nesting that violates the canonical\n * order.\n *\n * @packageDocumentation\n */\n\nimport type { Provider, ProviderMiddleware } from '@graphorin/core';\n\nimport { MiddlewareOrderingError } from '../errors/errors.js';\n\n/**\n * Symbol attached to every middleware-produced provider so the\n * composer can detect and validate the chain. The symbol is opaque\n * and cross-realm safe via `Symbol.for`.\n *\n * @stable\n */\nexport const MIDDLEWARE_KIND: unique symbol = Symbol.for('graphorin.provider.middleware.kind');\n\n/**\n * Canonical middleware ordering outermost → innermost. The table\n * is enforced by {@link composeProviderMiddleware} and is part of the\n * provider layer's public contract (DEC-145 / ADR-039).\n *\n * ## Why this order\n *\n * Composition is outermost-first, so a request flows top→bottom and a\n * response flows bottom→top:\n *\n * - `withTracing` is outermost so the span wraps everything below —\n * including retries and records true end-to-end latency.\n * - `withRetry` sits above the rate/cost limiters so each retry\n * attempt is independently counted and throttled.\n * - `withRateLimit` → `withCostLimit` → `withCostTracking` form the\n * budget stack: throttle before admitting, reject over-budget calls\n * before spending, then meter what actually went through.\n * - `withFallback` is just above redaction so a fallback to a\n * secondary provider still passes through the redactor.\n * - `withRedaction` is **innermost** (closest to the provider) so it\n * is the last thing to touch the outbound payload and the first to\n * touch the inbound stream guaranteeing every retry, fallback, and\n * cost-tracked request sees an already-redacted payload and no\n * secret can bypass it.\n *\n * @stable\n */\nexport const CANONICAL_MIDDLEWARE_ORDER: readonly string[] = [\n 'withTracing',\n 'withRetry',\n 'withRateLimit',\n 'withCostLimit',\n 'withCostTracking',\n 'withFallback',\n 'withRedaction',\n];\n\nconst CANONICAL_INDEX: Readonly<Record<string, number>> = Object.freeze(\n CANONICAL_MIDDLEWARE_ORDER.reduce<Record<string, number>>((acc, name, idx) => {\n acc[name] = idx;\n return acc;\n }, {}),\n);\n\ninterface KindedProvider extends Provider {\n readonly [MIDDLEWARE_KIND]?: string;\n readonly [INNER_PROVIDER]?: Provider;\n}\n\n/**\n * Symbol used to walk the chain every wrapper exposes the inner\n * provider so the composer can introspect the full stack at startup.\n *\n * @stable\n */\nexport const INNER_PROVIDER: unique symbol = Symbol.for('graphorin.provider.middleware.inner');\n\n/**\n * Read the discriminant kind attached to a middleware-produced\n * provider. Returns `undefined` if the provider is the bare adapter\n * or a custom wrapper that does not declare a kind.\n *\n * @stable\n */\nexport function getMiddlewareKind(provider: Provider): string | undefined {\n return (provider as KindedProvider)[MIDDLEWARE_KIND];\n}\n\n/**\n * Walk the middleware chain inside `provider` and return the array of\n * declared kinds (outer → inner).\n *\n * @stable\n */\nexport function listMiddlewareKinds(provider: Provider): readonly string[] {\n const kinds: string[] = [];\n let cur: KindedProvider | undefined = provider as KindedProvider;\n while (cur !== undefined) {\n const kind = cur[MIDDLEWARE_KIND];\n if (kind !== undefined) kinds.push(kind);\n cur = cur[INNER_PROVIDER] as KindedProvider | undefined;\n }\n return kinds;\n}\n\n/**\n * Return `true` iff the chain rooted at `provider` contains a\n * middleware whose kind matches `name`.\n *\n * @stable\n */\nexport function providerHasMiddleware(provider: Provider, name: string): boolean {\n return listMiddlewareKinds(provider).includes(name);\n}\n\n/**\n * Wrap an adapter in a middleware chain whose order is validated\n * against {@link CANONICAL_MIDDLEWARE_ORDER}. The argument array MUST\n * be ordered outermost → innermost the same way the layers appear\n * in the documented composition example. The composer validates that\n * every kind known to the canonical order is monotonically non-\n * decreasing in index, throws otherwise.\n *\n * Custom middleware whose kind is NOT in the canonical order is\n * silently allowed at any position operators registering bespoke\n * layers via {@link defineProviderMiddleware} carry the\n * responsibility of placing them sensibly.\n *\n * @stable\n */\nexport function composeProviderMiddleware(\n middlewares: ReadonlyArray<ProviderMiddleware>,\n): ProviderMiddleware {\n return (next: Provider): Provider => {\n let chain: Provider = next;\n // Apply innermost → outermost so the composed call order matches\n // the array's outer → inner declaration.\n for (let i = middlewares.length - 1; i >= 0; i--) {\n const factory = middlewares[i];\n if (typeof factory !== 'function') {\n throw new TypeError(\n `composeProviderMiddleware: entry at index ${i} is not a ProviderMiddleware function.`,\n );\n }\n chain = factory(chain);\n }\n validateMiddlewareOrder(chain);\n return chain;\n };\n}\n\n/**\n * Validate the kinds present in `provider` against the canonical\n * order. Exposed for tests; the composer calls it on every chain.\n *\n * @stable\n */\nexport function validateMiddlewareOrder(provider: Provider): void {\n const kinds = listMiddlewareKinds(provider);\n // Filter to the kinds we recognise.\n const recognised: { kind: string; index: number }[] = [];\n for (const kind of kinds) {\n const idx = CANONICAL_INDEX[kind];\n if (idx !== undefined) recognised.push({ kind, index: idx });\n }\n // The recognised list runs outer → inner; canonical indices must\n // increase monotonically. The first decreasing pair is the\n // violation.\n for (let i = 1; i < recognised.length; i++) {\n const prev = recognised[i - 1];\n const cur = recognised[i];\n if (prev !== undefined && cur !== undefined && cur.index < prev.index) {\n throw new MiddlewareOrderingError({\n offendingPair: [prev.kind, cur.kind],\n canonicalOrder: CANONICAL_MIDDLEWARE_ORDER,\n });\n }\n }\n}\n\n/**\n * Decorator factory used internally by every built-in middleware. The\n * returned function attaches the canonical kind discriminator and the\n * inner-provider symbol so the composer can introspect chains.\n *\n * @stable\n */\nexport function defineProviderMiddleware<T>(args: {\n readonly kind: string;\n readonly factory: (opts: T) => ProviderMiddleware;\n}): (opts: T) => ProviderMiddleware {\n return (opts: T): ProviderMiddleware => {\n const built = args.factory(opts);\n return (next: Provider): Provider => {\n const wrapped = built(next);\n const branded: Provider = Object.create(wrapped) as Provider;\n Object.defineProperty(branded, MIDDLEWARE_KIND, {\n value: args.kind,\n enumerable: false,\n });\n Object.defineProperty(branded, INNER_PROVIDER, {\n value: next,\n enumerable: false,\n });\n return branded;\n };\n };\n}\n"],"mappings":";;;;;;;;;;AAuBA,MAAaA,kBAAiC,OAAO,IAAI,qCAAqC;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6B9F,MAAaC,6BAAgD;CAC3D;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AAED,MAAMC,kBAAoD,OAAO,OAC/D,2BAA2B,QAAgC,KAAK,MAAM,QAAQ;AAC5E,KAAI,QAAQ;AACZ,QAAO;GACN,EAAE,CAAC,CACP;;;;;;;AAaD,MAAaC,iBAAgC,OAAO,IAAI,sCAAsC;;;;;;;;AAS9F,SAAgB,kBAAkB,UAAwC;AACxE,QAAQ,SAA4B;;;;;;;;AAStC,SAAgB,oBAAoB,UAAuC;CACzE,MAAMC,QAAkB,EAAE;CAC1B,IAAIC,MAAkC;AACtC,QAAO,QAAQ,QAAW;EACxB,MAAM,OAAO,IAAI;AACjB,MAAI,SAAS,OAAW,OAAM,KAAK,KAAK;AACxC,QAAM,IAAI;;AAEZ,QAAO;;;;;;;;AAST,SAAgB,sBAAsB,UAAoB,MAAuB;AAC/E,QAAO,oBAAoB,SAAS,CAAC,SAAS,KAAK;;;;;;;;;;;;;;;;;AAkBrD,SAAgB,0BACd,aACoB;AACpB,SAAQ,SAA6B;EACnC,IAAIC,QAAkB;AAGtB,OAAK,IAAI,IAAI,YAAY,SAAS,GAAG,KAAK,GAAG,KAAK;GAChD,MAAM,UAAU,YAAY;AAC5B,OAAI,OAAO,YAAY,WACrB,OAAM,IAAI,UACR,6CAA6C,EAAE,wCAChD;AAEH,WAAQ,QAAQ,MAAM;;AAExB,0BAAwB,MAAM;AAC9B,SAAO;;;;;;;;;AAUX,SAAgB,wBAAwB,UAA0B;CAChE,MAAM,QAAQ,oBAAoB,SAAS;CAE3C,MAAMC,aAAgD,EAAE;AACxD,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,MAAM,gBAAgB;AAC5B,MAAI,QAAQ,OAAW,YAAW,KAAK;GAAE;GAAM,OAAO;GAAK,CAAC;;AAK9D,MAAK,IAAI,IAAI,GAAG,IAAI,WAAW,QAAQ,KAAK;EAC1C,MAAM,OAAO,WAAW,IAAI;EAC5B,MAAM,MAAM,WAAW;AACvB,MAAI,SAAS,UAAa,QAAQ,UAAa,IAAI,QAAQ,KAAK,MAC9D,OAAM,IAAI,wBAAwB;GAChC,eAAe,CAAC,KAAK,MAAM,IAAI,KAAK;GACpC,gBAAgB;GACjB,CAAC;;;;;;;;;;AAYR,SAAgB,yBAA4B,MAGR;AAClC,SAAQ,SAAgC;EACtC,MAAM,QAAQ,KAAK,QAAQ,KAAK;AAChC,UAAQ,SAA6B;GACnC,MAAM,UAAU,MAAM,KAAK;GAC3B,MAAMC,UAAoB,OAAO,OAAO,QAAQ;AAChD,UAAO,eAAe,SAAS,iBAAiB;IAC9C,OAAO,KAAK;IACZ,YAAY;IACb,CAAC;AACF,UAAO,eAAe,SAAS,gBAAgB;IAC7C,OAAO;IACP,YAAY;IACb,CAAC;AACF,UAAO"}
1
+ {"version":3,"file":"compose.js","names":["MIDDLEWARE_KIND: unique symbol","CANONICAL_MIDDLEWARE_ORDER: readonly string[]","CANONICAL_INDEX: Readonly<Record<string, number>>","INNER_PROVIDER: unique symbol","kinds: string[]","cur: KindedProvider | undefined","chain: Provider","recognised: { kind: string; index: number }[]","branded: Provider"],"sources":["../../src/middleware/compose.ts"],"sourcesContent":["/**\n * Canonical-order middleware composer. The composer enforces the\n * documented ordering at startup and throws\n * {@link MiddlewareOrderingError} on violation. Manual\n * `withRetry(withRedaction(p))` style still works but emits a one-time\n * WARN unless the consumer flips `strict: true`, in which case the\n * composer also throws on manual nesting that violates the canonical\n * order.\n *\n * @packageDocumentation\n */\n\nimport type { Provider, ProviderMiddleware } from '@graphorin/core';\n\nimport { MiddlewareOrderingError } from '../errors/errors.js';\n\n/**\n * Symbol attached to every middleware-produced provider so the\n * composer can detect and validate the chain. The symbol is opaque\n * and cross-realm safe via `Symbol.for`.\n *\n * @stable\n */\nexport const MIDDLEWARE_KIND: unique symbol = Symbol.for('graphorin.provider.middleware.kind');\n\n/**\n * Canonical middleware ordering - outermost → innermost. The table\n * is enforced by {@link composeProviderMiddleware} and is part of the\n * provider layer's public contract (DEC-145 / ADR-039).\n *\n * ## Why this order\n *\n * Composition is outermost-first, so a request flows top→bottom and a\n * response flows bottom→top:\n *\n * - `withTracing` is outermost so the span wraps everything below -\n * including retries - and records true end-to-end latency.\n * - `withRetry` sits above the rate/cost limiters so each retry\n * attempt is independently counted and throttled.\n * - `withRateLimit` → `withCostLimit` → `withCostTracking` form the\n * budget stack: throttle before admitting, reject over-budget calls\n * before spending, then meter what actually went through.\n * - `withFallback` is just above redaction so a fallback to a\n * secondary provider still passes through the redactor.\n * - `withRedaction` is **innermost** (closest to the provider) so it\n * is the last thing to touch the outbound payload and the first to\n * touch the inbound stream - guaranteeing every retry, fallback, and\n * cost-tracked request sees an already-redacted payload and no\n * secret can bypass it.\n *\n * @stable\n */\nexport const CANONICAL_MIDDLEWARE_ORDER: readonly string[] = [\n 'withTracing',\n 'withRetry',\n 'withRateLimit',\n 'withCostLimit',\n 'withCostTracking',\n 'withFallback',\n 'withRedaction',\n];\n\nconst CANONICAL_INDEX: Readonly<Record<string, number>> = Object.freeze(\n CANONICAL_MIDDLEWARE_ORDER.reduce<Record<string, number>>((acc, name, idx) => {\n acc[name] = idx;\n return acc;\n }, {}),\n);\n\ninterface KindedProvider extends Provider {\n readonly [MIDDLEWARE_KIND]?: string;\n readonly [INNER_PROVIDER]?: Provider;\n}\n\n/**\n * Symbol used to walk the chain - every wrapper exposes the inner\n * provider so the composer can introspect the full stack at startup.\n *\n * @stable\n */\nexport const INNER_PROVIDER: unique symbol = Symbol.for('graphorin.provider.middleware.inner');\n\n/**\n * Read the discriminant kind attached to a middleware-produced\n * provider. Returns `undefined` if the provider is the bare adapter\n * or a custom wrapper that does not declare a kind.\n *\n * @stable\n */\nexport function getMiddlewareKind(provider: Provider): string | undefined {\n return (provider as KindedProvider)[MIDDLEWARE_KIND];\n}\n\n/**\n * Walk the middleware chain inside `provider` and return the array of\n * declared kinds (outer → inner).\n *\n * @stable\n */\nexport function listMiddlewareKinds(provider: Provider): readonly string[] {\n const kinds: string[] = [];\n let cur: KindedProvider | undefined = provider as KindedProvider;\n while (cur !== undefined) {\n const kind = cur[MIDDLEWARE_KIND];\n if (kind !== undefined) kinds.push(kind);\n cur = cur[INNER_PROVIDER] as KindedProvider | undefined;\n }\n return kinds;\n}\n\n/**\n * Return `true` iff the chain rooted at `provider` contains a\n * middleware whose kind matches `name`.\n *\n * @stable\n */\nexport function providerHasMiddleware(provider: Provider, name: string): boolean {\n return listMiddlewareKinds(provider).includes(name);\n}\n\n/**\n * Wrap an adapter in a middleware chain whose order is validated\n * against {@link CANONICAL_MIDDLEWARE_ORDER}. The argument array MUST\n * be ordered outermost → innermost - the same way the layers appear\n * in the documented composition example. The composer validates that\n * every kind known to the canonical order is monotonically non-\n * decreasing in index, throws otherwise.\n *\n * Custom middleware whose kind is NOT in the canonical order is\n * silently allowed at any position - operators registering bespoke\n * layers via {@link defineProviderMiddleware} carry the\n * responsibility of placing them sensibly.\n *\n * @stable\n */\nexport function composeProviderMiddleware(\n middlewares: ReadonlyArray<ProviderMiddleware>,\n): ProviderMiddleware {\n return (next: Provider): Provider => {\n let chain: Provider = next;\n // Apply innermost → outermost so the composed call order matches\n // the array's outer → inner declaration.\n for (let i = middlewares.length - 1; i >= 0; i--) {\n const factory = middlewares[i];\n if (typeof factory !== 'function') {\n throw new TypeError(\n `composeProviderMiddleware: entry at index ${i} is not a ProviderMiddleware function.`,\n );\n }\n chain = factory(chain);\n }\n validateMiddlewareOrder(chain);\n return chain;\n };\n}\n\n/**\n * Validate the kinds present in `provider` against the canonical\n * order. Exposed for tests; the composer calls it on every chain.\n *\n * @stable\n */\nexport function validateMiddlewareOrder(provider: Provider): void {\n const kinds = listMiddlewareKinds(provider);\n // Filter to the kinds we recognise.\n const recognised: { kind: string; index: number }[] = [];\n for (const kind of kinds) {\n const idx = CANONICAL_INDEX[kind];\n if (idx !== undefined) recognised.push({ kind, index: idx });\n }\n // The recognised list runs outer → inner; canonical indices must\n // increase monotonically. The first decreasing pair is the\n // violation.\n for (let i = 1; i < recognised.length; i++) {\n const prev = recognised[i - 1];\n const cur = recognised[i];\n if (prev !== undefined && cur !== undefined && cur.index < prev.index) {\n throw new MiddlewareOrderingError({\n offendingPair: [prev.kind, cur.kind],\n canonicalOrder: CANONICAL_MIDDLEWARE_ORDER,\n });\n }\n }\n}\n\n/**\n * Decorator factory used internally by every built-in middleware. The\n * returned function attaches the canonical kind discriminator and the\n * inner-provider symbol so the composer can introspect chains.\n *\n * @stable\n */\nexport function defineProviderMiddleware<T>(args: {\n readonly kind: string;\n readonly factory: (opts: T) => ProviderMiddleware;\n}): (opts: T) => ProviderMiddleware {\n return (opts: T): ProviderMiddleware => {\n const built = args.factory(opts);\n return (next: Provider): Provider => {\n const wrapped = built(next);\n const branded: Provider = Object.create(wrapped) as Provider;\n Object.defineProperty(branded, MIDDLEWARE_KIND, {\n value: args.kind,\n enumerable: false,\n });\n Object.defineProperty(branded, INNER_PROVIDER, {\n value: next,\n enumerable: false,\n });\n return branded;\n };\n };\n}\n"],"mappings":";;;;;;;;;;AAuBA,MAAaA,kBAAiC,OAAO,IAAI,qCAAqC;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6B9F,MAAaC,6BAAgD;CAC3D;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AAED,MAAMC,kBAAoD,OAAO,OAC/D,2BAA2B,QAAgC,KAAK,MAAM,QAAQ;AAC5E,KAAI,QAAQ;AACZ,QAAO;GACN,EAAE,CAAC,CACP;;;;;;;AAaD,MAAaC,iBAAgC,OAAO,IAAI,sCAAsC;;;;;;;;AAS9F,SAAgB,kBAAkB,UAAwC;AACxE,QAAQ,SAA4B;;;;;;;;AAStC,SAAgB,oBAAoB,UAAuC;CACzE,MAAMC,QAAkB,EAAE;CAC1B,IAAIC,MAAkC;AACtC,QAAO,QAAQ,QAAW;EACxB,MAAM,OAAO,IAAI;AACjB,MAAI,SAAS,OAAW,OAAM,KAAK,KAAK;AACxC,QAAM,IAAI;;AAEZ,QAAO;;;;;;;;AAST,SAAgB,sBAAsB,UAAoB,MAAuB;AAC/E,QAAO,oBAAoB,SAAS,CAAC,SAAS,KAAK;;;;;;;;;;;;;;;;;AAkBrD,SAAgB,0BACd,aACoB;AACpB,SAAQ,SAA6B;EACnC,IAAIC,QAAkB;AAGtB,OAAK,IAAI,IAAI,YAAY,SAAS,GAAG,KAAK,GAAG,KAAK;GAChD,MAAM,UAAU,YAAY;AAC5B,OAAI,OAAO,YAAY,WACrB,OAAM,IAAI,UACR,6CAA6C,EAAE,wCAChD;AAEH,WAAQ,QAAQ,MAAM;;AAExB,0BAAwB,MAAM;AAC9B,SAAO;;;;;;;;;AAUX,SAAgB,wBAAwB,UAA0B;CAChE,MAAM,QAAQ,oBAAoB,SAAS;CAE3C,MAAMC,aAAgD,EAAE;AACxD,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,MAAM,gBAAgB;AAC5B,MAAI,QAAQ,OAAW,YAAW,KAAK;GAAE;GAAM,OAAO;GAAK,CAAC;;AAK9D,MAAK,IAAI,IAAI,GAAG,IAAI,WAAW,QAAQ,KAAK;EAC1C,MAAM,OAAO,WAAW,IAAI;EAC5B,MAAM,MAAM,WAAW;AACvB,MAAI,SAAS,UAAa,QAAQ,UAAa,IAAI,QAAQ,KAAK,MAC9D,OAAM,IAAI,wBAAwB;GAChC,eAAe,CAAC,KAAK,MAAM,IAAI,KAAK;GACpC,gBAAgB;GACjB,CAAC;;;;;;;;;;AAYR,SAAgB,yBAA4B,MAGR;AAClC,SAAQ,SAAgC;EACtC,MAAM,QAAQ,KAAK,QAAQ,KAAK;AAChC,UAAQ,SAA6B;GACnC,MAAM,UAAU,MAAM,KAAK;GAC3B,MAAMC,UAAoB,OAAO,OAAO,QAAQ;AAChD,UAAO,eAAe,SAAS,iBAAiB;IAC9C,OAAO,KAAK;IACZ,YAAY;IACb,CAAC;AACF,UAAO,eAAe,SAAS,gBAAgB;IAC7C,OAAO;IACP,YAAY;IACb,CAAC;AACF,UAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"production-hook.js","names":[],"sources":["../../src/middleware/production-hook.ts"],"sourcesContent":["/**\n * Production startup hook fails fast when a security-critical\n * middleware is missing from the composed chain. The default\n * configuration enforces `withRedaction`; consumers can extend the\n * `requiredKinds` list to lock additional middlewares.\n *\n * @packageDocumentation\n */\n\nimport type { Provider } from '@graphorin/core';\n\nimport { MissingProductionMiddlewareError } from '../errors/errors.js';\nimport { providerHasMiddleware } from './compose.js';\n\n/**\n * Options for {@link assertProductionMiddleware}.\n *\n * @stable\n */\nexport interface ProductionStartupHookOptions {\n /** Middleware kinds that must be present. Defaults to `['withRedaction']`. */\n readonly requiredKinds?: ReadonlyArray<string>;\n /** Force the check regardless of `NODE_ENV`. */\n readonly force?: boolean;\n}\n\n/**\n * Throw {@link MissingProductionMiddlewareError} if a required\n * middleware is missing from the chain rooted at `provider`. The\n * check runs only when `NODE_ENV === 'production'` unless `force` is\n * `true`.\n *\n * @stable\n */\nexport function assertProductionMiddleware(\n provider: Provider,\n options: ProductionStartupHookOptions = {},\n): void {\n const isProd = process.env.NODE_ENV === 'production';\n if (!isProd && options.force !== true) return;\n const required = options.requiredKinds ?? ['withRedaction'];\n for (const kind of required) {\n if (!providerHasMiddleware(provider, kind)) {\n throw new MissingProductionMiddlewareError(kind);\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;AAkCA,SAAgB,2BACd,UACA,UAAwC,EAAE,EACpC;AAEN,KAAI,EADW,QAAQ,IAAI,aAAa,iBACzB,QAAQ,UAAU,KAAM;CACvC,MAAM,WAAW,QAAQ,iBAAiB,CAAC,gBAAgB;AAC3D,MAAK,MAAM,QAAQ,SACjB,KAAI,CAAC,sBAAsB,UAAU,KAAK,CACxC,OAAM,IAAI,iCAAiC,KAAK"}
1
+ {"version":3,"file":"production-hook.js","names":[],"sources":["../../src/middleware/production-hook.ts"],"sourcesContent":["/**\n * Production startup hook - fails fast when a security-critical\n * middleware is missing from the composed chain. The default\n * configuration enforces `withRedaction`; consumers can extend the\n * `requiredKinds` list to lock additional middlewares.\n *\n * @packageDocumentation\n */\n\nimport type { Provider } from '@graphorin/core';\n\nimport { MissingProductionMiddlewareError } from '../errors/errors.js';\nimport { providerHasMiddleware } from './compose.js';\n\n/**\n * Options for {@link assertProductionMiddleware}.\n *\n * @stable\n */\nexport interface ProductionStartupHookOptions {\n /** Middleware kinds that must be present. Defaults to `['withRedaction']`. */\n readonly requiredKinds?: ReadonlyArray<string>;\n /** Force the check regardless of `NODE_ENV`. */\n readonly force?: boolean;\n}\n\n/**\n * Throw {@link MissingProductionMiddlewareError} if a required\n * middleware is missing from the chain rooted at `provider`. The\n * check runs only when `NODE_ENV === 'production'` unless `force` is\n * `true`.\n *\n * @stable\n */\nexport function assertProductionMiddleware(\n provider: Provider,\n options: ProductionStartupHookOptions = {},\n): void {\n const isProd = process.env.NODE_ENV === 'production';\n if (!isProd && options.force !== true) return;\n const required = options.requiredKinds ?? ['withRedaction'];\n for (const kind of required) {\n if (!providerHasMiddleware(provider, kind)) {\n throw new MissingProductionMiddlewareError(kind);\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;AAkCA,SAAgB,2BACd,UACA,UAAwC,EAAE,EACpC;AAEN,KAAI,EADW,QAAQ,IAAI,aAAa,iBACzB,QAAQ,UAAU,KAAM;CACvC,MAAM,WAAW,QAAQ,iBAAiB,CAAC,gBAAgB;AAC3D,MAAK,MAAM,QAAQ,SACjB,KAAI,CAAC,sBAAsB,UAAU,KAAK,CACxC,OAAM,IAAI,iCAAiC,KAAK"}
@@ -3,7 +3,7 @@ import * as _graphorin_core1 from "@graphorin/core";
3
3
  //#region src/middleware/with-cost-limit.d.ts
4
4
 
5
5
  /**
6
- * `withCostLimit` enforce per-session / per-run / per-agent /
6
+ * `withCostLimit` - enforce per-session / per-run / per-agent /
7
7
  * per-hour cost ceilings. Couples with {@link withCostTracking} for
8
8
  * the underlying accumulator. The middleware is positioned between
9
9
  * `withRateLimit` and `withCostTracking` per the canonical order so a
@@ -1 +1 @@
1
- {"version":3,"file":"with-cost-limit.js","names":["checks: Array<['session' | 'run' | 'hour', number | undefined]>"],"sources":["../../src/middleware/with-cost-limit.ts"],"sourcesContent":["/**\n * `withCostLimit` enforce per-session / per-run / per-agent /\n * per-hour cost ceilings. Couples with {@link withCostTracking} for\n * the underlying accumulator. The middleware is positioned between\n * `withRateLimit` and `withCostTracking` per the canonical order so a\n * budget breach trips before the request hits the underlying\n * provider.\n *\n * @packageDocumentation\n */\n\nimport type { Provider } from '@graphorin/core';\n\nimport { CostBudgetExceededError } from '../errors/errors.js';\nimport { defineProviderMiddleware } from './compose.js';\n\n/**\n * Options for {@link withCostLimit}.\n *\n * @stable\n */\nexport interface WithCostLimitOptions {\n /** Maximum cumulative USD cost per session. */\n readonly maxPerSession?: number;\n /** Maximum cumulative USD cost per run. */\n readonly maxPerRun?: number;\n /** Maximum cumulative USD cost per hour. */\n readonly maxPerHour?: number;\n /** What to do on breach. Default `'throw'`. */\n readonly onExceed?: 'throw' | 'warn';\n /**\n * Resolver returning the current observed cost for the relevant\n * scope. The resolver lets consumers wire any accumulator (the\n * shipped `@graphorin/observability/cost.CostTracker` works out of\n * the box). When unset, the middleware is a no-op (a placeholder\n * for tooling that wires the accumulator later).\n */\n readonly resolveObservedCost?: (\n scope: 'session' | 'run' | 'hour',\n metadata: Readonly<Record<string, unknown>> | undefined,\n ) => number;\n /** Optional sink for `'warn'` mode. Defaults to `console.warn`. */\n readonly logger?: (message: string, meta?: object) => void;\n}\n\n/**\n * @stable\n */\nexport const withCostLimit = defineProviderMiddleware<WithCostLimitOptions>({\n kind: 'withCostLimit',\n factory: (opts: WithCostLimitOptions) => {\n const onExceed = opts.onExceed ?? 'throw';\n const resolver = opts.resolveObservedCost;\n const logger = opts.logger ?? defaultLogger;\n // PS-8: a ceiling without a resolver is a silent no-op the limits look\n // enforced but never trip. Warn loudly so the gap is visible. (No ceiling\n // + no resolver is the documented inert placeholder, so it stays quiet.)\n const hasCeiling =\n opts.maxPerSession !== undefined ||\n opts.maxPerRun !== undefined ||\n opts.maxPerHour !== undefined;\n if (hasCeiling && resolver === undefined) {\n logger(\n '[graphorin/provider] withCostLimit: a cost ceiling is configured but no ' +\n '`resolveObservedCost` was supplied, so the limit is UNENFORCED (no-op). ' +\n 'Wire a resolver (e.g. from createCostAccumulator / @graphorin/observability).',\n );\n }\n return (next: Provider): Provider => ({\n name: next.name,\n modelId: next.modelId,\n capabilities: next.capabilities,\n ...(next.acceptsSensitivity !== undefined\n ? { acceptsSensitivity: next.acceptsSensitivity }\n : {}),\n stream(req) {\n if (resolver !== undefined) {\n check(asRecord(req.metadata), resolver, opts, onExceed, logger);\n }\n return next.stream(req);\n },\n async generate(req) {\n if (resolver !== undefined) {\n check(asRecord(req.metadata), resolver, opts, onExceed, logger);\n }\n return next.generate(req);\n },\n ...(next.countTokens ? { countTokens: next.countTokens.bind(next) } : {}),\n });\n },\n});\n\nfunction check(\n metadata: Readonly<Record<string, unknown>> | undefined,\n resolver: (\n scope: 'session' | 'run' | 'hour',\n metadata?: Readonly<Record<string, unknown>>,\n ) => number,\n opts: WithCostLimitOptions,\n onExceed: 'throw' | 'warn',\n logger: (message: string, meta?: object) => void,\n): void {\n const checks: Array<['session' | 'run' | 'hour', number | undefined]> = [\n ['session', opts.maxPerSession],\n ['run', opts.maxPerRun],\n ['hour', opts.maxPerHour],\n ];\n for (const [scope, limit] of checks) {\n if (limit === undefined) continue;\n const observed = resolver(scope, metadata);\n if (observed > limit) {\n if (onExceed === 'throw') {\n throw new CostBudgetExceededError({ scope, limit, observed });\n }\n logger(\n `[graphorin/provider] withCostLimit: ${scope} budget breach (${observed.toFixed(4)} > ${limit.toFixed(4)} USD).`,\n { scope, limit, observed },\n );\n }\n }\n}\n\nfunction defaultLogger(message: string, meta?: object): void {\n if (meta !== undefined) console.warn(message, meta);\n else console.warn(message);\n}\n\nfunction asRecord(value: unknown): Readonly<Record<string, unknown>> | undefined {\n if (value === undefined || value === null) return undefined;\n return value as Readonly<Record<string, unknown>>;\n}\n"],"mappings":";;;;;;;AAgDA,MAAa,gBAAgB,yBAA+C;CAC1E,MAAM;CACN,UAAU,SAA+B;EACvC,MAAM,WAAW,KAAK,YAAY;EAClC,MAAM,WAAW,KAAK;EACtB,MAAM,SAAS,KAAK,UAAU;AAQ9B,OAHE,KAAK,kBAAkB,UACvB,KAAK,cAAc,UACnB,KAAK,eAAe,WACJ,aAAa,OAC7B,QACE,gOAGD;AAEH,UAAQ,UAA8B;GACpC,MAAM,KAAK;GACX,SAAS,KAAK;GACd,cAAc,KAAK;GACnB,GAAI,KAAK,uBAAuB,SAC5B,EAAE,oBAAoB,KAAK,oBAAoB,GAC/C,EAAE;GACN,OAAO,KAAK;AACV,QAAI,aAAa,OACf,OAAM,SAAS,IAAI,SAAS,EAAE,UAAU,MAAM,UAAU,OAAO;AAEjE,WAAO,KAAK,OAAO,IAAI;;GAEzB,MAAM,SAAS,KAAK;AAClB,QAAI,aAAa,OACf,OAAM,SAAS,IAAI,SAAS,EAAE,UAAU,MAAM,UAAU,OAAO;AAEjE,WAAO,KAAK,SAAS,IAAI;;GAE3B,GAAI,KAAK,cAAc,EAAE,aAAa,KAAK,YAAY,KAAK,KAAK,EAAE,GAAG,EAAE;GACzE;;CAEJ,CAAC;AAEF,SAAS,MACP,UACA,UAIA,MACA,UACA,QACM;CACN,MAAMA,SAAkE;EACtE,CAAC,WAAW,KAAK,cAAc;EAC/B,CAAC,OAAO,KAAK,UAAU;EACvB,CAAC,QAAQ,KAAK,WAAW;EAC1B;AACD,MAAK,MAAM,CAAC,OAAO,UAAU,QAAQ;AACnC,MAAI,UAAU,OAAW;EACzB,MAAM,WAAW,SAAS,OAAO,SAAS;AAC1C,MAAI,WAAW,OAAO;AACpB,OAAI,aAAa,QACf,OAAM,IAAI,wBAAwB;IAAE;IAAO;IAAO;IAAU,CAAC;AAE/D,UACE,uCAAuC,MAAM,kBAAkB,SAAS,QAAQ,EAAE,CAAC,KAAK,MAAM,QAAQ,EAAE,CAAC,SACzG;IAAE;IAAO;IAAO;IAAU,CAC3B;;;;AAKP,SAAS,cAAc,SAAiB,MAAqB;AAC3D,KAAI,SAAS,OAAW,SAAQ,KAAK,SAAS,KAAK;KAC9C,SAAQ,KAAK,QAAQ;;AAG5B,SAAS,SAAS,OAA+D;AAC/E,KAAI,UAAU,UAAa,UAAU,KAAM,QAAO;AAClD,QAAO"}
1
+ {"version":3,"file":"with-cost-limit.js","names":["checks: Array<['session' | 'run' | 'hour', number | undefined]>"],"sources":["../../src/middleware/with-cost-limit.ts"],"sourcesContent":["/**\n * `withCostLimit` - enforce per-session / per-run / per-agent /\n * per-hour cost ceilings. Couples with {@link withCostTracking} for\n * the underlying accumulator. The middleware is positioned between\n * `withRateLimit` and `withCostTracking` per the canonical order so a\n * budget breach trips before the request hits the underlying\n * provider.\n *\n * @packageDocumentation\n */\n\nimport type { Provider } from '@graphorin/core';\n\nimport { CostBudgetExceededError } from '../errors/errors.js';\nimport { defineProviderMiddleware } from './compose.js';\n\n/**\n * Options for {@link withCostLimit}.\n *\n * @stable\n */\nexport interface WithCostLimitOptions {\n /** Maximum cumulative USD cost per session. */\n readonly maxPerSession?: number;\n /** Maximum cumulative USD cost per run. */\n readonly maxPerRun?: number;\n /** Maximum cumulative USD cost per hour. */\n readonly maxPerHour?: number;\n /** What to do on breach. Default `'throw'`. */\n readonly onExceed?: 'throw' | 'warn';\n /**\n * Resolver returning the current observed cost for the relevant\n * scope. The resolver lets consumers wire any accumulator (the\n * shipped `@graphorin/observability/cost.CostTracker` works out of\n * the box). When unset, the middleware is a no-op (a placeholder\n * for tooling that wires the accumulator later).\n */\n readonly resolveObservedCost?: (\n scope: 'session' | 'run' | 'hour',\n metadata: Readonly<Record<string, unknown>> | undefined,\n ) => number;\n /** Optional sink for `'warn'` mode. Defaults to `console.warn`. */\n readonly logger?: (message: string, meta?: object) => void;\n}\n\n/**\n * @stable\n */\nexport const withCostLimit = defineProviderMiddleware<WithCostLimitOptions>({\n kind: 'withCostLimit',\n factory: (opts: WithCostLimitOptions) => {\n const onExceed = opts.onExceed ?? 'throw';\n const resolver = opts.resolveObservedCost;\n const logger = opts.logger ?? defaultLogger;\n // PS-8: a ceiling without a resolver is a silent no-op - the limits look\n // enforced but never trip. Warn loudly so the gap is visible. (No ceiling\n // + no resolver is the documented inert placeholder, so it stays quiet.)\n const hasCeiling =\n opts.maxPerSession !== undefined ||\n opts.maxPerRun !== undefined ||\n opts.maxPerHour !== undefined;\n if (hasCeiling && resolver === undefined) {\n logger(\n '[graphorin/provider] withCostLimit: a cost ceiling is configured but no ' +\n '`resolveObservedCost` was supplied, so the limit is UNENFORCED (no-op). ' +\n 'Wire a resolver (e.g. from createCostAccumulator / @graphorin/observability).',\n );\n }\n return (next: Provider): Provider => ({\n name: next.name,\n modelId: next.modelId,\n capabilities: next.capabilities,\n ...(next.acceptsSensitivity !== undefined\n ? { acceptsSensitivity: next.acceptsSensitivity }\n : {}),\n stream(req) {\n if (resolver !== undefined) {\n check(asRecord(req.metadata), resolver, opts, onExceed, logger);\n }\n return next.stream(req);\n },\n async generate(req) {\n if (resolver !== undefined) {\n check(asRecord(req.metadata), resolver, opts, onExceed, logger);\n }\n return next.generate(req);\n },\n ...(next.countTokens ? { countTokens: next.countTokens.bind(next) } : {}),\n });\n },\n});\n\nfunction check(\n metadata: Readonly<Record<string, unknown>> | undefined,\n resolver: (\n scope: 'session' | 'run' | 'hour',\n metadata?: Readonly<Record<string, unknown>>,\n ) => number,\n opts: WithCostLimitOptions,\n onExceed: 'throw' | 'warn',\n logger: (message: string, meta?: object) => void,\n): void {\n const checks: Array<['session' | 'run' | 'hour', number | undefined]> = [\n ['session', opts.maxPerSession],\n ['run', opts.maxPerRun],\n ['hour', opts.maxPerHour],\n ];\n for (const [scope, limit] of checks) {\n if (limit === undefined) continue;\n const observed = resolver(scope, metadata);\n if (observed > limit) {\n if (onExceed === 'throw') {\n throw new CostBudgetExceededError({ scope, limit, observed });\n }\n logger(\n `[graphorin/provider] withCostLimit: ${scope} budget breach (${observed.toFixed(4)} > ${limit.toFixed(4)} USD).`,\n { scope, limit, observed },\n );\n }\n }\n}\n\nfunction defaultLogger(message: string, meta?: object): void {\n if (meta !== undefined) console.warn(message, meta);\n else console.warn(message);\n}\n\nfunction asRecord(value: unknown): Readonly<Record<string, unknown>> | undefined {\n if (value === undefined || value === null) return undefined;\n return value as Readonly<Record<string, unknown>>;\n}\n"],"mappings":";;;;;;;AAgDA,MAAa,gBAAgB,yBAA+C;CAC1E,MAAM;CACN,UAAU,SAA+B;EACvC,MAAM,WAAW,KAAK,YAAY;EAClC,MAAM,WAAW,KAAK;EACtB,MAAM,SAAS,KAAK,UAAU;AAQ9B,OAHE,KAAK,kBAAkB,UACvB,KAAK,cAAc,UACnB,KAAK,eAAe,WACJ,aAAa,OAC7B,QACE,gOAGD;AAEH,UAAQ,UAA8B;GACpC,MAAM,KAAK;GACX,SAAS,KAAK;GACd,cAAc,KAAK;GACnB,GAAI,KAAK,uBAAuB,SAC5B,EAAE,oBAAoB,KAAK,oBAAoB,GAC/C,EAAE;GACN,OAAO,KAAK;AACV,QAAI,aAAa,OACf,OAAM,SAAS,IAAI,SAAS,EAAE,UAAU,MAAM,UAAU,OAAO;AAEjE,WAAO,KAAK,OAAO,IAAI;;GAEzB,MAAM,SAAS,KAAK;AAClB,QAAI,aAAa,OACf,OAAM,SAAS,IAAI,SAAS,EAAE,UAAU,MAAM,UAAU,OAAO;AAEjE,WAAO,KAAK,SAAS,IAAI;;GAE3B,GAAI,KAAK,cAAc,EAAE,aAAa,KAAK,YAAY,KAAK,KAAK,EAAE,GAAG,EAAE;GACzE;;CAEJ,CAAC;AAEF,SAAS,MACP,UACA,UAIA,MACA,UACA,QACM;CACN,MAAMA,SAAkE;EACtE,CAAC,WAAW,KAAK,cAAc;EAC/B,CAAC,OAAO,KAAK,UAAU;EACvB,CAAC,QAAQ,KAAK,WAAW;EAC1B;AACD,MAAK,MAAM,CAAC,OAAO,UAAU,QAAQ;AACnC,MAAI,UAAU,OAAW;EACzB,MAAM,WAAW,SAAS,OAAO,SAAS;AAC1C,MAAI,WAAW,OAAO;AACpB,OAAI,aAAa,QACf,OAAM,IAAI,wBAAwB;IAAE;IAAO;IAAO;IAAU,CAAC;AAE/D,UACE,uCAAuC,MAAM,kBAAkB,SAAS,QAAQ,EAAE,CAAC,KAAK,MAAM,QAAQ,EAAE,CAAC,SACzG;IAAE;IAAO;IAAO;IAAU,CAC3B;;;;AAKP,SAAS,cAAc,SAAiB,MAAqB;AAC3D,KAAI,SAAS,OAAW,SAAQ,KAAK,SAAS,KAAK;KAC9C,SAAQ,KAAK,QAAQ;;AAG5B,SAAS,SAAS,OAA+D;AAC/E,KAAI,UAAU,UAAa,UAAU,KAAM,QAAO;AAClD,QAAO"}
@@ -3,7 +3,7 @@ import * as _graphorin_core2 from "@graphorin/core";
3
3
  //#region src/middleware/with-cost-tracking.d.ts
4
4
 
5
5
  /**
6
- * `withCostTracking` reports `tokensUsed` + `cost` per `provider × model`
6
+ * `withCostTracking` - reports `tokensUsed` + `cost` per `provider × model`
7
7
  * from the `finish` event of every stream / one-shot generation via the
8
8
  * `onUsage` hook. For a ready-made process-local accumulator, wire
9
9
  * {@link createCostAccumulator}'s `onUsage` and read its `.totals()`; consumers
@@ -23,12 +23,16 @@ interface CostTrackingTotals {
23
23
  readonly promptTokens: number;
24
24
  readonly completionTokens: number;
25
25
  readonly totalTokens: number;
26
+ /** Prompt tokens served from the provider cache (subset of promptTokens). */
27
+ readonly cachedReadTokens: number;
28
+ /** Prompt tokens written to the provider cache (subset of promptTokens). */
29
+ readonly cacheWriteTokens: number;
26
30
  readonly costUsd: number;
27
31
  }
28
32
  /**
29
33
  * A process-local cost accumulator (PS-8). Wire {@link CostAccumulator.onUsage}
30
- * into {@link withCostTracking} and read the running totals keyed by
31
- * `provider × model` back via {@link CostAccumulator.totals} /
34
+ * into {@link withCostTracking} and read the running totals - keyed by
35
+ * `provider × model` - back via {@link CostAccumulator.totals} /
32
36
  * {@link CostAccumulator.totalFor}.
33
37
  *
34
38
  * @stable
@@ -44,7 +48,7 @@ interface CostAccumulator {
44
48
  reset(): void;
45
49
  }
46
50
  /**
47
- * Create a {@link CostAccumulator} the process-local accumulator described on
51
+ * Create a {@link CostAccumulator} - the process-local accumulator described on
48
52
  * {@link withCostTracking}. Keys totals by `'<providerName>::<modelId>'`.
49
53
  *
50
54
  * @stable
@@ -67,12 +71,19 @@ interface WithCostTrackingOptions {
67
71
  readonly promptTokens: number;
68
72
  readonly completionTokens: number;
69
73
  readonly totalTokens: number;
74
+ readonly cachedReadTokens?: number;
75
+ readonly cacheWriteTokens?: number;
70
76
  readonly costUsd: number;
71
77
  readonly metadata: Readonly<Record<string, unknown>> | undefined;
72
78
  }) => void;
73
79
  /**
74
80
  * Optional pricing lookup. When set, the middleware computes
75
81
  * `costUsd` from the returned price and surfaces it on the hook.
82
+ *
83
+ * `cachedReadPerMtok` / `cacheWritePerMtok` price the prompt-cache legs
84
+ * (core-provider-02); when omitted, cache tokens are billed at the full
85
+ * input rate (never cheaper than reality, so absent price data degrades
86
+ * to the pre-cache behaviour).
76
87
  */
77
88
  readonly priceLookup?: (info: {
78
89
  readonly providerName: string;
@@ -80,6 +91,8 @@ interface WithCostTrackingOptions {
80
91
  }) => {
81
92
  readonly inputPerMtok?: number;
82
93
  readonly outputPerMtok?: number;
94
+ readonly cachedReadPerMtok?: number;
95
+ readonly cacheWritePerMtok?: number;
83
96
  } | null;
84
97
  }
85
98
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"with-cost-tracking.d.ts","names":[],"sources":["../../src/middleware/with-cost-tracking.ts"],"sourcesContent":[],"mappings":";;;;;;;AAqBA;AAgBA;;;;;;;AAyBA;AA0BA;AA4BA;;;UA/FiB,kBAAA;;;;;;;;;;;;;;;UAgBA,eAAA;;oBAEG,YAAY;;YAEpB,oBAAoB;;mDAEmB;;;;;;;;;;iBAmBnC,qBAAA,CAAA,GAAyB;;;;;;UA0BxB,uBAAA;;;;;;;;;;;;;uBAaM,SAAS;;;;;;;;;;;;;;;;;cAenB,yBAAgB,4BAAA,gBAAA,CAAA"}
1
+ {"version":3,"file":"with-cost-tracking.d.ts","names":[],"sources":["../../src/middleware/with-cost-tracking.ts"],"sourcesContent":[],"mappings":";;;;;;;AAqBA;AAoBA;;;;;;;AA2BA;AA4BA;AAqCA;;;UAhHiB,kBAAA;;;;;;;;;;;;;;;;;;;UAoBA,eAAA;;oBAEG,YAAY;;YAEpB,oBAAoB;;mDAEmB;;;;;;;;;;iBAqBnC,qBAAA,CAAA,GAAyB;;;;;;UA4BxB,uBAAA;;;;;;;;;;;;;;;uBAeM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;cAsBnB,yBAAgB,4BAAA,gBAAA,CAAA"}
@@ -6,10 +6,12 @@ const ZERO_TOTALS = Object.freeze({
6
6
  promptTokens: 0,
7
7
  completionTokens: 0,
8
8
  totalTokens: 0,
9
+ cachedReadTokens: 0,
10
+ cacheWriteTokens: 0,
9
11
  costUsd: 0
10
12
  });
11
13
  /**
12
- * Create a {@link CostAccumulator} the process-local accumulator described on
14
+ * Create a {@link CostAccumulator} - the process-local accumulator described on
13
15
  * {@link withCostTracking}. Keys totals by `'<providerName>::<modelId>'`.
14
16
  *
15
17
  * @stable
@@ -26,6 +28,8 @@ function createCostAccumulator() {
26
28
  promptTokens: prev.promptTokens + info.promptTokens,
27
29
  completionTokens: prev.completionTokens + info.completionTokens,
28
30
  totalTokens: prev.totalTokens + info.totalTokens,
31
+ cachedReadTokens: prev.cachedReadTokens + (info.cachedReadTokens ?? 0),
32
+ cacheWriteTokens: prev.cacheWriteTokens + (info.cacheWriteTokens ?? 0),
29
33
  costUsd: prev.costUsd + info.costUsd
30
34
  });
31
35
  },
@@ -78,13 +82,19 @@ function emitUsage(result, next, opts, metadata) {
78
82
  providerName: next.name,
79
83
  modelId: next.modelId
80
84
  }) ?? null;
81
- const costUsd = price !== null ? usage.promptTokens * (price.inputPerMtok ?? 0) / 1e6 + usage.completionTokens * (price.outputPerMtok ?? 0) / 1e6 : usage.cost?.amount ?? 0;
85
+ const cachedRead = usage.cachedReadTokens ?? 0;
86
+ const cacheWrite = usage.cacheWriteTokens ?? 0;
87
+ const basePromptTokens = Math.max(0, usage.promptTokens - cachedRead - cacheWrite);
88
+ const inputRate = price?.inputPerMtok ?? 0;
89
+ const costUsd = price !== null ? (basePromptTokens * inputRate + cachedRead * (price.cachedReadPerMtok ?? inputRate) + cacheWrite * (price.cacheWritePerMtok ?? inputRate) + (usage.completionTokens + (usage.reasoningTokens ?? 0)) * (price.outputPerMtok ?? 0)) / 1e6 : usage.cost?.amount ?? 0;
82
90
  opts.onUsage({
83
91
  providerName: next.name,
84
92
  modelId: next.modelId,
85
93
  promptTokens: usage.promptTokens,
86
94
  completionTokens: usage.completionTokens,
87
95
  totalTokens: usage.totalTokens,
96
+ ...usage.cachedReadTokens !== void 0 ? { cachedReadTokens: usage.cachedReadTokens } : {},
97
+ ...usage.cacheWriteTokens !== void 0 ? { cacheWriteTokens: usage.cacheWriteTokens } : {},
88
98
  costUsd,
89
99
  metadata
90
100
  });
@@ -1 +1 @@
1
- {"version":3,"file":"with-cost-tracking.js","names":["ZERO_TOTALS: CostTrackingTotals"],"sources":["../../src/middleware/with-cost-tracking.ts"],"sourcesContent":["/**\n * `withCostTracking` — reports `tokensUsed` + `cost` per `provider × model`\n * from the `finish` event of every stream / one-shot generation via the\n * `onUsage` hook. For a ready-made process-local accumulator, wire\n * {@link createCostAccumulator}'s `onUsage` and read its `.totals()`; consumers\n * that prefer the framework's `CostTracker` from `@graphorin/observability`\n * pass their own delegate.\n *\n * @packageDocumentation\n */\n\nimport type { Provider, ProviderEvent, ProviderResponse } from '@graphorin/core';\n\nimport { defineProviderMiddleware } from './compose.js';\n\n/**\n * Aggregated totals for one `provider × model`, returned by\n * {@link CostAccumulator.totalFor} / {@link CostAccumulator.totals}.\n *\n * @stable\n */\nexport interface CostTrackingTotals {\n readonly callCount: number;\n readonly promptTokens: number;\n readonly completionTokens: number;\n readonly totalTokens: number;\n readonly costUsd: number;\n}\n\n/**\n * A process-local cost accumulator (PS-8). Wire {@link CostAccumulator.onUsage}\n * into {@link withCostTracking} and read the running totals — keyed by\n * `provider × model` — back via {@link CostAccumulator.totals} /\n * {@link CostAccumulator.totalFor}.\n *\n * @stable\n */\nexport interface CostAccumulator {\n /** Pass this to {@link withCostTracking}'s `onUsage`. */\n readonly onUsage: NonNullable<WithCostTrackingOptions['onUsage']>;\n /** Snapshot of every tracked `provider::model` → totals. */\n totals(): ReadonlyMap<string, CostTrackingTotals>;\n /** Running totals for one `provider × model` (zeros when unseen). */\n totalFor(providerName: string, modelId: string): CostTrackingTotals;\n /** Clear all accumulated totals. */\n reset(): void;\n}\n\nconst ZERO_TOTALS: CostTrackingTotals = Object.freeze({\n callCount: 0,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n costUsd: 0,\n});\n\n/**\n * Create a {@link CostAccumulator} — the process-local accumulator described on\n * {@link withCostTracking}. Keys totals by `'<providerName>::<modelId>'`.\n *\n * @stable\n */\nexport function createCostAccumulator(): CostAccumulator {\n const map = new Map<string, CostTrackingTotals>();\n const keyOf = (providerName: string, modelId: string): string => `${providerName}::${modelId}`;\n return {\n onUsage: (info) => {\n const key = keyOf(info.providerName, info.modelId);\n const prev = map.get(key) ?? ZERO_TOTALS;\n map.set(key, {\n callCount: prev.callCount + 1,\n promptTokens: prev.promptTokens + info.promptTokens,\n completionTokens: prev.completionTokens + info.completionTokens,\n totalTokens: prev.totalTokens + info.totalTokens,\n costUsd: prev.costUsd + info.costUsd,\n });\n },\n totals: () => new Map(map),\n totalFor: (providerName, modelId) => map.get(keyOf(providerName, modelId)) ?? ZERO_TOTALS,\n reset: () => map.clear(),\n };\n}\n\n/**\n * Options for {@link withCostTracking}.\n *\n * @stable\n */\nexport interface WithCostTrackingOptions {\n /**\n * Hook fired on every `finish` event with the parsed usage. The\n * hook receives the underlying provider's name + modelId so the\n * caller can route into a per-model accumulator.\n */\n readonly onUsage?: (info: {\n readonly providerName: string;\n readonly modelId: string;\n readonly promptTokens: number;\n readonly completionTokens: number;\n readonly totalTokens: number;\n readonly costUsd: number;\n readonly metadata: Readonly<Record<string, unknown>> | undefined;\n }) => void;\n /**\n * Optional pricing lookup. When set, the middleware computes\n * `costUsd` from the returned price and surfaces it on the hook.\n */\n readonly priceLookup?: (info: {\n readonly providerName: string;\n readonly modelId: string;\n }) => { readonly inputPerMtok?: number; readonly outputPerMtok?: number } | null;\n}\n\n/**\n * @stable\n */\nexport const withCostTracking = defineProviderMiddleware<WithCostTrackingOptions>({\n kind: 'withCostTracking',\n factory: (opts: WithCostTrackingOptions) => {\n return (next: Provider): Provider => ({\n name: next.name,\n modelId: next.modelId,\n capabilities: next.capabilities,\n ...(next.acceptsSensitivity !== undefined\n ? { acceptsSensitivity: next.acceptsSensitivity }\n : {}),\n stream(req) {\n return trackingStream(next, req, opts);\n },\n async generate(req) {\n const result = await next.generate(req);\n emitUsage(result, next, opts, asRecord(req.metadata));\n return result;\n },\n ...(next.countTokens ? { countTokens: next.countTokens.bind(next) } : {}),\n });\n },\n});\n\nasync function* trackingStream(\n next: Provider,\n req: import('@graphorin/core').ProviderRequest,\n opts: WithCostTrackingOptions,\n): AsyncIterable<ProviderEvent> {\n const metadata = asRecord(req.metadata);\n for await (const event of next.stream(req)) {\n yield event;\n if (event.type === 'finish') {\n emitUsage({ usage: event.usage, finishReason: event.finishReason }, next, opts, metadata);\n }\n }\n}\n\nfunction asRecord(value: unknown): Readonly<Record<string, unknown>> | undefined {\n if (value === undefined || value === null) return undefined;\n return value as Readonly<Record<string, unknown>>;\n}\n\nfunction emitUsage(\n result:\n | ProviderResponse\n | { usage: ProviderResponse['usage']; finishReason: ProviderResponse['finishReason'] },\n next: Provider,\n opts: WithCostTrackingOptions,\n metadata: Readonly<Record<string, unknown>> | undefined,\n): void {\n if (opts.onUsage === undefined) return;\n const usage = result.usage;\n const price = opts.priceLookup?.({ providerName: next.name, modelId: next.modelId }) ?? null;\n const costUsd =\n price !== null\n ? (usage.promptTokens * (price.inputPerMtok ?? 0)) / 1_000_000 +\n (usage.completionTokens * (price.outputPerMtok ?? 0)) / 1_000_000\n : (usage.cost?.amount ?? 0);\n opts.onUsage({\n providerName: next.name,\n modelId: next.modelId,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n costUsd,\n metadata,\n });\n}\n"],"mappings":";;;AAgDA,MAAMA,cAAkC,OAAO,OAAO;CACpD,WAAW;CACX,cAAc;CACd,kBAAkB;CAClB,aAAa;CACb,SAAS;CACV,CAAC;;;;;;;AAQF,SAAgB,wBAAyC;CACvD,MAAM,sBAAM,IAAI,KAAiC;CACjD,MAAM,SAAS,cAAsB,YAA4B,GAAG,aAAa,IAAI;AACrF,QAAO;EACL,UAAU,SAAS;GACjB,MAAM,MAAM,MAAM,KAAK,cAAc,KAAK,QAAQ;GAClD,MAAM,OAAO,IAAI,IAAI,IAAI,IAAI;AAC7B,OAAI,IAAI,KAAK;IACX,WAAW,KAAK,YAAY;IAC5B,cAAc,KAAK,eAAe,KAAK;IACvC,kBAAkB,KAAK,mBAAmB,KAAK;IAC/C,aAAa,KAAK,cAAc,KAAK;IACrC,SAAS,KAAK,UAAU,KAAK;IAC9B,CAAC;;EAEJ,cAAc,IAAI,IAAI,IAAI;EAC1B,WAAW,cAAc,YAAY,IAAI,IAAI,MAAM,cAAc,QAAQ,CAAC,IAAI;EAC9E,aAAa,IAAI,OAAO;EACzB;;;;;AAoCH,MAAa,mBAAmB,yBAAkD;CAChF,MAAM;CACN,UAAU,SAAkC;AAC1C,UAAQ,UAA8B;GACpC,MAAM,KAAK;GACX,SAAS,KAAK;GACd,cAAc,KAAK;GACnB,GAAI,KAAK,uBAAuB,SAC5B,EAAE,oBAAoB,KAAK,oBAAoB,GAC/C,EAAE;GACN,OAAO,KAAK;AACV,WAAO,eAAe,MAAM,KAAK,KAAK;;GAExC,MAAM,SAAS,KAAK;IAClB,MAAM,SAAS,MAAM,KAAK,SAAS,IAAI;AACvC,cAAU,QAAQ,MAAM,MAAM,SAAS,IAAI,SAAS,CAAC;AACrD,WAAO;;GAET,GAAI,KAAK,cAAc,EAAE,aAAa,KAAK,YAAY,KAAK,KAAK,EAAE,GAAG,EAAE;GACzE;;CAEJ,CAAC;AAEF,gBAAgB,eACd,MACA,KACA,MAC8B;CAC9B,MAAM,WAAW,SAAS,IAAI,SAAS;AACvC,YAAW,MAAM,SAAS,KAAK,OAAO,IAAI,EAAE;AAC1C,QAAM;AACN,MAAI,MAAM,SAAS,SACjB,WAAU;GAAE,OAAO,MAAM;GAAO,cAAc,MAAM;GAAc,EAAE,MAAM,MAAM,SAAS;;;AAK/F,SAAS,SAAS,OAA+D;AAC/E,KAAI,UAAU,UAAa,UAAU,KAAM,QAAO;AAClD,QAAO;;AAGT,SAAS,UACP,QAGA,MACA,MACA,UACM;AACN,KAAI,KAAK,YAAY,OAAW;CAChC,MAAM,QAAQ,OAAO;CACrB,MAAM,QAAQ,KAAK,cAAc;EAAE,cAAc,KAAK;EAAM,SAAS,KAAK;EAAS,CAAC,IAAI;CACxF,MAAM,UACJ,UAAU,OACL,MAAM,gBAAgB,MAAM,gBAAgB,KAAM,MAClD,MAAM,oBAAoB,MAAM,iBAAiB,KAAM,MACvD,MAAM,MAAM,UAAU;AAC7B,MAAK,QAAQ;EACX,cAAc,KAAK;EACnB,SAAS,KAAK;EACd,cAAc,MAAM;EACpB,kBAAkB,MAAM;EACxB,aAAa,MAAM;EACnB;EACA;EACD,CAAC"}
1
+ {"version":3,"file":"with-cost-tracking.js","names":["ZERO_TOTALS: CostTrackingTotals"],"sources":["../../src/middleware/with-cost-tracking.ts"],"sourcesContent":["/**\n * `withCostTracking` - reports `tokensUsed` + `cost` per `provider × model`\n * from the `finish` event of every stream / one-shot generation via the\n * `onUsage` hook. For a ready-made process-local accumulator, wire\n * {@link createCostAccumulator}'s `onUsage` and read its `.totals()`; consumers\n * that prefer the framework's `CostTracker` from `@graphorin/observability`\n * pass their own delegate.\n *\n * @packageDocumentation\n */\n\nimport type { Provider, ProviderEvent, ProviderResponse } from '@graphorin/core';\n\nimport { defineProviderMiddleware } from './compose.js';\n\n/**\n * Aggregated totals for one `provider × model`, returned by\n * {@link CostAccumulator.totalFor} / {@link CostAccumulator.totals}.\n *\n * @stable\n */\nexport interface CostTrackingTotals {\n readonly callCount: number;\n readonly promptTokens: number;\n readonly completionTokens: number;\n readonly totalTokens: number;\n /** Prompt tokens served from the provider cache (subset of promptTokens). */\n readonly cachedReadTokens: number;\n /** Prompt tokens written to the provider cache (subset of promptTokens). */\n readonly cacheWriteTokens: number;\n readonly costUsd: number;\n}\n\n/**\n * A process-local cost accumulator (PS-8). Wire {@link CostAccumulator.onUsage}\n * into {@link withCostTracking} and read the running totals - keyed by\n * `provider × model` - back via {@link CostAccumulator.totals} /\n * {@link CostAccumulator.totalFor}.\n *\n * @stable\n */\nexport interface CostAccumulator {\n /** Pass this to {@link withCostTracking}'s `onUsage`. */\n readonly onUsage: NonNullable<WithCostTrackingOptions['onUsage']>;\n /** Snapshot of every tracked `provider::model` → totals. */\n totals(): ReadonlyMap<string, CostTrackingTotals>;\n /** Running totals for one `provider × model` (zeros when unseen). */\n totalFor(providerName: string, modelId: string): CostTrackingTotals;\n /** Clear all accumulated totals. */\n reset(): void;\n}\n\nconst ZERO_TOTALS: CostTrackingTotals = Object.freeze({\n callCount: 0,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n cachedReadTokens: 0,\n cacheWriteTokens: 0,\n costUsd: 0,\n});\n\n/**\n * Create a {@link CostAccumulator} - the process-local accumulator described on\n * {@link withCostTracking}. Keys totals by `'<providerName>::<modelId>'`.\n *\n * @stable\n */\nexport function createCostAccumulator(): CostAccumulator {\n const map = new Map<string, CostTrackingTotals>();\n const keyOf = (providerName: string, modelId: string): string => `${providerName}::${modelId}`;\n return {\n onUsage: (info) => {\n const key = keyOf(info.providerName, info.modelId);\n const prev = map.get(key) ?? ZERO_TOTALS;\n map.set(key, {\n callCount: prev.callCount + 1,\n promptTokens: prev.promptTokens + info.promptTokens,\n completionTokens: prev.completionTokens + info.completionTokens,\n totalTokens: prev.totalTokens + info.totalTokens,\n cachedReadTokens: prev.cachedReadTokens + (info.cachedReadTokens ?? 0),\n cacheWriteTokens: prev.cacheWriteTokens + (info.cacheWriteTokens ?? 0),\n costUsd: prev.costUsd + info.costUsd,\n });\n },\n totals: () => new Map(map),\n totalFor: (providerName, modelId) => map.get(keyOf(providerName, modelId)) ?? ZERO_TOTALS,\n reset: () => map.clear(),\n };\n}\n\n/**\n * Options for {@link withCostTracking}.\n *\n * @stable\n */\nexport interface WithCostTrackingOptions {\n /**\n * Hook fired on every `finish` event with the parsed usage. The\n * hook receives the underlying provider's name + modelId so the\n * caller can route into a per-model accumulator.\n */\n readonly onUsage?: (info: {\n readonly providerName: string;\n readonly modelId: string;\n readonly promptTokens: number;\n readonly completionTokens: number;\n readonly totalTokens: number;\n readonly cachedReadTokens?: number;\n readonly cacheWriteTokens?: number;\n readonly costUsd: number;\n readonly metadata: Readonly<Record<string, unknown>> | undefined;\n }) => void;\n /**\n * Optional pricing lookup. When set, the middleware computes\n * `costUsd` from the returned price and surfaces it on the hook.\n *\n * `cachedReadPerMtok` / `cacheWritePerMtok` price the prompt-cache legs\n * (core-provider-02); when omitted, cache tokens are billed at the full\n * input rate (never cheaper than reality, so absent price data degrades\n * to the pre-cache behaviour).\n */\n readonly priceLookup?: (info: { readonly providerName: string; readonly modelId: string }) => {\n readonly inputPerMtok?: number;\n readonly outputPerMtok?: number;\n readonly cachedReadPerMtok?: number;\n readonly cacheWritePerMtok?: number;\n } | null;\n}\n\n/**\n * @stable\n */\nexport const withCostTracking = defineProviderMiddleware<WithCostTrackingOptions>({\n kind: 'withCostTracking',\n factory: (opts: WithCostTrackingOptions) => {\n return (next: Provider): Provider => ({\n name: next.name,\n modelId: next.modelId,\n capabilities: next.capabilities,\n ...(next.acceptsSensitivity !== undefined\n ? { acceptsSensitivity: next.acceptsSensitivity }\n : {}),\n stream(req) {\n return trackingStream(next, req, opts);\n },\n async generate(req) {\n const result = await next.generate(req);\n emitUsage(result, next, opts, asRecord(req.metadata));\n return result;\n },\n ...(next.countTokens ? { countTokens: next.countTokens.bind(next) } : {}),\n });\n },\n});\n\nasync function* trackingStream(\n next: Provider,\n req: import('@graphorin/core').ProviderRequest,\n opts: WithCostTrackingOptions,\n): AsyncIterable<ProviderEvent> {\n const metadata = asRecord(req.metadata);\n for await (const event of next.stream(req)) {\n yield event;\n if (event.type === 'finish') {\n emitUsage({ usage: event.usage, finishReason: event.finishReason }, next, opts, metadata);\n }\n }\n}\n\nfunction asRecord(value: unknown): Readonly<Record<string, unknown>> | undefined {\n if (value === undefined || value === null) return undefined;\n return value as Readonly<Record<string, unknown>>;\n}\n\nfunction emitUsage(\n result:\n | ProviderResponse\n | { usage: ProviderResponse['usage']; finishReason: ProviderResponse['finishReason'] },\n next: Provider,\n opts: WithCostTrackingOptions,\n metadata: Readonly<Record<string, unknown>> | undefined,\n): void {\n if (opts.onUsage === undefined) return;\n const usage = result.usage;\n const price = opts.priceLookup?.({ providerName: next.name, modelId: next.modelId }) ?? null;\n // Reasoning tokens are billed at the output rate (the PS-19 contract\n // in @graphorin/pricing) - providers that report them separately from\n // completionTokens must not get them for free.\n // Cache legs (core-provider-02): promptTokens INCLUDES cache reads and\n // writes, so subtract them from the base-rate leg and bill each at its\n // own rate; a missing cache rate falls back to the full input rate.\n const cachedRead = usage.cachedReadTokens ?? 0;\n const cacheWrite = usage.cacheWriteTokens ?? 0;\n const basePromptTokens = Math.max(0, usage.promptTokens - cachedRead - cacheWrite);\n const inputRate = price?.inputPerMtok ?? 0;\n const costUsd =\n price !== null\n ? (basePromptTokens * inputRate +\n cachedRead * (price.cachedReadPerMtok ?? inputRate) +\n cacheWrite * (price.cacheWritePerMtok ?? inputRate) +\n (usage.completionTokens + (usage.reasoningTokens ?? 0)) * (price.outputPerMtok ?? 0)) /\n 1_000_000\n : (usage.cost?.amount ?? 0);\n opts.onUsage({\n providerName: next.name,\n modelId: next.modelId,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n ...(usage.cachedReadTokens !== undefined ? { cachedReadTokens: usage.cachedReadTokens } : {}),\n ...(usage.cacheWriteTokens !== undefined ? { cacheWriteTokens: usage.cacheWriteTokens } : {}),\n costUsd,\n metadata,\n });\n}\n"],"mappings":"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"}
@@ -68,8 +68,10 @@ function defaultShouldFallback(err) {
68
68
  if (err === null || typeof err !== "object") return false;
69
69
  if (isAbortError(err)) return false;
70
70
  const e = err;
71
- if (e.kind === "unauthorized" || e.kind === "invalid-request") return false;
72
- if (e.kind === "transient" || e.kind === "rate-limit" || e.kind === "capacity") return true;
71
+ const kinds = [e.kind, e.errorKind];
72
+ if (kinds.includes("unauthorized") || kinds.includes("invalid-request") || kinds.includes("context-length") || kinds.includes("content-filter")) return false;
73
+ if (kinds.includes("transient") || kinds.includes("rate-limit") || kinds.includes("rate-limit-exceeded") || kinds.includes("capacity")) return true;
74
+ if (typeof e.status === "number" && e.status === 429) return true;
73
75
  if (typeof e.status === "number" && e.status >= 500) return true;
74
76
  if (typeof e.status === "number" && e.status === 0) return true;
75
77
  return false;