@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.
- package/CHANGELOG.md +42 -0
- package/README.md +8 -3
- package/dist/adapters/llamacpp-server.d.ts +1 -1
- package/dist/adapters/llamacpp-server.js.map +1 -1
- package/dist/adapters/ollama.d.ts.map +1 -1
- package/dist/adapters/ollama.js +19 -6
- package/dist/adapters/ollama.js.map +1 -1
- package/dist/adapters/vercel.d.ts +1 -1
- package/dist/adapters/vercel.d.ts.map +1 -1
- package/dist/adapters/vercel.js +111 -33
- package/dist/adapters/vercel.js.map +1 -1
- package/dist/errors/errors.d.ts +1 -1
- package/dist/errors/errors.js +1 -1
- package/dist/errors/errors.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -8
- package/dist/index.js.map +1 -1
- package/dist/internal/http.js +111 -7
- package/dist/internal/http.js.map +1 -1
- package/dist/internal/openai-shaped.js +21 -4
- package/dist/internal/openai-shaped.js.map +1 -1
- package/dist/middleware/with-fallback.js +1 -1
- package/dist/middleware/with-rate-limit.d.ts +21 -8
- package/dist/middleware/with-rate-limit.d.ts.map +1 -1
- package/dist/middleware/with-rate-limit.js +65 -12
- package/dist/middleware/with-rate-limit.js.map +1 -1
- package/dist/middleware/with-retry.js +1 -1
- package/dist/package.js +6 -0
- package/dist/package.js.map +1 -0
- package/package.json +18 -16
- package/src/adapters/index.ts +14 -0
- package/src/adapters/llamacpp-server.ts +102 -0
- package/src/adapters/ollama.ts +382 -0
- package/src/adapters/openai-compatible.ts +95 -0
- package/src/adapters/vercel-messages.ts +308 -0
- package/src/adapters/vercel.ts +706 -0
- package/src/counters/anthropic-wire.ts +199 -0
- package/src/counters/anthropic.ts +114 -0
- package/src/counters/bedrock.ts +46 -0
- package/src/counters/dispatcher.ts +127 -0
- package/src/counters/global.ts +39 -0
- package/src/counters/google.ts +46 -0
- package/src/counters/heuristic.ts +107 -0
- package/src/counters/index.ts +35 -0
- package/src/counters/js-tiktoken.ts +135 -0
- package/src/counters/serialize.ts +85 -0
- package/src/errors/errors.ts +316 -0
- package/src/errors/index.ts +20 -0
- package/src/index.ts +42 -0
- package/src/internal/abort.ts +30 -0
- package/src/internal/http.ts +388 -0
- package/src/internal/openai-shaped.ts +555 -0
- package/src/internal/sse.ts +112 -0
- package/src/internal/url-utils.ts +20 -0
- package/src/middleware/compose.ts +213 -0
- package/src/middleware/index.ts +37 -0
- package/src/middleware/production-hook.ts +47 -0
- package/src/middleware/with-cost-limit.ts +131 -0
- package/src/middleware/with-cost-tracking.ts +216 -0
- package/src/middleware/with-fallback.ts +157 -0
- package/src/middleware/with-rate-limit.ts +306 -0
- package/src/middleware/with-redaction.ts +671 -0
- package/src/middleware/with-retry.ts +274 -0
- package/src/middleware/with-tracing.ts +117 -0
- package/src/model-tier/classify.ts +125 -0
- package/src/model-tier/index.ts +11 -0
- package/src/provider.ts +121 -0
- package/src/reasoning/apply-policy.ts +89 -0
- package/src/reasoning/classify-contract.ts +120 -0
- package/src/reasoning/index.ts +21 -0
- package/src/reasoning/retention.ts +64 -0
- package/src/tool-examples.ts +54 -0
- package/src/trust/classify-local-provider.ts +254 -0
- package/src/trust/index.ts +14 -0
- 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":"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"}
|
|
@@ -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":"
|
|
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.
|
|
51
|
-
bucket.
|
|
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 =
|
|
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
|
|
69
|
-
bucket
|
|
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(
|
|
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
|
-
|
|
114
|
-
|
|
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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"}
|
package/dist/package.js
ADDED
|
@@ -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.
|
|
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.
|
|
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
|
-
"
|
|
48
|
+
"default": "./dist/index.js"
|
|
48
49
|
},
|
|
49
50
|
"./adapters/vercel": {
|
|
50
51
|
"types": "./dist/adapters/vercel.d.ts",
|
|
51
|
-
"
|
|
52
|
+
"default": "./dist/adapters/vercel.js"
|
|
52
53
|
},
|
|
53
54
|
"./adapters/ollama": {
|
|
54
55
|
"types": "./dist/adapters/ollama.d.ts",
|
|
55
|
-
"
|
|
56
|
+
"default": "./dist/adapters/ollama.js"
|
|
56
57
|
},
|
|
57
58
|
"./adapters/llamacpp-server": {
|
|
58
59
|
"types": "./dist/adapters/llamacpp-server.d.ts",
|
|
59
|
-
"
|
|
60
|
+
"default": "./dist/adapters/llamacpp-server.js"
|
|
60
61
|
},
|
|
61
62
|
"./adapters/openai-compatible": {
|
|
62
63
|
"types": "./dist/adapters/openai-compatible.d.ts",
|
|
63
|
-
"
|
|
64
|
+
"default": "./dist/adapters/openai-compatible.js"
|
|
64
65
|
},
|
|
65
66
|
"./middleware": {
|
|
66
67
|
"types": "./dist/middleware/index.d.ts",
|
|
67
|
-
"
|
|
68
|
+
"default": "./dist/middleware/index.js"
|
|
68
69
|
},
|
|
69
70
|
"./counters": {
|
|
70
71
|
"types": "./dist/counters/index.d.ts",
|
|
71
|
-
"
|
|
72
|
+
"default": "./dist/counters/index.js"
|
|
72
73
|
},
|
|
73
74
|
"./trust": {
|
|
74
75
|
"types": "./dist/trust/index.d.ts",
|
|
75
|
-
"
|
|
76
|
+
"default": "./dist/trust/index.js"
|
|
76
77
|
},
|
|
77
78
|
"./model-tier": {
|
|
78
79
|
"types": "./dist/model-tier/index.d.ts",
|
|
79
|
-
"
|
|
80
|
+
"default": "./dist/model-tier/index.js"
|
|
80
81
|
},
|
|
81
82
|
"./reasoning": {
|
|
82
83
|
"types": "./dist/reasoning/index.d.ts",
|
|
83
|
-
"
|
|
84
|
+
"default": "./dist/reasoning/index.js"
|
|
84
85
|
},
|
|
85
86
|
"./errors": {
|
|
86
87
|
"types": "./dist/errors/index.d.ts",
|
|
87
|
-
"
|
|
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.
|
|
99
|
-
"@graphorin/observability": "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.
|
|
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';
|