@graphorin/provider 0.6.0 → 0.7.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 (76) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +8 -3
  3. package/dist/adapters/llamacpp-server.d.ts +1 -1
  4. package/dist/adapters/llamacpp-server.js.map +1 -1
  5. package/dist/adapters/ollama.d.ts.map +1 -1
  6. package/dist/adapters/ollama.js +19 -6
  7. package/dist/adapters/ollama.js.map +1 -1
  8. package/dist/adapters/vercel.d.ts +1 -1
  9. package/dist/adapters/vercel.d.ts.map +1 -1
  10. package/dist/adapters/vercel.js +111 -33
  11. package/dist/adapters/vercel.js.map +1 -1
  12. package/dist/errors/errors.d.ts +1 -1
  13. package/dist/errors/errors.js +1 -1
  14. package/dist/errors/errors.js.map +1 -1
  15. package/dist/index.d.ts +1 -2
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +3 -8
  18. package/dist/index.js.map +1 -1
  19. package/dist/internal/http.js +111 -7
  20. package/dist/internal/http.js.map +1 -1
  21. package/dist/internal/openai-shaped.js +21 -4
  22. package/dist/internal/openai-shaped.js.map +1 -1
  23. package/dist/middleware/with-fallback.js +1 -1
  24. package/dist/middleware/with-rate-limit.d.ts +21 -8
  25. package/dist/middleware/with-rate-limit.d.ts.map +1 -1
  26. package/dist/middleware/with-rate-limit.js +65 -12
  27. package/dist/middleware/with-rate-limit.js.map +1 -1
  28. package/dist/middleware/with-retry.js +1 -1
  29. package/dist/package.js +6 -0
  30. package/dist/package.js.map +1 -0
  31. package/package.json +18 -16
  32. package/src/adapters/index.ts +14 -0
  33. package/src/adapters/llamacpp-server.ts +102 -0
  34. package/src/adapters/ollama.ts +382 -0
  35. package/src/adapters/openai-compatible.ts +95 -0
  36. package/src/adapters/vercel-messages.ts +308 -0
  37. package/src/adapters/vercel.ts +706 -0
  38. package/src/counters/anthropic-wire.ts +199 -0
  39. package/src/counters/anthropic.ts +114 -0
  40. package/src/counters/bedrock.ts +46 -0
  41. package/src/counters/dispatcher.ts +127 -0
  42. package/src/counters/global.ts +39 -0
  43. package/src/counters/google.ts +46 -0
  44. package/src/counters/heuristic.ts +107 -0
  45. package/src/counters/index.ts +35 -0
  46. package/src/counters/js-tiktoken.ts +135 -0
  47. package/src/counters/serialize.ts +85 -0
  48. package/src/errors/errors.ts +316 -0
  49. package/src/errors/index.ts +20 -0
  50. package/src/index.ts +42 -0
  51. package/src/internal/abort.ts +30 -0
  52. package/src/internal/http.ts +388 -0
  53. package/src/internal/openai-shaped.ts +555 -0
  54. package/src/internal/sse.ts +112 -0
  55. package/src/internal/url-utils.ts +20 -0
  56. package/src/middleware/compose.ts +213 -0
  57. package/src/middleware/index.ts +37 -0
  58. package/src/middleware/production-hook.ts +47 -0
  59. package/src/middleware/with-cost-limit.ts +131 -0
  60. package/src/middleware/with-cost-tracking.ts +216 -0
  61. package/src/middleware/with-fallback.ts +157 -0
  62. package/src/middleware/with-rate-limit.ts +306 -0
  63. package/src/middleware/with-redaction.ts +671 -0
  64. package/src/middleware/with-retry.ts +274 -0
  65. package/src/middleware/with-tracing.ts +117 -0
  66. package/src/model-tier/classify.ts +125 -0
  67. package/src/model-tier/index.ts +11 -0
  68. package/src/provider.ts +121 -0
  69. package/src/reasoning/apply-policy.ts +89 -0
  70. package/src/reasoning/classify-contract.ts +120 -0
  71. package/src/reasoning/index.ts +21 -0
  72. package/src/reasoning/retention.ts +64 -0
  73. package/src/tool-examples.ts +54 -0
  74. package/src/trust/classify-local-provider.ts +254 -0
  75. package/src/trust/index.ts +14 -0
  76. package/dist/adapters/index.js +0 -6
@@ -82,7 +82,7 @@ function applyOpenAIShapedPreflight(req, capabilities) {
82
82
  };
83
83
  }
84
84
  async function* streamOpenAIShaped(opts, url, req) {
85
- const body = buildBody(opts.model, req, true, opts.capabilities?.structuredOutput ?? true);
85
+ const body = buildBody(opts.model, req, true, opts.capabilities?.structuredOutput ?? true, conversionOptionsFor(opts));
86
86
  const resp = await callJsonHttp({
87
87
  providerName: opts.providerName,
88
88
  url,
@@ -177,7 +177,7 @@ async function* streamOpenAIShaped(opts, url, req) {
177
177
  };
178
178
  }
179
179
  async function generateOpenAIShaped(opts, url, req) {
180
- const body = buildBody(opts.model, req, false, opts.capabilities?.structuredOutput ?? true);
180
+ const body = buildBody(opts.model, req, false, opts.capabilities?.structuredOutput ?? true, conversionOptionsFor(opts));
181
181
  const resp = await callJsonHttp({
182
182
  providerName: opts.providerName,
183
183
  url,
@@ -212,13 +212,13 @@ async function generateOpenAIShaped(opts, url, req) {
212
212
  ...toolCalls !== void 0 ? { toolCalls } : {}
213
213
  };
214
214
  }
215
- function buildBody(model, req, stream, structuredOutput) {
215
+ function buildBody(model, req, stream, structuredOutput, conversion) {
216
216
  const body = {
217
217
  model,
218
218
  messages: toOpenAIChatMessages(req.systemMessage !== void 0 ? [{
219
219
  role: "system",
220
220
  content: req.systemMessage
221
- }, ...req.messages] : req.messages),
221
+ }, ...req.messages] : req.messages, conversion),
222
222
  stream
223
223
  };
224
224
  if (stream) body.stream_options = { include_usage: true };
@@ -307,6 +307,23 @@ function defaultLogger(level, message, meta) {
307
307
  if (meta !== void 0) fn(`[graphorin/provider] ${message}`, meta);
308
308
  else fn(`[graphorin/provider] ${message}`);
309
309
  }
310
+ /**
311
+ * W-095: one dropped-content WARN per adapter INSTANCE (keyed on the
312
+ * factory's options object, following KNOWN_LOOPBACK_OVERRIDES_WARNED),
313
+ * so a chat loop does not repeat it every call.
314
+ */
315
+ const droppedContentWarned = /* @__PURE__ */ new WeakSet();
316
+ function conversionOptionsFor(opts) {
317
+ const log = opts.logger ?? defaultLogger;
318
+ return {
319
+ multimodal: opts.capabilities?.multimodal ?? false,
320
+ warn: (message) => {
321
+ if (droppedContentWarned.has(opts)) return;
322
+ droppedContentWarned.add(opts);
323
+ log("warn", message);
324
+ }
325
+ };
326
+ }
310
327
 
311
328
  //#endregion
312
329
  export { buildOpenAIShapedProvider };
@@ -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 { 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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"}
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 {\n type ChatMessageConversionOptions,\n callJsonHttp,\n makeStreamStartEvent,\n toOpenAIChatMessages,\n} 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(\n opts.model,\n req,\n true,\n opts.capabilities?.structuredOutput ?? true,\n conversionOptionsFor(opts),\n );\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(\n opts.model,\n req,\n false,\n opts.capabilities?.structuredOutput ?? true,\n conversionOptionsFor(opts),\n );\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 conversion: ChatMessageConversionOptions,\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, conversion),\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\n/**\n * W-095: one dropped-content WARN per adapter INSTANCE (keyed on the\n * factory's options object, following KNOWN_LOOPBACK_OVERRIDES_WARNED),\n * so a chat loop does not repeat it every call.\n */\nconst droppedContentWarned = new WeakSet<object>();\n\nfunction conversionOptionsFor(opts: {\n readonly capabilities?: { readonly multimodal?: boolean };\n readonly logger?: (level: 'warn' | 'info', message: string, meta?: object) => void;\n}): ChatMessageConversionOptions {\n const log = opts.logger ?? defaultLogger;\n return {\n multimodal: opts.capabilities?.multimodal ?? false,\n warn: (message) => {\n if (droppedContentWarned.has(opts)) return;\n droppedContentWarned.add(opts);\n log('warn', message);\n },\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":";;;;;;;;;;AAyEA,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,UACX,KAAK,OACL,KACA,MACA,KAAK,cAAc,oBAAoB,MACvC,qBAAqB,KAAK,CAC3B;CACD,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,UACX,KAAK,OACL,KACA,OACA,KAAK,cAAc,oBAAoB,MACvC,qBAAqB,KAAK,CAC3B;CACD,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,kBACA,YACyB;CAKzB,MAAMC,OAAgC;EACpC;EACA,UAAU,qBALV,IAAI,kBAAkB,SAClB,CAAC;GAAE,MAAM;GAAmB,SAAS,IAAI;GAAe,EAAE,GAAG,IAAI,SAAS,GAC1E,IAAI,UAGiC,WAAW;EACpD;EACD;AACD,KAAI,OAOF,MAAK,iBAAiB,EAAE,eAAe,MAAM;AAE/C,KAAI,IAAI,gBAAgB,OAAW,MAAK,cAAc,IAAI;AAC1D,KAAI,IAAI,cAAc,OAAW,MAAK,aAAa,IAAI;AACvD,KAAI,IAAI,UAAU,UAAa,IAAI,MAAM,SAAS,EAGhD,MAAK,QAAQ,iBAAiB,IAAI,MAAM,CAAC,KAAK,OAAO;EACnD,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;CASV,MAAM,eAAe,EAAE,iBAAiB;CACxC,MAAM,mBAAmB,EAAE,qBAAqB;CAChD,MAAM,cAAc,EAAE,gBAAgB,eAAe;CACrD,MAAM,eAAe,EAAE,uBAAuB;AAU9C,QATqB;EACnB;EACA;EACA;EACA,GAAI,EAAE,qBAAqB,SAAY,EAAE,iBAAiB,EAAE,kBAAkB,GAAG,EAAE;EACnF,GAAI,OAAO,iBAAiB,YAAY,OAAO,SAAS,aAAa,IAAI,eAAe,IACpF,EAAE,kBAAkB,cAAc,GAClC,EAAE;EACP;;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;;;;;;;AASzC,MAAM,uCAAuB,IAAI,SAAiB;AAElD,SAAS,qBAAqB,MAGG;CAC/B,MAAM,MAAM,KAAK,UAAU;AAC3B,QAAO;EACL,YAAY,KAAK,cAAc,cAAc;EAC7C,OAAO,YAAY;AACjB,OAAI,qBAAqB,IAAI,KAAK,CAAE;AACpC,wBAAqB,IAAI,KAAK;AAC9B,OAAI,QAAQ,QAAQ;;EAEvB"}
@@ -1,5 +1,5 @@
1
- import { defineProviderMiddleware } from "./compose.js";
2
1
  import { isAbortError } from "../internal/abort.js";
2
+ import { defineProviderMiddleware } from "./compose.js";
3
3
 
4
4
  //#region src/middleware/with-fallback.ts
5
5
  /**
@@ -1,15 +1,8 @@
1
1
  import * as _graphorin_core4 from "@graphorin/core";
2
+ import { ProviderRequest } from "@graphorin/core";
2
3
 
3
4
  //#region src/middleware/with-rate-limit.d.ts
4
5
 
5
- /**
6
- * `withRateLimit` - token-bucket rate limiter keyed per `provider × model`.
7
- * Configurable mode: `'throw'` (default) raises
8
- * {@link RateLimitExceededError} on overflow; `'queue'` waits for the
9
- * next available slot.
10
- *
11
- * @packageDocumentation
12
- */
13
6
  /**
14
7
  * Options for {@link withRateLimit}.
15
8
  *
@@ -20,6 +13,26 @@ interface WithRateLimitOptions {
20
13
  readonly requestsPerMinute: number;
21
14
  /** Burst size - defaults to `requestsPerMinute / 4` (rounded up to >= 1). */
22
15
  readonly burst?: number;
16
+ /**
17
+ * Optional token budget per minute (W-145). When set, each request
18
+ * additionally reserves its estimated token weight from a second
19
+ * bucket whose capacity is the full minute budget; a request whose
20
+ * weight exceeds the remaining budget waits (queue mode) or throws
21
+ * with the TPM-aware `retryAfterMs` (throw mode) even when the RPM
22
+ * bucket has room. Unset: behaviour is byte-identical to the
23
+ * RPM-only limiter.
24
+ */
25
+ readonly tokensPerMinute?: number;
26
+ /**
27
+ * Estimator for a request's token weight (only consulted when
28
+ * `tokensPerMinute` is set). The default is the deliberate cheap
29
+ * heuristic `ceil(textChars / 4) + (maxTokens ?? 0)` - synchronous
30
+ * and allocation-free, because this runs in the request hot path and
31
+ * must not add latency or network. Wire the counter from
32
+ * `@graphorin/provider/counters` (`createDefaultCounter`) here when
33
+ * you need provider-accurate weights.
34
+ */
35
+ readonly estimateTokens?: (req: ProviderRequest) => number;
23
36
  /** What to do on overflow. Default `'throw'`. */
24
37
  readonly mode?: 'throw' | 'queue';
25
38
  /** Test hook overriding `Date.now`. */
@@ -1 +1 @@
1
- {"version":3,"file":"with-rate-limit.d.ts","names":[],"sources":["../../src/middleware/with-rate-limit.ts"],"sourcesContent":[],"mappings":";;;;;;;AAmBA;AAgCA;;;;;;;;;UAhCiB,oBAAA;;;;;;;;;;6CAU4B,gBAAgB;;;;;cAsBhD,sBAAa,yBAAA,gBAAA,CAAA"}
1
+ {"version":3,"file":"with-rate-limit.d.ts","names":[],"sources":["../../src/middleware/with-rate-limit.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;UA0BiB,oBAAA;;;;;;;;;;;;;;;;;;;;;;;;kCAwBiB;;;;;;6CAMW,gBAAgB;;;;;cA2BhD,sBAAa,yBAAA,gBAAA,CAAA"}
@@ -9,8 +9,12 @@ const withRateLimit = defineProviderMiddleware({
9
9
  kind: "withRateLimit",
10
10
  factory: (opts) => {
11
11
  if (!(opts.requestsPerMinute > 0)) throw new RangeError("withRateLimit: requestsPerMinute must be > 0.");
12
+ if (opts.tokensPerMinute !== void 0 && !(opts.tokensPerMinute > 0)) throw new RangeError("withRateLimit: tokensPerMinute must be > 0 when set.");
12
13
  const burst = Math.max(opts.burst ?? Math.ceil(opts.requestsPerMinute / 4), 1);
13
14
  const refillPerMs = opts.requestsPerMinute / 6e4;
15
+ const tpm = opts.tokensPerMinute;
16
+ const tpmRefillPerMs = tpm === void 0 ? 0 : tpm / 6e4;
17
+ const estimate = opts.estimateTokens ?? estimateTokensHeuristic;
14
18
  const mode = opts.mode ?? "throw";
15
19
  const now = opts.nowImpl ?? (() => Date.now());
16
20
  const sleep = opts.sleepImpl ?? defaultSleep;
@@ -23,6 +27,8 @@ const withRateLimit = defineProviderMiddleware({
23
27
  bucket = {
24
28
  tokens: burst,
25
29
  lastRefill: now(),
30
+ tpmTokens: tpm ?? 0,
31
+ tpmLastRefill: now(),
26
32
  queue: [],
27
33
  draining: false
28
34
  };
@@ -34,6 +40,34 @@ const withRateLimit = defineProviderMiddleware({
34
40
  const ts = now();
35
41
  bucket.tokens = Math.min(burst, bucket.tokens + (ts - bucket.lastRefill) * refillPerMs);
36
42
  bucket.lastRefill = ts;
43
+ if (tpm !== void 0) {
44
+ bucket.tpmTokens = Math.min(tpm, bucket.tpmTokens + (ts - bucket.tpmLastRefill) * tpmRefillPerMs);
45
+ bucket.tpmLastRefill = ts;
46
+ }
47
+ };
48
+ /**
49
+ * A request's TPM weight, clamped to the bucket capacity - a
50
+ * single request estimated above the whole minute budget must
51
+ * degrade to "wait for a full bucket", not deadlock the queue
52
+ * forever behind an unsatisfiable reservation.
53
+ */
54
+ const weightFor = (req) => {
55
+ if (tpm === void 0) return 0;
56
+ const raw = estimate(req);
57
+ return Math.min(Math.max(0, Number.isFinite(raw) ? raw : tpm), tpm);
58
+ };
59
+ /** Both dimensions available for this weight? */
60
+ const available = (bucket, weight) => bucket.tokens >= 1 && (tpm === void 0 || bucket.tpmTokens >= weight);
61
+ /** Wait until BOTH dimensions can serve `weight`, in ms. */
62
+ const waitMsFor = (bucket, weight) => {
63
+ const reqWait = bucket.tokens >= 1 ? 0 : (1 - bucket.tokens) / refillPerMs;
64
+ const tpmWait = tpm === void 0 || bucket.tpmTokens >= weight ? 0 : (weight - bucket.tpmTokens) / tpmRefillPerMs;
65
+ return Math.ceil(Math.max(reqWait, tpmWait));
66
+ };
67
+ /** Deduct BOTH dimensions atomically (caller checked `available`). */
68
+ const take = (bucket, weight) => {
69
+ bucket.tokens -= 1;
70
+ if (tpm !== void 0) bucket.tpmTokens -= weight;
37
71
  };
38
72
  const drain = (bucket) => {
39
73
  if (bucket.draining) return;
@@ -47,13 +81,13 @@ const withRateLimit = defineProviderMiddleware({
47
81
  continue;
48
82
  }
49
83
  refill(bucket);
50
- if (bucket.tokens >= 1) {
51
- bucket.tokens -= 1;
84
+ if (available(bucket, head.weight)) {
85
+ take(bucket, head.weight);
52
86
  bucket.queue.shift();
53
87
  head.resolve();
54
88
  continue;
55
89
  }
56
- const waitMs = Math.ceil((1 - bucket.tokens) / refillPerMs);
90
+ const waitMs = waitMsFor(bucket, head.weight);
57
91
  try {
58
92
  await sleep(waitMs);
59
93
  } catch {}
@@ -61,21 +95,22 @@ const withRateLimit = defineProviderMiddleware({
61
95
  bucket.draining = false;
62
96
  })();
63
97
  };
64
- const acquire = async (signal) => {
98
+ const acquire = async (signal, weight = 0) => {
65
99
  if (signal?.aborted === true) throw signal.reason ?? /* @__PURE__ */ new Error("aborted");
66
100
  const bucket = bucketFor();
67
101
  refill(bucket);
68
- if (bucket.tokens >= 1 && bucket.queue.length === 0) {
69
- bucket.tokens -= 1;
102
+ if (available(bucket, weight) && bucket.queue.length === 0) {
103
+ take(bucket, weight);
70
104
  return;
71
105
  }
72
- if (mode === "throw") throw new RateLimitExceededError(Math.ceil((1 - bucket.tokens) / refillPerMs));
106
+ if (mode === "throw") throw new RateLimitExceededError(waitMsFor(bucket, weight));
73
107
  await new Promise((resolve, reject) => {
74
108
  const waiter = {
75
109
  resolve,
76
110
  reject,
77
111
  ...signal ? { signal } : {},
78
- cancelled: false
112
+ cancelled: false,
113
+ weight
79
114
  };
80
115
  if (signal !== void 0) {
81
116
  const onAbort = () => {
@@ -99,10 +134,10 @@ const withRateLimit = defineProviderMiddleware({
99
134
  capabilities: next.capabilities,
100
135
  ...next.acceptsSensitivity !== void 0 ? { acceptsSensitivity: next.acceptsSensitivity } : {},
101
136
  stream(req) {
102
- return rateLimitedStream(next, req, acquire);
137
+ return rateLimitedStream(next, req, acquire, weightFor(req));
103
138
  },
104
139
  async generate(req) {
105
- await acquire(req.signal);
140
+ await acquire(req.signal, weightFor(req));
106
141
  return next.generate(req);
107
142
  },
108
143
  ...next.countTokens ? { countTokens: next.countTokens.bind(next) } : {}
@@ -110,8 +145,26 @@ const withRateLimit = defineProviderMiddleware({
110
145
  };
111
146
  }
112
147
  });
113
- async function* rateLimitedStream(next, req, acquire) {
114
- await acquire(req.signal);
148
+ /**
149
+ * Default TPM weight heuristic: `ceil(textChars / 4) + (maxTokens ?? 0)`.
150
+ * Counts string contents and the `text` of content parts plus the
151
+ * system message; images / other parts are not weighed (heuristic by
152
+ * design - see {@link WithRateLimitOptions.estimateTokens}).
153
+ */
154
+ function estimateTokensHeuristic(req) {
155
+ let chars = req.systemMessage?.length ?? 0;
156
+ for (const message of req.messages) {
157
+ const content = message.content;
158
+ if (typeof content === "string") chars += content.length;
159
+ else if (Array.isArray(content)) for (const part of content) {
160
+ const text = part.text;
161
+ if (typeof text === "string") chars += text.length;
162
+ }
163
+ }
164
+ return Math.ceil(chars / 4) + (req.maxTokens ?? 0);
165
+ }
166
+ async function* rateLimitedStream(next, req, acquire, weight) {
167
+ await acquire(req.signal, weight);
115
168
  for await (const event of next.stream(req)) yield event;
116
169
  }
117
170
  function defaultSleep(ms, signal) {
@@ -1 +1 @@
1
- {"version":3,"file":"with-rate-limit.js","names":["waiter: Waiter"],"sources":["../../src/middleware/with-rate-limit.ts"],"sourcesContent":["/**\n * `withRateLimit` - token-bucket rate limiter keyed per `provider × model`.\n * Configurable mode: `'throw'` (default) raises\n * {@link RateLimitExceededError} on overflow; `'queue'` waits for the\n * next available slot.\n *\n * @packageDocumentation\n */\n\nimport type { Provider } from '@graphorin/core';\n\nimport { RateLimitExceededError } from '../errors/errors.js';\nimport { defineProviderMiddleware } from './compose.js';\n\n/**\n * Options for {@link withRateLimit}.\n *\n * @stable\n */\nexport interface WithRateLimitOptions {\n /** Allowed requests per minute. */\n readonly requestsPerMinute: number;\n /** Burst size - defaults to `requestsPerMinute / 4` (rounded up to >= 1). */\n readonly burst?: number;\n /** What to do on overflow. Default `'throw'`. */\n readonly mode?: 'throw' | 'queue';\n /** Test hook overriding `Date.now`. */\n readonly nowImpl?: () => number;\n /** Test hook overriding `setTimeout`-based wait. */\n readonly sleepImpl?: (ms: number, signal?: AbortSignal) => Promise<void>;\n}\n\ninterface Waiter {\n resolve: () => void;\n reject: (err: unknown) => void;\n signal?: AbortSignal;\n cancelled: boolean;\n}\n\ninterface BucketState {\n tokens: number;\n lastRefill: number;\n /** FIFO waiters in `'queue'` mode (PS-18). */\n queue: Waiter[];\n /** Whether the single drain loop for this bucket is running. */\n draining: boolean;\n}\n\n/**\n * @stable\n */\nexport const withRateLimit = defineProviderMiddleware<WithRateLimitOptions>({\n kind: 'withRateLimit',\n factory: (opts: WithRateLimitOptions) => {\n if (!(opts.requestsPerMinute > 0)) {\n throw new RangeError('withRateLimit: requestsPerMinute must be > 0.');\n }\n const burst = Math.max(opts.burst ?? Math.ceil(opts.requestsPerMinute / 4), 1);\n const refillPerMs = opts.requestsPerMinute / 60_000;\n const mode = opts.mode ?? 'throw';\n const now = opts.nowImpl ?? (() => Date.now());\n const sleep = opts.sleepImpl ?? defaultSleep;\n const buckets = new Map<string, BucketState>();\n return (next: Provider): Provider => {\n const key = `${next.name}::${next.modelId}`;\n const bucketFor = (): BucketState => {\n let bucket = buckets.get(key);\n if (bucket === undefined) {\n bucket = { tokens: burst, lastRefill: now(), queue: [], draining: false };\n buckets.set(key, bucket);\n }\n return bucket;\n };\n const refill = (bucket: BucketState): void => {\n const ts = now();\n bucket.tokens = Math.min(burst, bucket.tokens + (ts - bucket.lastRefill) * refillPerMs);\n bucket.lastRefill = ts;\n };\n // PS-18: a single drain loop per bucket grants queued waiters ONE token at\n // a time, sleeping until the next token actually refills. Previously every\n // concurrent waiter computed the same wait, slept once, and then all\n // passed - letting N requests through on ~1 token (a burst). FIFO order is\n // preserved and grants track the real refill schedule.\n const drain = (bucket: BucketState): void => {\n if (bucket.draining) return;\n bucket.draining = true;\n void (async () => {\n while (bucket.queue.length > 0) {\n const head = bucket.queue[0];\n if (head === undefined) break;\n if (head.cancelled) {\n bucket.queue.shift();\n continue;\n }\n refill(bucket);\n if (bucket.tokens >= 1) {\n bucket.tokens -= 1;\n bucket.queue.shift();\n head.resolve();\n continue;\n }\n const waitMs = Math.ceil((1 - bucket.tokens) / refillPerMs);\n try {\n await sleep(waitMs);\n } catch {\n // The shared drain sleep carries no per-waiter signal; individual\n // aborts are handled on the waiter via its listener.\n }\n }\n bucket.draining = false;\n })();\n };\n const acquire = async (signal?: AbortSignal): Promise<void> => {\n if (signal?.aborted === true) throw signal.reason ?? new Error('aborted');\n const bucket = bucketFor();\n refill(bucket);\n // Fast path: a token is free AND nobody is queued ahead (preserve FIFO).\n if (bucket.tokens >= 1 && bucket.queue.length === 0) {\n bucket.tokens -= 1;\n return;\n }\n if (mode === 'throw') {\n throw new RateLimitExceededError(Math.ceil((1 - bucket.tokens) / refillPerMs));\n }\n await new Promise<void>((resolve, reject) => {\n const waiter: Waiter = {\n resolve,\n reject,\n ...(signal ? { signal } : {}),\n cancelled: false,\n };\n if (signal !== undefined) {\n const onAbort = (): void => {\n waiter.cancelled = true;\n reject(signal.reason ?? new Error('aborted'));\n };\n signal.addEventListener('abort', onAbort, { once: true });\n const settle = resolve;\n waiter.resolve = (): void => {\n signal.removeEventListener('abort', onAbort);\n settle();\n };\n }\n bucket.queue.push(waiter);\n drain(bucket);\n });\n };\n return {\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 rateLimitedStream(next, req, acquire);\n },\n async generate(req) {\n await acquire(req.signal);\n return next.generate(req);\n },\n ...(next.countTokens ? { countTokens: next.countTokens.bind(next) } : {}),\n };\n };\n },\n});\n\nasync function* rateLimitedStream(\n next: Provider,\n req: import('@graphorin/core').ProviderRequest,\n acquire: (signal?: AbortSignal) => Promise<void>,\n): AsyncIterable<import('@graphorin/core').ProviderEvent> {\n await acquire(req.signal);\n for await (const event of next.stream(req)) {\n yield event;\n }\n}\n\nfunction defaultSleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n if (signal?.aborted) {\n reject(signal.reason ?? new Error('aborted'));\n return;\n }\n const timer = setTimeout(() => {\n cleanup();\n resolve();\n }, ms);\n const onAbort = () => {\n cleanup();\n reject(signal?.reason ?? new Error('aborted'));\n };\n function cleanup(): void {\n clearTimeout(timer);\n signal?.removeEventListener('abort', onAbort);\n }\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"with-rate-limit.js","names":["waiter: Waiter","content: unknown"],"sources":["../../src/middleware/with-rate-limit.ts"],"sourcesContent":["/**\n * `withRateLimit` - token-bucket rate limiter keyed per `provider × model`.\n * Configurable mode: `'throw'` (default) raises\n * {@link RateLimitExceededError} on overflow; `'queue'` waits for the\n * next available slot.\n *\n * W-145: an optional second dimension, `tokensPerMinute`, budgets\n * MODEL tokens alongside requests. For agentic workloads the binding\n * provider limit is TPM, not RPM - a 150k-token compacted transcript\n * used to occupy the same single slot as a 200-token reranker call,\n * so bursts of long-context steps sailed through the RPM gate and\n * surfaced as provider 429s that only `withRetry` absorbed.\n *\n * @packageDocumentation\n */\n\nimport type { Provider, ProviderRequest } from '@graphorin/core';\n\nimport { RateLimitExceededError } from '../errors/errors.js';\nimport { defineProviderMiddleware } from './compose.js';\n\n/**\n * Options for {@link withRateLimit}.\n *\n * @stable\n */\nexport interface WithRateLimitOptions {\n /** Allowed requests per minute. */\n readonly requestsPerMinute: number;\n /** Burst size - defaults to `requestsPerMinute / 4` (rounded up to >= 1). */\n readonly burst?: number;\n /**\n * Optional token budget per minute (W-145). When set, each request\n * additionally reserves its estimated token weight from a second\n * bucket whose capacity is the full minute budget; a request whose\n * weight exceeds the remaining budget waits (queue mode) or throws\n * with the TPM-aware `retryAfterMs` (throw mode) even when the RPM\n * bucket has room. Unset: behaviour is byte-identical to the\n * RPM-only limiter.\n */\n readonly tokensPerMinute?: number;\n /**\n * Estimator for a request's token weight (only consulted when\n * `tokensPerMinute` is set). The default is the deliberate cheap\n * heuristic `ceil(textChars / 4) + (maxTokens ?? 0)` - synchronous\n * and allocation-free, because this runs in the request hot path and\n * must not add latency or network. Wire the counter from\n * `@graphorin/provider/counters` (`createDefaultCounter`) here when\n * you need provider-accurate weights.\n */\n readonly estimateTokens?: (req: ProviderRequest) => number;\n /** What to do on overflow. Default `'throw'`. */\n readonly mode?: 'throw' | 'queue';\n /** Test hook overriding `Date.now`. */\n readonly nowImpl?: () => number;\n /** Test hook overriding `setTimeout`-based wait. */\n readonly sleepImpl?: (ms: number, signal?: AbortSignal) => Promise<void>;\n}\n\ninterface Waiter {\n resolve: () => void;\n reject: (err: unknown) => void;\n signal?: AbortSignal;\n cancelled: boolean;\n /** TPM weight this waiter must reserve (0 when TPM is unset). */\n weight: number;\n}\n\ninterface BucketState {\n tokens: number;\n lastRefill: number;\n /** W-145: remaining TPM budget (only meaningful when TPM is set). */\n tpmTokens: number;\n tpmLastRefill: number;\n /** FIFO waiters in `'queue'` mode (PS-18). */\n queue: Waiter[];\n /** Whether the single drain loop for this bucket is running. */\n draining: boolean;\n}\n\n/**\n * @stable\n */\nexport const withRateLimit = defineProviderMiddleware<WithRateLimitOptions>({\n kind: 'withRateLimit',\n factory: (opts: WithRateLimitOptions) => {\n if (!(opts.requestsPerMinute > 0)) {\n throw new RangeError('withRateLimit: requestsPerMinute must be > 0.');\n }\n if (opts.tokensPerMinute !== undefined && !(opts.tokensPerMinute > 0)) {\n throw new RangeError('withRateLimit: tokensPerMinute must be > 0 when set.');\n }\n const burst = Math.max(opts.burst ?? Math.ceil(opts.requestsPerMinute / 4), 1);\n const refillPerMs = opts.requestsPerMinute / 60_000;\n const tpm = opts.tokensPerMinute;\n const tpmRefillPerMs = tpm === undefined ? 0 : tpm / 60_000;\n const estimate = opts.estimateTokens ?? estimateTokensHeuristic;\n const mode = opts.mode ?? 'throw';\n const now = opts.nowImpl ?? (() => Date.now());\n const sleep = opts.sleepImpl ?? defaultSleep;\n const buckets = new Map<string, BucketState>();\n return (next: Provider): Provider => {\n const key = `${next.name}::${next.modelId}`;\n const bucketFor = (): BucketState => {\n let bucket = buckets.get(key);\n if (bucket === undefined) {\n bucket = {\n tokens: burst,\n lastRefill: now(),\n tpmTokens: tpm ?? 0,\n tpmLastRefill: now(),\n queue: [],\n draining: false,\n };\n buckets.set(key, bucket);\n }\n return bucket;\n };\n const refill = (bucket: BucketState): void => {\n const ts = now();\n bucket.tokens = Math.min(burst, bucket.tokens + (ts - bucket.lastRefill) * refillPerMs);\n bucket.lastRefill = ts;\n if (tpm !== undefined) {\n // TPM bucket capacity = the full minute budget, so any\n // (clamped) weight is eventually satisfiable.\n bucket.tpmTokens = Math.min(\n tpm,\n bucket.tpmTokens + (ts - bucket.tpmLastRefill) * tpmRefillPerMs,\n );\n bucket.tpmLastRefill = ts;\n }\n };\n /**\n * A request's TPM weight, clamped to the bucket capacity - a\n * single request estimated above the whole minute budget must\n * degrade to \"wait for a full bucket\", not deadlock the queue\n * forever behind an unsatisfiable reservation.\n */\n const weightFor = (req: ProviderRequest): number => {\n if (tpm === undefined) return 0;\n const raw = estimate(req);\n return Math.min(Math.max(0, Number.isFinite(raw) ? raw : tpm), tpm);\n };\n /** Both dimensions available for this weight? */\n const available = (bucket: BucketState, weight: number): boolean =>\n bucket.tokens >= 1 && (tpm === undefined || bucket.tpmTokens >= weight);\n /** Wait until BOTH dimensions can serve `weight`, in ms. */\n const waitMsFor = (bucket: BucketState, weight: number): number => {\n const reqWait = bucket.tokens >= 1 ? 0 : (1 - bucket.tokens) / refillPerMs;\n const tpmWait =\n tpm === undefined || bucket.tpmTokens >= weight\n ? 0\n : (weight - bucket.tpmTokens) / tpmRefillPerMs;\n return Math.ceil(Math.max(reqWait, tpmWait));\n };\n /** Deduct BOTH dimensions atomically (caller checked `available`). */\n const take = (bucket: BucketState, weight: number): void => {\n bucket.tokens -= 1;\n if (tpm !== undefined) bucket.tpmTokens -= weight;\n };\n // PS-18: a single drain loop per bucket grants queued waiters ONE slot at\n // a time, sleeping until the next token actually refills. Previously every\n // concurrent waiter computed the same wait, slept once, and then all\n // passed - letting N requests through on ~1 token (a burst). FIFO order is\n // preserved and grants track the real refill schedule. W-145: a grant now\n // requires BOTH the request token and the head's TPM weight.\n const drain = (bucket: BucketState): void => {\n if (bucket.draining) return;\n bucket.draining = true;\n void (async () => {\n while (bucket.queue.length > 0) {\n const head = bucket.queue[0];\n if (head === undefined) break;\n if (head.cancelled) {\n bucket.queue.shift();\n continue;\n }\n refill(bucket);\n if (available(bucket, head.weight)) {\n take(bucket, head.weight);\n bucket.queue.shift();\n head.resolve();\n continue;\n }\n const waitMs = waitMsFor(bucket, head.weight);\n try {\n await sleep(waitMs);\n } catch {\n // The shared drain sleep carries no per-waiter signal; individual\n // aborts are handled on the waiter via its listener.\n }\n }\n bucket.draining = false;\n })();\n };\n const acquire = async (signal?: AbortSignal, weight = 0): Promise<void> => {\n if (signal?.aborted === true) throw signal.reason ?? new Error('aborted');\n const bucket = bucketFor();\n refill(bucket);\n // Fast path: both dimensions free AND nobody queued ahead (FIFO).\n if (available(bucket, weight) && bucket.queue.length === 0) {\n take(bucket, weight);\n return;\n }\n if (mode === 'throw') {\n throw new RateLimitExceededError(waitMsFor(bucket, weight));\n }\n await new Promise<void>((resolve, reject) => {\n const waiter: Waiter = {\n resolve,\n reject,\n ...(signal ? { signal } : {}),\n cancelled: false,\n weight,\n };\n if (signal !== undefined) {\n const onAbort = (): void => {\n waiter.cancelled = true;\n reject(signal.reason ?? new Error('aborted'));\n };\n signal.addEventListener('abort', onAbort, { once: true });\n const settle = resolve;\n waiter.resolve = (): void => {\n signal.removeEventListener('abort', onAbort);\n settle();\n };\n }\n bucket.queue.push(waiter);\n drain(bucket);\n });\n };\n return {\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 rateLimitedStream(next, req, acquire, weightFor(req));\n },\n async generate(req) {\n await acquire(req.signal, weightFor(req));\n return next.generate(req);\n },\n ...(next.countTokens ? { countTokens: next.countTokens.bind(next) } : {}),\n };\n };\n },\n});\n\n/**\n * Default TPM weight heuristic: `ceil(textChars / 4) + (maxTokens ?? 0)`.\n * Counts string contents and the `text` of content parts plus the\n * system message; images / other parts are not weighed (heuristic by\n * design - see {@link WithRateLimitOptions.estimateTokens}).\n */\nfunction estimateTokensHeuristic(req: ProviderRequest): number {\n let chars = req.systemMessage?.length ?? 0;\n for (const message of req.messages) {\n const content: unknown = message.content;\n if (typeof content === 'string') {\n chars += content.length;\n } else if (Array.isArray(content)) {\n for (const part of content) {\n const text = (part as { readonly text?: unknown }).text;\n if (typeof text === 'string') chars += text.length;\n }\n }\n }\n return Math.ceil(chars / 4) + (req.maxTokens ?? 0);\n}\n\nasync function* rateLimitedStream(\n next: Provider,\n req: import('@graphorin/core').ProviderRequest,\n acquire: (signal?: AbortSignal, weight?: number) => Promise<void>,\n weight: number,\n): AsyncIterable<import('@graphorin/core').ProviderEvent> {\n await acquire(req.signal, weight);\n for await (const event of next.stream(req)) {\n yield event;\n }\n}\n\nfunction defaultSleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n if (signal?.aborted) {\n reject(signal.reason ?? new Error('aborted'));\n return;\n }\n const timer = setTimeout(() => {\n cleanup();\n resolve();\n }, ms);\n const onAbort = () => {\n cleanup();\n reject(signal?.reason ?? new Error('aborted'));\n };\n function cleanup(): void {\n clearTimeout(timer);\n signal?.removeEventListener('abort', onAbort);\n }\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
- import { defineProviderMiddleware } from "./compose.js";
2
1
  import { isAbortError } from "../internal/abort.js";
2
+ import { defineProviderMiddleware } from "./compose.js";
3
3
 
4
4
  //#region src/middleware/with-retry.ts
5
5
  /**
@@ -0,0 +1,6 @@
1
+ //#region package.json
2
+ var version = "0.7.0";
3
+
4
+ //#endregion
5
+ export { version };
6
+ //# sourceMappingURL=package.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"package.js","names":[],"sources":["../package.json"],"sourcesContent":["{\n \"name\": \"@graphorin/provider\",\n \"version\": \"0.7.0\",\n \"description\": \"Vendor-neutral LLM provider layer for the Graphorin framework. Ships the canonical Provider interface, the Vercel AI SDK adapter (default cloud path), three local-LLM adapters (direct Ollama HTTP, llama-server HTTP, and a generic OpenAI-compatible adapter for LMStudio / LocalAI / vLLM / Together-style endpoints), the shared LocalProviderTrust classifier (loopback / private / public-tls / public-cleartext), the canonical-order middleware composer (withTracing / withRetry / withRateLimit / withCostLimit / withCostTracking / withFallback / withRedaction), the pluggable TokenCounter dispatcher (js-tiktoken default plus per-vendor native counters and a heuristic fallback), the per-provider model-tier auto-classifier (fast / balanced / smart), and the production startup hook that fails fast when withRedaction is missing.\",\n \"license\": \"MIT\",\n \"author\": \"Oleksiy Stepurenko\",\n \"homepage\": \"https://github.com/o-stepper/graphorin/tree/main/packages/provider\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/o-stepper/graphorin.git\",\n \"directory\": \"packages/provider\"\n },\n \"bugs\": {\n \"url\": \"https://github.com/o-stepper/graphorin/issues\"\n },\n \"keywords\": [\n \"graphorin\",\n \"ai\",\n \"agents\",\n \"framework\",\n \"provider\",\n \"llm\",\n \"vercel-ai-sdk\",\n \"ollama\",\n \"llama-cpp\",\n \"openai-compatible\",\n \"middleware\",\n \"retry\",\n \"fallback\",\n \"rate-limit\",\n \"cost-limit\",\n \"redaction\",\n \"token-counter\",\n \"tiktoken\",\n \"local-first\"\n ],\n \"type\": \"module\",\n \"sideEffects\": false,\n \"engines\": {\n \"node\": \">=22.12.0\"\n },\n \"main\": \"./dist/index.js\",\n \"module\": \"./dist/index.js\",\n \"types\": \"./dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"default\": \"./dist/index.js\"\n },\n \"./adapters/vercel\": {\n \"types\": \"./dist/adapters/vercel.d.ts\",\n \"default\": \"./dist/adapters/vercel.js\"\n },\n \"./adapters/ollama\": {\n \"types\": \"./dist/adapters/ollama.d.ts\",\n \"default\": \"./dist/adapters/ollama.js\"\n },\n \"./adapters/llamacpp-server\": {\n \"types\": \"./dist/adapters/llamacpp-server.d.ts\",\n \"default\": \"./dist/adapters/llamacpp-server.js\"\n },\n \"./adapters/openai-compatible\": {\n \"types\": \"./dist/adapters/openai-compatible.d.ts\",\n \"default\": \"./dist/adapters/openai-compatible.js\"\n },\n \"./middleware\": {\n \"types\": \"./dist/middleware/index.d.ts\",\n \"default\": \"./dist/middleware/index.js\"\n },\n \"./counters\": {\n \"types\": \"./dist/counters/index.d.ts\",\n \"default\": \"./dist/counters/index.js\"\n },\n \"./trust\": {\n \"types\": \"./dist/trust/index.d.ts\",\n \"default\": \"./dist/trust/index.js\"\n },\n \"./model-tier\": {\n \"types\": \"./dist/model-tier/index.d.ts\",\n \"default\": \"./dist/model-tier/index.js\"\n },\n \"./reasoning\": {\n \"types\": \"./dist/reasoning/index.d.ts\",\n \"default\": \"./dist/reasoning/index.js\"\n },\n \"./errors\": {\n \"types\": \"./dist/errors/index.d.ts\",\n \"default\": \"./dist/errors/index.js\"\n },\n \"./package.json\": \"./package.json\"\n },\n \"files\": [\n \"dist\",\n \"src\",\n \"README.md\",\n \"CHANGELOG.md\",\n \"LICENSE\"\n ],\n \"scripts\": {\n \"build\": \"tsdown\",\n \"typecheck\": \"tsc --noEmit && tsc -p tsconfig.tests.json\",\n \"test\": \"vitest run\",\n \"lint\": \"biome check .\",\n \"clean\": \"rimraf dist .turbo *.tsbuildinfo\"\n },\n \"dependencies\": {\n \"@graphorin/core\": \"workspace:*\",\n \"@graphorin/observability\": \"workspace:*\"\n },\n \"peerDependencies\": {\n \"ai\": \"^7.0.0-beta.76\",\n \"js-tiktoken\": \"^1.0.0\"\n },\n \"peerDependenciesMeta\": {\n \"ai\": {\n \"optional\": true\n },\n \"js-tiktoken\": {\n \"optional\": true\n }\n },\n \"publishConfig\": {\n \"access\": \"public\",\n \"provenance\": true\n },\n \"devDependencies\": {\n \"@graphorin/pricing\": \"workspace:*\",\n \"ai\": \"^7.0.15\",\n \"fast-check\": \"^3.23.0\",\n \"js-tiktoken\": \"^1.0.21\"\n }\n}\n"],"mappings":";cAEa"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graphorin/provider",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Vendor-neutral LLM provider layer for the Graphorin framework. Ships the canonical Provider interface, the Vercel AI SDK adapter (default cloud path), three local-LLM adapters (direct Ollama HTTP, llama-server HTTP, and a generic OpenAI-compatible adapter for LMStudio / LocalAI / vLLM / Together-style endpoints), the shared LocalProviderTrust classifier (loopback / private / public-tls / public-cleartext), the canonical-order middleware composer (withTracing / withRetry / withRateLimit / withCostLimit / withCostTracking / withFallback / withRedaction), the pluggable TokenCounter dispatcher (js-tiktoken default plus per-vendor native counters and a heuristic fallback), the per-provider model-tier auto-classifier (fast / balanced / smart), and the production startup hook that fails fast when withRedaction is missing.",
5
5
  "license": "MIT",
6
6
  "author": "Oleksiy Stepurenko",
@@ -35,8 +35,9 @@
35
35
  "local-first"
36
36
  ],
37
37
  "type": "module",
38
+ "sideEffects": false,
38
39
  "engines": {
39
- "node": ">=22.0.0"
40
+ "node": ">=22.12.0"
40
41
  },
41
42
  "main": "./dist/index.js",
42
43
  "module": "./dist/index.js",
@@ -44,59 +45,60 @@
44
45
  "exports": {
45
46
  ".": {
46
47
  "types": "./dist/index.d.ts",
47
- "import": "./dist/index.js"
48
+ "default": "./dist/index.js"
48
49
  },
49
50
  "./adapters/vercel": {
50
51
  "types": "./dist/adapters/vercel.d.ts",
51
- "import": "./dist/adapters/vercel.js"
52
+ "default": "./dist/adapters/vercel.js"
52
53
  },
53
54
  "./adapters/ollama": {
54
55
  "types": "./dist/adapters/ollama.d.ts",
55
- "import": "./dist/adapters/ollama.js"
56
+ "default": "./dist/adapters/ollama.js"
56
57
  },
57
58
  "./adapters/llamacpp-server": {
58
59
  "types": "./dist/adapters/llamacpp-server.d.ts",
59
- "import": "./dist/adapters/llamacpp-server.js"
60
+ "default": "./dist/adapters/llamacpp-server.js"
60
61
  },
61
62
  "./adapters/openai-compatible": {
62
63
  "types": "./dist/adapters/openai-compatible.d.ts",
63
- "import": "./dist/adapters/openai-compatible.js"
64
+ "default": "./dist/adapters/openai-compatible.js"
64
65
  },
65
66
  "./middleware": {
66
67
  "types": "./dist/middleware/index.d.ts",
67
- "import": "./dist/middleware/index.js"
68
+ "default": "./dist/middleware/index.js"
68
69
  },
69
70
  "./counters": {
70
71
  "types": "./dist/counters/index.d.ts",
71
- "import": "./dist/counters/index.js"
72
+ "default": "./dist/counters/index.js"
72
73
  },
73
74
  "./trust": {
74
75
  "types": "./dist/trust/index.d.ts",
75
- "import": "./dist/trust/index.js"
76
+ "default": "./dist/trust/index.js"
76
77
  },
77
78
  "./model-tier": {
78
79
  "types": "./dist/model-tier/index.d.ts",
79
- "import": "./dist/model-tier/index.js"
80
+ "default": "./dist/model-tier/index.js"
80
81
  },
81
82
  "./reasoning": {
82
83
  "types": "./dist/reasoning/index.d.ts",
83
- "import": "./dist/reasoning/index.js"
84
+ "default": "./dist/reasoning/index.js"
84
85
  },
85
86
  "./errors": {
86
87
  "types": "./dist/errors/index.d.ts",
87
- "import": "./dist/errors/index.js"
88
+ "default": "./dist/errors/index.js"
88
89
  },
89
90
  "./package.json": "./package.json"
90
91
  },
91
92
  "files": [
92
93
  "dist",
94
+ "src",
93
95
  "README.md",
94
96
  "CHANGELOG.md",
95
97
  "LICENSE"
96
98
  ],
97
99
  "dependencies": {
98
- "@graphorin/core": "0.6.0",
99
- "@graphorin/observability": "0.6.0"
100
+ "@graphorin/core": "0.7.0",
101
+ "@graphorin/observability": "0.7.0"
100
102
  },
101
103
  "peerDependencies": {
102
104
  "ai": "^7.0.0-beta.76",
@@ -118,7 +120,7 @@
118
120
  "ai": "^7.0.15",
119
121
  "fast-check": "^3.23.0",
120
122
  "js-tiktoken": "^1.0.21",
121
- "@graphorin/pricing": "0.6.0"
123
+ "@graphorin/pricing": "0.7.0"
122
124
  },
123
125
  "scripts": {
124
126
  "build": "tsdown",
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Adapter barrel - re-exports the four bundled adapters.
3
+ *
4
+ * @packageDocumentation
5
+ */
6
+
7
+ export type { LlamaCppServerAdapterOptions } from './llamacpp-server.js';
8
+ export { llamaCppServerAdapter } from './llamacpp-server.js';
9
+ export type { OllamaAdapterOptions } from './ollama.js';
10
+ export { ollamaAdapter } from './ollama.js';
11
+ export type { OpenAICompatibleAdapterOptions } from './openai-compatible.js';
12
+ export { openAICompatibleAdapter } from './openai-compatible.js';
13
+ export type { VercelAdapterOptions } from './vercel.js';
14
+ export { vercelAdapter } from './vercel.js';