@merkie/agentic 0.1.0 → 0.1.1

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/README.md CHANGED
@@ -35,7 +35,7 @@ Works in TypeScript and JavaScript, ESM and CommonJS.
35
35
  import { streamText } from "ai";
36
36
  import { createOpenRouter, logStream } from "@merkie/agentic";
37
37
 
38
- // Reads OPENROUTER_API_KEY from the environment and turns on usage tracking.
38
+ // Behaves exactly like the upstream factory, just with usage tracking on.
39
39
  const openrouter = createOpenRouter();
40
40
 
41
41
  const result = streamText({
@@ -56,20 +56,19 @@ const { createOpenRouter, logStream } = require("@merkie/agentic");
56
56
 
57
57
  ### `createOpenRouter(settings?)`
58
58
 
59
- Same signature and return type as `createOpenRouter` from
60
- `@openrouter/ai-sdk-provider`, with two defaults applied:
59
+ Same signature, return type, and defaults as `createOpenRouter` from
60
+ `@openrouter/ai-sdk-provider` (including reading `OPENROUTER_API_KEY` from the
61
+ environment). The **only** thing added:
61
62
 
62
63
  | Default | Behavior | Override |
63
64
  | --- | --- | --- |
64
- | `apiKey` | Falls back to `process.env.OPENROUTER_API_KEY` | Pass `apiKey` |
65
65
  | `extraBody.usage.include` | `true` (returns cost + token usage) | Pass `extraBody.usage` |
66
66
 
67
- Every option you pass wins over the defaults, so it's 1:1 compatible with the
67
+ Every option you pass wins over that default, so it's 1:1 compatible with the
68
68
  upstream factory:
69
69
 
70
70
  ```ts
71
71
  const openrouter = createOpenRouter({
72
- apiKey: myKey,
73
72
  extraBody: {
74
73
  transforms: ["middle-out"],
75
74
  usage: { include: false }, // opt back out if you want
package/dist/index.cjs CHANGED
@@ -38,10 +38,9 @@ module.exports = __toCommonJS(index_exports);
38
38
  // src/openrouter.ts
39
39
  var import_ai_sdk_provider = require("@openrouter/ai-sdk-provider");
40
40
  function createOpenRouter(settings = {}) {
41
- const { apiKey, extraBody, ...rest } = settings;
41
+ const { extraBody, ...rest } = settings;
42
42
  const userUsage = extraBody && typeof extraBody === "object" && "usage" in extraBody ? extraBody.usage : void 0;
43
43
  return (0, import_ai_sdk_provider.createOpenRouter)({
44
- apiKey: apiKey ?? process.env.OPENROUTER_API_KEY,
45
44
  ...rest,
46
45
  extraBody: {
47
46
  ...extraBody,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/openrouter.ts","../src/logStream.ts"],"sourcesContent":["export {\n\tcreateOpenRouter,\n\ttype OpenRouterProvider,\n\ttype OpenRouterProviderSettings,\n} from \"./openrouter.js\";\nexport { logStream } from \"./logStream.js\";\n","import {\n\tcreateOpenRouter as baseCreateOpenRouter,\n\ttype OpenRouterProvider,\n\ttype OpenRouterProviderSettings,\n} from \"@openrouter/ai-sdk-provider\";\n\n/**\n * Drop-in replacement for `createOpenRouter` from `@openrouter/ai-sdk-provider`.\n *\n * It behaves identically to the original, with two conveniences baked in:\n *\n * 1. `apiKey` defaults to `process.env.OPENROUTER_API_KEY` when you don't pass\n * one, so the common case is just `createOpenRouter()`.\n * 2. `extraBody.usage.include` defaults to `true`, which tells OpenRouter to\n * return cost + token accounting on every response. This is what powers the\n * mid-run cost/usage tracking in {@link logStream}.\n *\n * Every option you pass through wins over the defaults — including `usage` — so\n * this is 1:1 compatible with the upstream `createOpenRouter`. You can override\n * the api key, add your own `extraBody`, flip `usage.include` off, etc.\n *\n * @example\n * ```ts\n * import { createOpenRouter } from \"@merkie/agentic\";\n *\n * // Reads OPENROUTER_API_KEY from the environment, usage tracking on.\n * const openrouter = createOpenRouter();\n *\n * const model = openrouter(\"openai/gpt-4o-mini\");\n * ```\n */\nexport function createOpenRouter(\n\tsettings: OpenRouterProviderSettings = {},\n): OpenRouterProvider {\n\tconst { apiKey, extraBody, ...rest } = settings;\n\n\tconst userUsage =\n\t\textraBody && typeof extraBody === \"object\" && \"usage\" in extraBody\n\t\t\t? (extraBody as Record<string, unknown>).usage\n\t\t\t: undefined;\n\n\treturn baseCreateOpenRouter({\n\t\tapiKey: apiKey ?? process.env.OPENROUTER_API_KEY,\n\t\t...rest,\n\t\textraBody: {\n\t\t\t...extraBody,\n\t\t\tusage: {\n\t\t\t\tinclude: true,\n\t\t\t\t...(userUsage && typeof userUsage === \"object\"\n\t\t\t\t\t? (userUsage as Record<string, unknown>)\n\t\t\t\t\t: {}),\n\t\t\t},\n\t\t},\n\t});\n}\n\nexport type { OpenRouterProvider, OpenRouterProviderSettings };\n","import type { LanguageModelUsage, TextStreamPart, ToolSet } from \"ai\";\nimport chalk from \"chalk\";\n\ntype OpenRouterUsageCost = {\n\tcost?: number;\n\tcostDetails?: {\n\t\tupstreamInferenceCost?: number | null;\n\t};\n};\n\ntype OpenRouterModelResponse = {\n\tdata?: {\n\t\tcontext_length?: unknown;\n\t\ttop_provider?: {\n\t\t\tcontext_length?: unknown;\n\t\t};\n\t};\n};\n\nconst openRouterContextLengthCache = new Map<string, Promise<number | null>>();\n\nfunction getOpenRouterCost(part: TextStreamPart<ToolSet>) {\n\tif (part.type !== \"finish-step\") return null;\n\n\tconst metadataUsage = part.providerMetadata?.openrouter?.usage as\n\t\t| OpenRouterUsageCost\n\t\t| undefined;\n\tconst rawUsage = part.usage.raw as\n\t\t| {\n\t\t\t\tcost?: number;\n\t\t\t\tcost_details?: { upstream_inference_cost?: number | null };\n\t\t }\n\t\t| undefined;\n\n\tconst cost = metadataUsage?.cost ?? rawUsage?.cost;\n\tconst upstreamInferenceCost =\n\t\tmetadataUsage?.costDetails?.upstreamInferenceCost ??\n\t\trawUsage?.cost_details?.upstream_inference_cost;\n\n\t// OpenRouter credit usage reports `cost`; BYOK usage can report `cost: 0`\n\t// with the real provider charge in `upstreamInferenceCost`.\n\tif (cost === 0 && upstreamInferenceCost != null) {\n\t\treturn upstreamInferenceCost;\n\t}\n\n\treturn cost ?? upstreamInferenceCost ?? null;\n}\n\nfunction formatCost(cost: number) {\n\treturn `$${cost.toFixed(6)}`;\n}\n\nfunction formatTokens(tokens: number) {\n\treturn tokens.toLocaleString(\"en-US\");\n}\n\nfunction parsePositiveInteger(value: unknown) {\n\tif (typeof value === \"number\" && Number.isInteger(value) && value > 0) {\n\t\treturn value;\n\t}\n\n\tif (typeof value === \"string\") {\n\t\tconst parsed = Number(value);\n\t\tif (Number.isInteger(parsed) && parsed > 0) {\n\t\t\treturn parsed;\n\t\t}\n\t}\n\n\treturn null;\n}\n\nfunction getOpenRouterContextLengthFromResponse(payload: unknown) {\n\tconst data = (payload as OpenRouterModelResponse).data;\n\treturn (\n\t\tparsePositiveInteger(data?.context_length) ??\n\t\tparsePositiveInteger(data?.top_provider?.context_length)\n\t);\n}\n\nasync function fetchOpenRouterContextLength(modelId: string) {\n\tconst pathModelId = modelId.split(\"/\").map(encodeURIComponent).join(\"/\");\n\tconst response = await fetch(\n\t\t`https://openrouter.ai/api/v1/model/${pathModelId}`,\n\t);\n\n\tif (!response.ok) {\n\t\treturn null;\n\t}\n\n\treturn getOpenRouterContextLengthFromResponse(await response.json());\n}\n\nfunction getOpenRouterContextLength(modelId: string) {\n\tconst cached = openRouterContextLengthCache.get(modelId);\n\tif (cached) return cached;\n\n\tconst promise = fetchOpenRouterContextLength(modelId).catch(() => null);\n\topenRouterContextLengthCache.set(modelId, promise);\n\treturn promise;\n}\n\nfunction getModelId(part: TextStreamPart<ToolSet>) {\n\tif (part.type === \"start-step\") {\n\t\tconst body = part.request.body;\n\t\tif (body && typeof body === \"object\" && \"model\" in body) {\n\t\t\tconst model = body.model;\n\t\t\tif (typeof model === \"string\" && model.length > 0) {\n\t\t\t\treturn model;\n\t\t\t}\n\t\t}\n\t}\n\n\tif (part.type === \"finish-step\") {\n\t\treturn part.response.modelId;\n\t}\n\n\treturn null;\n}\n\nfunction getUsageTokens(usage: LanguageModelUsage | undefined) {\n\tif (!usage) return null;\n\n\t// Context usage is the stopping-point conversation size estimate:\n\t// the last step's input tokens are the full history before the final response,\n\t// and its output tokens are what will be appended to that history. An exact\n\t// \"next empty user message\" count would require another provider tokenization\n\t// pass because the stream does not emit usage for hypothetical future calls.\n\tif (usage.inputTokens != null || usage.outputTokens != null) {\n\t\treturn (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0);\n\t}\n\n\treturn usage.totalTokens ?? null;\n}\n\nfunction formatContextUsage(\n\tusage: LanguageModelUsage | undefined,\n\tcontextWindowTokens: number | null,\n) {\n\tconst tokens = getUsageTokens(usage);\n\tif (tokens == null) return \"context used ?\";\n\n\tif (contextWindowTokens == null) {\n\t\treturn `context used ${formatTokens(tokens)} tokens`;\n\t}\n\n\tconst percentage = (tokens / contextWindowTokens) * 100;\n\tconst formattedPercentage =\n\t\tpercentage > 0 && percentage < 0.01 ? \"<0.01\" : percentage.toFixed(2);\n\n\treturn `${formattedPercentage}% context used (${formatTokens(tokens)} / ${formatTokens(contextWindowTokens)} tokens)`;\n}\n\n/**\n * Pretty-prints every event coming off an AI SDK full stream so you can watch\n * the model think, talk, and call tools in real time — and see live token /\n * cost accounting when the model is served through OpenRouter (see\n * {@link createOpenRouter}).\n *\n * Pass it the `fullStream` from `streamText`:\n *\n * @example\n * ```ts\n * import { streamText } from \"ai\";\n * import { createOpenRouter, logStream } from \"@merkie/agentic\";\n *\n * const openrouter = createOpenRouter();\n *\n * const result = streamText({\n * model: openrouter(\"openai/gpt-4o-mini\"),\n * prompt: \"Explain quantum tunneling in one paragraph.\",\n * });\n *\n * await logStream(result.fullStream);\n * ```\n */\nexport async function logStream(\n\tstream: AsyncIterable<TextStreamPart<ToolSet>>,\n): Promise<void> {\n\t// Track which \"channel\" (reasoning vs. text) we're currently writing to so we\n\t// only print a header when it changes, and stream deltas onto the same line.\n\tlet active: \"reasoning\" | \"text\" | null = null;\n\tlet openRouterCost = 0;\n\tlet hasOpenRouterCost = false;\n\tlet latestStepUsage: LanguageModelUsage | undefined;\n\tlet contextWindowPromise: Promise<number | null> | undefined;\n\n\tconst endActive = () => {\n\t\tif (active) process.stdout.write(\"\\n\");\n\t\tactive = null;\n\t};\n\n\tconst startContextWindowFetch = (modelId: string | null) => {\n\t\tif (!modelId || contextWindowPromise) return;\n\t\tcontextWindowPromise = getOpenRouterContextLength(modelId);\n\t};\n\n\tfor await (const part of stream) {\n\t\tswitch (part.type) {\n\t\t\tcase \"start-step\":\n\t\t\t\tendActive();\n\t\t\t\tstartContextWindowFetch(getModelId(part));\n\t\t\t\tconsole.log(chalk.dim(\"──────── step ────────\"));\n\t\t\t\tbreak;\n\n\t\t\tcase \"finish-step\": {\n\t\t\t\tlatestStepUsage = part.usage;\n\t\t\t\tstartContextWindowFetch(getModelId(part));\n\t\t\t\tconst cost = getOpenRouterCost(part);\n\t\t\t\tif (cost != null) {\n\t\t\t\t\topenRouterCost += cost;\n\t\t\t\t\thasOpenRouterCost = true;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"reasoning-delta\":\n\t\t\t\tif (active !== \"reasoning\") {\n\t\t\t\t\tendActive();\n\t\t\t\t\tprocess.stdout.write(chalk.magenta.bold(\"🧠 reasoning \"));\n\t\t\t\t\tactive = \"reasoning\";\n\t\t\t\t}\n\t\t\t\tprocess.stdout.write(chalk.magenta(part.text));\n\t\t\t\tbreak;\n\n\t\t\tcase \"text-delta\":\n\t\t\t\tif (active !== \"text\") {\n\t\t\t\t\tendActive();\n\t\t\t\t\tprocess.stdout.write(chalk.white.bold(\"💬 message \"));\n\t\t\t\t\tactive = \"text\";\n\t\t\t\t}\n\t\t\t\tprocess.stdout.write(chalk.white(part.text));\n\t\t\t\tbreak;\n\n\t\t\tcase \"tool-call\":\n\t\t\t\tendActive();\n\t\t\t\tconsole.log(\n\t\t\t\t\tchalk.cyan.bold(`🔧 tool call ${part.toolName}`) +\n\t\t\t\t\t\tchalk.cyan(`(${JSON.stringify(part.input)})`),\n\t\t\t\t);\n\t\t\t\tbreak;\n\n\t\t\tcase \"tool-result\":\n\t\t\t\tendActive();\n\t\t\t\tconsole.log(\n\t\t\t\t\tchalk.green.bold(`✅ tool result ${part.toolName} `) +\n\t\t\t\t\t\tchalk.green(JSON.stringify(part.output)),\n\t\t\t\t);\n\t\t\t\tbreak;\n\n\t\t\tcase \"tool-error\":\n\t\t\t\tendActive();\n\t\t\t\tconsole.log(\n\t\t\t\t\tchalk.red.bold(`❌ tool error ${part.toolName} `) +\n\t\t\t\t\t\tchalk.red(String(part.error)),\n\t\t\t\t);\n\t\t\t\tbreak;\n\n\t\t\tcase \"error\":\n\t\t\t\tendActive();\n\t\t\t\tconsole.log(chalk.red.bold(\"‼️ stream error \"), part.error);\n\t\t\t\tbreak;\n\n\t\t\tcase \"finish\": {\n\t\t\t\tendActive();\n\t\t\t\tconst contextWindowTokens = contextWindowPromise\n\t\t\t\t\t? await contextWindowPromise\n\t\t\t\t\t: null;\n\t\t\t\tconsole.log(\n\t\t\t\t\tchalk.dim(\n\t\t\t\t\t\t`\\n── done (${part.finishReason}) · ${formatContextUsage(latestStepUsage, contextWindowTokens)}${hasOpenRouterCost ? ` · cost ${formatCost(openRouterCost)}` : \"\"} ──`,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n}\n\nexport default logStream;\n"],"mappings":"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","names":["baseCreateOpenRouter","chalk"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/openrouter.ts","../src/logStream.ts"],"sourcesContent":["export {\n\tcreateOpenRouter,\n\ttype OpenRouterProvider,\n\ttype OpenRouterProviderSettings,\n} from \"./openrouter.js\";\nexport { logStream } from \"./logStream.js\";\n","import {\n\tcreateOpenRouter as baseCreateOpenRouter,\n\ttype OpenRouterProvider,\n\ttype OpenRouterProviderSettings,\n} from \"@openrouter/ai-sdk-provider\";\n\n/**\n * Drop-in replacement for `createOpenRouter` from `@openrouter/ai-sdk-provider`.\n *\n * It behaves identically to the original — same options, same defaults\n * (including reading `OPENROUTER_API_KEY` from the environment) — with exactly\n * one thing added: `extraBody.usage.include` defaults to `true`, which tells\n * OpenRouter to return cost + token accounting on every response. That's what\n * powers the mid-run cost/usage tracking in {@link logStream}.\n *\n * Everything you pass through wins over that default — including `usage` — so it\n * stays 1:1 compatible with the upstream factory. Add your own `extraBody`, flip\n * `usage.include` off, etc.\n *\n * @example\n * ```ts\n * import { createOpenRouter } from \"@merkie/agentic\";\n *\n * // Same as the upstream factory, just with usage tracking switched on.\n * const openrouter = createOpenRouter();\n *\n * const model = openrouter(\"openai/gpt-4o-mini\");\n * ```\n */\nexport function createOpenRouter(\n\tsettings: OpenRouterProviderSettings = {},\n): OpenRouterProvider {\n\tconst { extraBody, ...rest } = settings;\n\n\tconst userUsage =\n\t\textraBody && typeof extraBody === \"object\" && \"usage\" in extraBody\n\t\t\t? (extraBody as Record<string, unknown>).usage\n\t\t\t: undefined;\n\n\treturn baseCreateOpenRouter({\n\t\t...rest,\n\t\textraBody: {\n\t\t\t...extraBody,\n\t\t\tusage: {\n\t\t\t\tinclude: true,\n\t\t\t\t...(userUsage && typeof userUsage === \"object\"\n\t\t\t\t\t? (userUsage as Record<string, unknown>)\n\t\t\t\t\t: {}),\n\t\t\t},\n\t\t},\n\t});\n}\n\nexport type { OpenRouterProvider, OpenRouterProviderSettings };\n","import type { LanguageModelUsage, TextStreamPart, ToolSet } from \"ai\";\nimport chalk from \"chalk\";\n\ntype OpenRouterUsageCost = {\n\tcost?: number;\n\tcostDetails?: {\n\t\tupstreamInferenceCost?: number | null;\n\t};\n};\n\ntype OpenRouterModelResponse = {\n\tdata?: {\n\t\tcontext_length?: unknown;\n\t\ttop_provider?: {\n\t\t\tcontext_length?: unknown;\n\t\t};\n\t};\n};\n\nconst openRouterContextLengthCache = new Map<string, Promise<number | null>>();\n\nfunction getOpenRouterCost(part: TextStreamPart<ToolSet>) {\n\tif (part.type !== \"finish-step\") return null;\n\n\tconst metadataUsage = part.providerMetadata?.openrouter?.usage as\n\t\t| OpenRouterUsageCost\n\t\t| undefined;\n\tconst rawUsage = part.usage.raw as\n\t\t| {\n\t\t\t\tcost?: number;\n\t\t\t\tcost_details?: { upstream_inference_cost?: number | null };\n\t\t }\n\t\t| undefined;\n\n\tconst cost = metadataUsage?.cost ?? rawUsage?.cost;\n\tconst upstreamInferenceCost =\n\t\tmetadataUsage?.costDetails?.upstreamInferenceCost ??\n\t\trawUsage?.cost_details?.upstream_inference_cost;\n\n\t// OpenRouter credit usage reports `cost`; BYOK usage can report `cost: 0`\n\t// with the real provider charge in `upstreamInferenceCost`.\n\tif (cost === 0 && upstreamInferenceCost != null) {\n\t\treturn upstreamInferenceCost;\n\t}\n\n\treturn cost ?? upstreamInferenceCost ?? null;\n}\n\nfunction formatCost(cost: number) {\n\treturn `$${cost.toFixed(6)}`;\n}\n\nfunction formatTokens(tokens: number) {\n\treturn tokens.toLocaleString(\"en-US\");\n}\n\nfunction parsePositiveInteger(value: unknown) {\n\tif (typeof value === \"number\" && Number.isInteger(value) && value > 0) {\n\t\treturn value;\n\t}\n\n\tif (typeof value === \"string\") {\n\t\tconst parsed = Number(value);\n\t\tif (Number.isInteger(parsed) && parsed > 0) {\n\t\t\treturn parsed;\n\t\t}\n\t}\n\n\treturn null;\n}\n\nfunction getOpenRouterContextLengthFromResponse(payload: unknown) {\n\tconst data = (payload as OpenRouterModelResponse).data;\n\treturn (\n\t\tparsePositiveInteger(data?.context_length) ??\n\t\tparsePositiveInteger(data?.top_provider?.context_length)\n\t);\n}\n\nasync function fetchOpenRouterContextLength(modelId: string) {\n\tconst pathModelId = modelId.split(\"/\").map(encodeURIComponent).join(\"/\");\n\tconst response = await fetch(\n\t\t`https://openrouter.ai/api/v1/model/${pathModelId}`,\n\t);\n\n\tif (!response.ok) {\n\t\treturn null;\n\t}\n\n\treturn getOpenRouterContextLengthFromResponse(await response.json());\n}\n\nfunction getOpenRouterContextLength(modelId: string) {\n\tconst cached = openRouterContextLengthCache.get(modelId);\n\tif (cached) return cached;\n\n\tconst promise = fetchOpenRouterContextLength(modelId).catch(() => null);\n\topenRouterContextLengthCache.set(modelId, promise);\n\treturn promise;\n}\n\nfunction getModelId(part: TextStreamPart<ToolSet>) {\n\tif (part.type === \"start-step\") {\n\t\tconst body = part.request.body;\n\t\tif (body && typeof body === \"object\" && \"model\" in body) {\n\t\t\tconst model = body.model;\n\t\t\tif (typeof model === \"string\" && model.length > 0) {\n\t\t\t\treturn model;\n\t\t\t}\n\t\t}\n\t}\n\n\tif (part.type === \"finish-step\") {\n\t\treturn part.response.modelId;\n\t}\n\n\treturn null;\n}\n\nfunction getUsageTokens(usage: LanguageModelUsage | undefined) {\n\tif (!usage) return null;\n\n\t// Context usage is the stopping-point conversation size estimate:\n\t// the last step's input tokens are the full history before the final response,\n\t// and its output tokens are what will be appended to that history. An exact\n\t// \"next empty user message\" count would require another provider tokenization\n\t// pass because the stream does not emit usage for hypothetical future calls.\n\tif (usage.inputTokens != null || usage.outputTokens != null) {\n\t\treturn (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0);\n\t}\n\n\treturn usage.totalTokens ?? null;\n}\n\nfunction formatContextUsage(\n\tusage: LanguageModelUsage | undefined,\n\tcontextWindowTokens: number | null,\n) {\n\tconst tokens = getUsageTokens(usage);\n\tif (tokens == null) return \"context used ?\";\n\n\tif (contextWindowTokens == null) {\n\t\treturn `context used ${formatTokens(tokens)} tokens`;\n\t}\n\n\tconst percentage = (tokens / contextWindowTokens) * 100;\n\tconst formattedPercentage =\n\t\tpercentage > 0 && percentage < 0.01 ? \"<0.01\" : percentage.toFixed(2);\n\n\treturn `${formattedPercentage}% context used (${formatTokens(tokens)} / ${formatTokens(contextWindowTokens)} tokens)`;\n}\n\n/**\n * Pretty-prints every event coming off an AI SDK full stream so you can watch\n * the model think, talk, and call tools in real time — and see live token /\n * cost accounting when the model is served through OpenRouter (see\n * {@link createOpenRouter}).\n *\n * Pass it the `fullStream` from `streamText`:\n *\n * @example\n * ```ts\n * import { streamText } from \"ai\";\n * import { createOpenRouter, logStream } from \"@merkie/agentic\";\n *\n * const openrouter = createOpenRouter();\n *\n * const result = streamText({\n * model: openrouter(\"openai/gpt-4o-mini\"),\n * prompt: \"Explain quantum tunneling in one paragraph.\",\n * });\n *\n * await logStream(result.fullStream);\n * ```\n */\nexport async function logStream(\n\tstream: AsyncIterable<TextStreamPart<ToolSet>>,\n): Promise<void> {\n\t// Track which \"channel\" (reasoning vs. text) we're currently writing to so we\n\t// only print a header when it changes, and stream deltas onto the same line.\n\tlet active: \"reasoning\" | \"text\" | null = null;\n\tlet openRouterCost = 0;\n\tlet hasOpenRouterCost = false;\n\tlet latestStepUsage: LanguageModelUsage | undefined;\n\tlet contextWindowPromise: Promise<number | null> | undefined;\n\n\tconst endActive = () => {\n\t\tif (active) process.stdout.write(\"\\n\");\n\t\tactive = null;\n\t};\n\n\tconst startContextWindowFetch = (modelId: string | null) => {\n\t\tif (!modelId || contextWindowPromise) return;\n\t\tcontextWindowPromise = getOpenRouterContextLength(modelId);\n\t};\n\n\tfor await (const part of stream) {\n\t\tswitch (part.type) {\n\t\t\tcase \"start-step\":\n\t\t\t\tendActive();\n\t\t\t\tstartContextWindowFetch(getModelId(part));\n\t\t\t\tconsole.log(chalk.dim(\"──────── step ────────\"));\n\t\t\t\tbreak;\n\n\t\t\tcase \"finish-step\": {\n\t\t\t\tlatestStepUsage = part.usage;\n\t\t\t\tstartContextWindowFetch(getModelId(part));\n\t\t\t\tconst cost = getOpenRouterCost(part);\n\t\t\t\tif (cost != null) {\n\t\t\t\t\topenRouterCost += cost;\n\t\t\t\t\thasOpenRouterCost = true;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"reasoning-delta\":\n\t\t\t\tif (active !== \"reasoning\") {\n\t\t\t\t\tendActive();\n\t\t\t\t\tprocess.stdout.write(chalk.magenta.bold(\"🧠 reasoning \"));\n\t\t\t\t\tactive = \"reasoning\";\n\t\t\t\t}\n\t\t\t\tprocess.stdout.write(chalk.magenta(part.text));\n\t\t\t\tbreak;\n\n\t\t\tcase \"text-delta\":\n\t\t\t\tif (active !== \"text\") {\n\t\t\t\t\tendActive();\n\t\t\t\t\tprocess.stdout.write(chalk.white.bold(\"💬 message \"));\n\t\t\t\t\tactive = \"text\";\n\t\t\t\t}\n\t\t\t\tprocess.stdout.write(chalk.white(part.text));\n\t\t\t\tbreak;\n\n\t\t\tcase \"tool-call\":\n\t\t\t\tendActive();\n\t\t\t\tconsole.log(\n\t\t\t\t\tchalk.cyan.bold(`🔧 tool call ${part.toolName}`) +\n\t\t\t\t\t\tchalk.cyan(`(${JSON.stringify(part.input)})`),\n\t\t\t\t);\n\t\t\t\tbreak;\n\n\t\t\tcase \"tool-result\":\n\t\t\t\tendActive();\n\t\t\t\tconsole.log(\n\t\t\t\t\tchalk.green.bold(`✅ tool result ${part.toolName} `) +\n\t\t\t\t\t\tchalk.green(JSON.stringify(part.output)),\n\t\t\t\t);\n\t\t\t\tbreak;\n\n\t\t\tcase \"tool-error\":\n\t\t\t\tendActive();\n\t\t\t\tconsole.log(\n\t\t\t\t\tchalk.red.bold(`❌ tool error ${part.toolName} `) +\n\t\t\t\t\t\tchalk.red(String(part.error)),\n\t\t\t\t);\n\t\t\t\tbreak;\n\n\t\t\tcase \"error\":\n\t\t\t\tendActive();\n\t\t\t\tconsole.log(chalk.red.bold(\"‼️ stream error \"), part.error);\n\t\t\t\tbreak;\n\n\t\t\tcase \"finish\": {\n\t\t\t\tendActive();\n\t\t\t\tconst contextWindowTokens = contextWindowPromise\n\t\t\t\t\t? await contextWindowPromise\n\t\t\t\t\t: null;\n\t\t\t\tconsole.log(\n\t\t\t\t\tchalk.dim(\n\t\t\t\t\t\t`\\n── done (${part.finishReason}) · ${formatContextUsage(latestStepUsage, contextWindowTokens)}${hasOpenRouterCost ? ` · cost ${formatCost(openRouterCost)}` : \"\"} ──`,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n}\n\nexport default logStream;\n"],"mappings":"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","names":["baseCreateOpenRouter","chalk"]}
package/dist/index.d.cts CHANGED
@@ -5,23 +5,21 @@ import { TextStreamPart, ToolSet } from 'ai';
5
5
  /**
6
6
  * Drop-in replacement for `createOpenRouter` from `@openrouter/ai-sdk-provider`.
7
7
  *
8
- * It behaves identically to the original, with two conveniences baked in:
8
+ * It behaves identically to the original same options, same defaults
9
+ * (including reading `OPENROUTER_API_KEY` from the environment) — with exactly
10
+ * one thing added: `extraBody.usage.include` defaults to `true`, which tells
11
+ * OpenRouter to return cost + token accounting on every response. That's what
12
+ * powers the mid-run cost/usage tracking in {@link logStream}.
9
13
  *
10
- * 1. `apiKey` defaults to `process.env.OPENROUTER_API_KEY` when you don't pass
11
- * one, so the common case is just `createOpenRouter()`.
12
- * 2. `extraBody.usage.include` defaults to `true`, which tells OpenRouter to
13
- * return cost + token accounting on every response. This is what powers the
14
- * mid-run cost/usage tracking in {@link logStream}.
15
- *
16
- * Every option you pass through wins over the defaults — including `usage` — so
17
- * this is 1:1 compatible with the upstream `createOpenRouter`. You can override
18
- * the api key, add your own `extraBody`, flip `usage.include` off, etc.
14
+ * Everything you pass through wins over that default — including `usage` so it
15
+ * stays 1:1 compatible with the upstream factory. Add your own `extraBody`, flip
16
+ * `usage.include` off, etc.
19
17
  *
20
18
  * @example
21
19
  * ```ts
22
20
  * import { createOpenRouter } from "@merkie/agentic";
23
21
  *
24
- * // Reads OPENROUTER_API_KEY from the environment, usage tracking on.
22
+ * // Same as the upstream factory, just with usage tracking switched on.
25
23
  * const openrouter = createOpenRouter();
26
24
  *
27
25
  * const model = openrouter("openai/gpt-4o-mini");
package/dist/index.d.ts CHANGED
@@ -5,23 +5,21 @@ import { TextStreamPart, ToolSet } from 'ai';
5
5
  /**
6
6
  * Drop-in replacement for `createOpenRouter` from `@openrouter/ai-sdk-provider`.
7
7
  *
8
- * It behaves identically to the original, with two conveniences baked in:
8
+ * It behaves identically to the original same options, same defaults
9
+ * (including reading `OPENROUTER_API_KEY` from the environment) — with exactly
10
+ * one thing added: `extraBody.usage.include` defaults to `true`, which tells
11
+ * OpenRouter to return cost + token accounting on every response. That's what
12
+ * powers the mid-run cost/usage tracking in {@link logStream}.
9
13
  *
10
- * 1. `apiKey` defaults to `process.env.OPENROUTER_API_KEY` when you don't pass
11
- * one, so the common case is just `createOpenRouter()`.
12
- * 2. `extraBody.usage.include` defaults to `true`, which tells OpenRouter to
13
- * return cost + token accounting on every response. This is what powers the
14
- * mid-run cost/usage tracking in {@link logStream}.
15
- *
16
- * Every option you pass through wins over the defaults — including `usage` — so
17
- * this is 1:1 compatible with the upstream `createOpenRouter`. You can override
18
- * the api key, add your own `extraBody`, flip `usage.include` off, etc.
14
+ * Everything you pass through wins over that default — including `usage` so it
15
+ * stays 1:1 compatible with the upstream factory. Add your own `extraBody`, flip
16
+ * `usage.include` off, etc.
19
17
  *
20
18
  * @example
21
19
  * ```ts
22
20
  * import { createOpenRouter } from "@merkie/agentic";
23
21
  *
24
- * // Reads OPENROUTER_API_KEY from the environment, usage tracking on.
22
+ * // Same as the upstream factory, just with usage tracking switched on.
25
23
  * const openrouter = createOpenRouter();
26
24
  *
27
25
  * const model = openrouter("openai/gpt-4o-mini");
package/dist/index.js CHANGED
@@ -3,10 +3,9 @@ import {
3
3
  createOpenRouter as baseCreateOpenRouter
4
4
  } from "@openrouter/ai-sdk-provider";
5
5
  function createOpenRouter(settings = {}) {
6
- const { apiKey, extraBody, ...rest } = settings;
6
+ const { extraBody, ...rest } = settings;
7
7
  const userUsage = extraBody && typeof extraBody === "object" && "usage" in extraBody ? extraBody.usage : void 0;
8
8
  return baseCreateOpenRouter({
9
- apiKey: apiKey ?? process.env.OPENROUTER_API_KEY,
10
9
  ...rest,
11
10
  extraBody: {
12
11
  ...extraBody,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/openrouter.ts","../src/logStream.ts"],"sourcesContent":["import {\n\tcreateOpenRouter as baseCreateOpenRouter,\n\ttype OpenRouterProvider,\n\ttype OpenRouterProviderSettings,\n} from \"@openrouter/ai-sdk-provider\";\n\n/**\n * Drop-in replacement for `createOpenRouter` from `@openrouter/ai-sdk-provider`.\n *\n * It behaves identically to the original, with two conveniences baked in:\n *\n * 1. `apiKey` defaults to `process.env.OPENROUTER_API_KEY` when you don't pass\n * one, so the common case is just `createOpenRouter()`.\n * 2. `extraBody.usage.include` defaults to `true`, which tells OpenRouter to\n * return cost + token accounting on every response. This is what powers the\n * mid-run cost/usage tracking in {@link logStream}.\n *\n * Every option you pass through wins over the defaults — including `usage` — so\n * this is 1:1 compatible with the upstream `createOpenRouter`. You can override\n * the api key, add your own `extraBody`, flip `usage.include` off, etc.\n *\n * @example\n * ```ts\n * import { createOpenRouter } from \"@merkie/agentic\";\n *\n * // Reads OPENROUTER_API_KEY from the environment, usage tracking on.\n * const openrouter = createOpenRouter();\n *\n * const model = openrouter(\"openai/gpt-4o-mini\");\n * ```\n */\nexport function createOpenRouter(\n\tsettings: OpenRouterProviderSettings = {},\n): OpenRouterProvider {\n\tconst { apiKey, extraBody, ...rest } = settings;\n\n\tconst userUsage =\n\t\textraBody && typeof extraBody === \"object\" && \"usage\" in extraBody\n\t\t\t? (extraBody as Record<string, unknown>).usage\n\t\t\t: undefined;\n\n\treturn baseCreateOpenRouter({\n\t\tapiKey: apiKey ?? process.env.OPENROUTER_API_KEY,\n\t\t...rest,\n\t\textraBody: {\n\t\t\t...extraBody,\n\t\t\tusage: {\n\t\t\t\tinclude: true,\n\t\t\t\t...(userUsage && typeof userUsage === \"object\"\n\t\t\t\t\t? (userUsage as Record<string, unknown>)\n\t\t\t\t\t: {}),\n\t\t\t},\n\t\t},\n\t});\n}\n\nexport type { OpenRouterProvider, OpenRouterProviderSettings };\n","import type { LanguageModelUsage, TextStreamPart, ToolSet } from \"ai\";\nimport chalk from \"chalk\";\n\ntype OpenRouterUsageCost = {\n\tcost?: number;\n\tcostDetails?: {\n\t\tupstreamInferenceCost?: number | null;\n\t};\n};\n\ntype OpenRouterModelResponse = {\n\tdata?: {\n\t\tcontext_length?: unknown;\n\t\ttop_provider?: {\n\t\t\tcontext_length?: unknown;\n\t\t};\n\t};\n};\n\nconst openRouterContextLengthCache = new Map<string, Promise<number | null>>();\n\nfunction getOpenRouterCost(part: TextStreamPart<ToolSet>) {\n\tif (part.type !== \"finish-step\") return null;\n\n\tconst metadataUsage = part.providerMetadata?.openrouter?.usage as\n\t\t| OpenRouterUsageCost\n\t\t| undefined;\n\tconst rawUsage = part.usage.raw as\n\t\t| {\n\t\t\t\tcost?: number;\n\t\t\t\tcost_details?: { upstream_inference_cost?: number | null };\n\t\t }\n\t\t| undefined;\n\n\tconst cost = metadataUsage?.cost ?? rawUsage?.cost;\n\tconst upstreamInferenceCost =\n\t\tmetadataUsage?.costDetails?.upstreamInferenceCost ??\n\t\trawUsage?.cost_details?.upstream_inference_cost;\n\n\t// OpenRouter credit usage reports `cost`; BYOK usage can report `cost: 0`\n\t// with the real provider charge in `upstreamInferenceCost`.\n\tif (cost === 0 && upstreamInferenceCost != null) {\n\t\treturn upstreamInferenceCost;\n\t}\n\n\treturn cost ?? upstreamInferenceCost ?? null;\n}\n\nfunction formatCost(cost: number) {\n\treturn `$${cost.toFixed(6)}`;\n}\n\nfunction formatTokens(tokens: number) {\n\treturn tokens.toLocaleString(\"en-US\");\n}\n\nfunction parsePositiveInteger(value: unknown) {\n\tif (typeof value === \"number\" && Number.isInteger(value) && value > 0) {\n\t\treturn value;\n\t}\n\n\tif (typeof value === \"string\") {\n\t\tconst parsed = Number(value);\n\t\tif (Number.isInteger(parsed) && parsed > 0) {\n\t\t\treturn parsed;\n\t\t}\n\t}\n\n\treturn null;\n}\n\nfunction getOpenRouterContextLengthFromResponse(payload: unknown) {\n\tconst data = (payload as OpenRouterModelResponse).data;\n\treturn (\n\t\tparsePositiveInteger(data?.context_length) ??\n\t\tparsePositiveInteger(data?.top_provider?.context_length)\n\t);\n}\n\nasync function fetchOpenRouterContextLength(modelId: string) {\n\tconst pathModelId = modelId.split(\"/\").map(encodeURIComponent).join(\"/\");\n\tconst response = await fetch(\n\t\t`https://openrouter.ai/api/v1/model/${pathModelId}`,\n\t);\n\n\tif (!response.ok) {\n\t\treturn null;\n\t}\n\n\treturn getOpenRouterContextLengthFromResponse(await response.json());\n}\n\nfunction getOpenRouterContextLength(modelId: string) {\n\tconst cached = openRouterContextLengthCache.get(modelId);\n\tif (cached) return cached;\n\n\tconst promise = fetchOpenRouterContextLength(modelId).catch(() => null);\n\topenRouterContextLengthCache.set(modelId, promise);\n\treturn promise;\n}\n\nfunction getModelId(part: TextStreamPart<ToolSet>) {\n\tif (part.type === \"start-step\") {\n\t\tconst body = part.request.body;\n\t\tif (body && typeof body === \"object\" && \"model\" in body) {\n\t\t\tconst model = body.model;\n\t\t\tif (typeof model === \"string\" && model.length > 0) {\n\t\t\t\treturn model;\n\t\t\t}\n\t\t}\n\t}\n\n\tif (part.type === \"finish-step\") {\n\t\treturn part.response.modelId;\n\t}\n\n\treturn null;\n}\n\nfunction getUsageTokens(usage: LanguageModelUsage | undefined) {\n\tif (!usage) return null;\n\n\t// Context usage is the stopping-point conversation size estimate:\n\t// the last step's input tokens are the full history before the final response,\n\t// and its output tokens are what will be appended to that history. An exact\n\t// \"next empty user message\" count would require another provider tokenization\n\t// pass because the stream does not emit usage for hypothetical future calls.\n\tif (usage.inputTokens != null || usage.outputTokens != null) {\n\t\treturn (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0);\n\t}\n\n\treturn usage.totalTokens ?? null;\n}\n\nfunction formatContextUsage(\n\tusage: LanguageModelUsage | undefined,\n\tcontextWindowTokens: number | null,\n) {\n\tconst tokens = getUsageTokens(usage);\n\tif (tokens == null) return \"context used ?\";\n\n\tif (contextWindowTokens == null) {\n\t\treturn `context used ${formatTokens(tokens)} tokens`;\n\t}\n\n\tconst percentage = (tokens / contextWindowTokens) * 100;\n\tconst formattedPercentage =\n\t\tpercentage > 0 && percentage < 0.01 ? \"<0.01\" : percentage.toFixed(2);\n\n\treturn `${formattedPercentage}% context used (${formatTokens(tokens)} / ${formatTokens(contextWindowTokens)} tokens)`;\n}\n\n/**\n * Pretty-prints every event coming off an AI SDK full stream so you can watch\n * the model think, talk, and call tools in real time — and see live token /\n * cost accounting when the model is served through OpenRouter (see\n * {@link createOpenRouter}).\n *\n * Pass it the `fullStream` from `streamText`:\n *\n * @example\n * ```ts\n * import { streamText } from \"ai\";\n * import { createOpenRouter, logStream } from \"@merkie/agentic\";\n *\n * const openrouter = createOpenRouter();\n *\n * const result = streamText({\n * model: openrouter(\"openai/gpt-4o-mini\"),\n * prompt: \"Explain quantum tunneling in one paragraph.\",\n * });\n *\n * await logStream(result.fullStream);\n * ```\n */\nexport async function logStream(\n\tstream: AsyncIterable<TextStreamPart<ToolSet>>,\n): Promise<void> {\n\t// Track which \"channel\" (reasoning vs. text) we're currently writing to so we\n\t// only print a header when it changes, and stream deltas onto the same line.\n\tlet active: \"reasoning\" | \"text\" | null = null;\n\tlet openRouterCost = 0;\n\tlet hasOpenRouterCost = false;\n\tlet latestStepUsage: LanguageModelUsage | undefined;\n\tlet contextWindowPromise: Promise<number | null> | undefined;\n\n\tconst endActive = () => {\n\t\tif (active) process.stdout.write(\"\\n\");\n\t\tactive = null;\n\t};\n\n\tconst startContextWindowFetch = (modelId: string | null) => {\n\t\tif (!modelId || contextWindowPromise) return;\n\t\tcontextWindowPromise = getOpenRouterContextLength(modelId);\n\t};\n\n\tfor await (const part of stream) {\n\t\tswitch (part.type) {\n\t\t\tcase \"start-step\":\n\t\t\t\tendActive();\n\t\t\t\tstartContextWindowFetch(getModelId(part));\n\t\t\t\tconsole.log(chalk.dim(\"──────── step ────────\"));\n\t\t\t\tbreak;\n\n\t\t\tcase \"finish-step\": {\n\t\t\t\tlatestStepUsage = part.usage;\n\t\t\t\tstartContextWindowFetch(getModelId(part));\n\t\t\t\tconst cost = getOpenRouterCost(part);\n\t\t\t\tif (cost != null) {\n\t\t\t\t\topenRouterCost += cost;\n\t\t\t\t\thasOpenRouterCost = true;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"reasoning-delta\":\n\t\t\t\tif (active !== \"reasoning\") {\n\t\t\t\t\tendActive();\n\t\t\t\t\tprocess.stdout.write(chalk.magenta.bold(\"🧠 reasoning \"));\n\t\t\t\t\tactive = \"reasoning\";\n\t\t\t\t}\n\t\t\t\tprocess.stdout.write(chalk.magenta(part.text));\n\t\t\t\tbreak;\n\n\t\t\tcase \"text-delta\":\n\t\t\t\tif (active !== \"text\") {\n\t\t\t\t\tendActive();\n\t\t\t\t\tprocess.stdout.write(chalk.white.bold(\"💬 message \"));\n\t\t\t\t\tactive = \"text\";\n\t\t\t\t}\n\t\t\t\tprocess.stdout.write(chalk.white(part.text));\n\t\t\t\tbreak;\n\n\t\t\tcase \"tool-call\":\n\t\t\t\tendActive();\n\t\t\t\tconsole.log(\n\t\t\t\t\tchalk.cyan.bold(`🔧 tool call ${part.toolName}`) +\n\t\t\t\t\t\tchalk.cyan(`(${JSON.stringify(part.input)})`),\n\t\t\t\t);\n\t\t\t\tbreak;\n\n\t\t\tcase \"tool-result\":\n\t\t\t\tendActive();\n\t\t\t\tconsole.log(\n\t\t\t\t\tchalk.green.bold(`✅ tool result ${part.toolName} `) +\n\t\t\t\t\t\tchalk.green(JSON.stringify(part.output)),\n\t\t\t\t);\n\t\t\t\tbreak;\n\n\t\t\tcase \"tool-error\":\n\t\t\t\tendActive();\n\t\t\t\tconsole.log(\n\t\t\t\t\tchalk.red.bold(`❌ tool error ${part.toolName} `) +\n\t\t\t\t\t\tchalk.red(String(part.error)),\n\t\t\t\t);\n\t\t\t\tbreak;\n\n\t\t\tcase \"error\":\n\t\t\t\tendActive();\n\t\t\t\tconsole.log(chalk.red.bold(\"‼️ stream error \"), part.error);\n\t\t\t\tbreak;\n\n\t\t\tcase \"finish\": {\n\t\t\t\tendActive();\n\t\t\t\tconst contextWindowTokens = contextWindowPromise\n\t\t\t\t\t? await contextWindowPromise\n\t\t\t\t\t: null;\n\t\t\t\tconsole.log(\n\t\t\t\t\tchalk.dim(\n\t\t\t\t\t\t`\\n── done (${part.finishReason}) · ${formatContextUsage(latestStepUsage, contextWindowTokens)}${hasOpenRouterCost ? ` · cost ${formatCost(openRouterCost)}` : \"\"} ──`,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n}\n\nexport default logStream;\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/openrouter.ts","../src/logStream.ts"],"sourcesContent":["import {\n\tcreateOpenRouter as baseCreateOpenRouter,\n\ttype OpenRouterProvider,\n\ttype OpenRouterProviderSettings,\n} from \"@openrouter/ai-sdk-provider\";\n\n/**\n * Drop-in replacement for `createOpenRouter` from `@openrouter/ai-sdk-provider`.\n *\n * It behaves identically to the original — same options, same defaults\n * (including reading `OPENROUTER_API_KEY` from the environment) — with exactly\n * one thing added: `extraBody.usage.include` defaults to `true`, which tells\n * OpenRouter to return cost + token accounting on every response. That's what\n * powers the mid-run cost/usage tracking in {@link logStream}.\n *\n * Everything you pass through wins over that default — including `usage` — so it\n * stays 1:1 compatible with the upstream factory. Add your own `extraBody`, flip\n * `usage.include` off, etc.\n *\n * @example\n * ```ts\n * import { createOpenRouter } from \"@merkie/agentic\";\n *\n * // Same as the upstream factory, just with usage tracking switched on.\n * const openrouter = createOpenRouter();\n *\n * const model = openrouter(\"openai/gpt-4o-mini\");\n * ```\n */\nexport function createOpenRouter(\n\tsettings: OpenRouterProviderSettings = {},\n): OpenRouterProvider {\n\tconst { extraBody, ...rest } = settings;\n\n\tconst userUsage =\n\t\textraBody && typeof extraBody === \"object\" && \"usage\" in extraBody\n\t\t\t? (extraBody as Record<string, unknown>).usage\n\t\t\t: undefined;\n\n\treturn baseCreateOpenRouter({\n\t\t...rest,\n\t\textraBody: {\n\t\t\t...extraBody,\n\t\t\tusage: {\n\t\t\t\tinclude: true,\n\t\t\t\t...(userUsage && typeof userUsage === \"object\"\n\t\t\t\t\t? (userUsage as Record<string, unknown>)\n\t\t\t\t\t: {}),\n\t\t\t},\n\t\t},\n\t});\n}\n\nexport type { OpenRouterProvider, OpenRouterProviderSettings };\n","import type { LanguageModelUsage, TextStreamPart, ToolSet } from \"ai\";\nimport chalk from \"chalk\";\n\ntype OpenRouterUsageCost = {\n\tcost?: number;\n\tcostDetails?: {\n\t\tupstreamInferenceCost?: number | null;\n\t};\n};\n\ntype OpenRouterModelResponse = {\n\tdata?: {\n\t\tcontext_length?: unknown;\n\t\ttop_provider?: {\n\t\t\tcontext_length?: unknown;\n\t\t};\n\t};\n};\n\nconst openRouterContextLengthCache = new Map<string, Promise<number | null>>();\n\nfunction getOpenRouterCost(part: TextStreamPart<ToolSet>) {\n\tif (part.type !== \"finish-step\") return null;\n\n\tconst metadataUsage = part.providerMetadata?.openrouter?.usage as\n\t\t| OpenRouterUsageCost\n\t\t| undefined;\n\tconst rawUsage = part.usage.raw as\n\t\t| {\n\t\t\t\tcost?: number;\n\t\t\t\tcost_details?: { upstream_inference_cost?: number | null };\n\t\t }\n\t\t| undefined;\n\n\tconst cost = metadataUsage?.cost ?? rawUsage?.cost;\n\tconst upstreamInferenceCost =\n\t\tmetadataUsage?.costDetails?.upstreamInferenceCost ??\n\t\trawUsage?.cost_details?.upstream_inference_cost;\n\n\t// OpenRouter credit usage reports `cost`; BYOK usage can report `cost: 0`\n\t// with the real provider charge in `upstreamInferenceCost`.\n\tif (cost === 0 && upstreamInferenceCost != null) {\n\t\treturn upstreamInferenceCost;\n\t}\n\n\treturn cost ?? upstreamInferenceCost ?? null;\n}\n\nfunction formatCost(cost: number) {\n\treturn `$${cost.toFixed(6)}`;\n}\n\nfunction formatTokens(tokens: number) {\n\treturn tokens.toLocaleString(\"en-US\");\n}\n\nfunction parsePositiveInteger(value: unknown) {\n\tif (typeof value === \"number\" && Number.isInteger(value) && value > 0) {\n\t\treturn value;\n\t}\n\n\tif (typeof value === \"string\") {\n\t\tconst parsed = Number(value);\n\t\tif (Number.isInteger(parsed) && parsed > 0) {\n\t\t\treturn parsed;\n\t\t}\n\t}\n\n\treturn null;\n}\n\nfunction getOpenRouterContextLengthFromResponse(payload: unknown) {\n\tconst data = (payload as OpenRouterModelResponse).data;\n\treturn (\n\t\tparsePositiveInteger(data?.context_length) ??\n\t\tparsePositiveInteger(data?.top_provider?.context_length)\n\t);\n}\n\nasync function fetchOpenRouterContextLength(modelId: string) {\n\tconst pathModelId = modelId.split(\"/\").map(encodeURIComponent).join(\"/\");\n\tconst response = await fetch(\n\t\t`https://openrouter.ai/api/v1/model/${pathModelId}`,\n\t);\n\n\tif (!response.ok) {\n\t\treturn null;\n\t}\n\n\treturn getOpenRouterContextLengthFromResponse(await response.json());\n}\n\nfunction getOpenRouterContextLength(modelId: string) {\n\tconst cached = openRouterContextLengthCache.get(modelId);\n\tif (cached) return cached;\n\n\tconst promise = fetchOpenRouterContextLength(modelId).catch(() => null);\n\topenRouterContextLengthCache.set(modelId, promise);\n\treturn promise;\n}\n\nfunction getModelId(part: TextStreamPart<ToolSet>) {\n\tif (part.type === \"start-step\") {\n\t\tconst body = part.request.body;\n\t\tif (body && typeof body === \"object\" && \"model\" in body) {\n\t\t\tconst model = body.model;\n\t\t\tif (typeof model === \"string\" && model.length > 0) {\n\t\t\t\treturn model;\n\t\t\t}\n\t\t}\n\t}\n\n\tif (part.type === \"finish-step\") {\n\t\treturn part.response.modelId;\n\t}\n\n\treturn null;\n}\n\nfunction getUsageTokens(usage: LanguageModelUsage | undefined) {\n\tif (!usage) return null;\n\n\t// Context usage is the stopping-point conversation size estimate:\n\t// the last step's input tokens are the full history before the final response,\n\t// and its output tokens are what will be appended to that history. An exact\n\t// \"next empty user message\" count would require another provider tokenization\n\t// pass because the stream does not emit usage for hypothetical future calls.\n\tif (usage.inputTokens != null || usage.outputTokens != null) {\n\t\treturn (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0);\n\t}\n\n\treturn usage.totalTokens ?? null;\n}\n\nfunction formatContextUsage(\n\tusage: LanguageModelUsage | undefined,\n\tcontextWindowTokens: number | null,\n) {\n\tconst tokens = getUsageTokens(usage);\n\tif (tokens == null) return \"context used ?\";\n\n\tif (contextWindowTokens == null) {\n\t\treturn `context used ${formatTokens(tokens)} tokens`;\n\t}\n\n\tconst percentage = (tokens / contextWindowTokens) * 100;\n\tconst formattedPercentage =\n\t\tpercentage > 0 && percentage < 0.01 ? \"<0.01\" : percentage.toFixed(2);\n\n\treturn `${formattedPercentage}% context used (${formatTokens(tokens)} / ${formatTokens(contextWindowTokens)} tokens)`;\n}\n\n/**\n * Pretty-prints every event coming off an AI SDK full stream so you can watch\n * the model think, talk, and call tools in real time — and see live token /\n * cost accounting when the model is served through OpenRouter (see\n * {@link createOpenRouter}).\n *\n * Pass it the `fullStream` from `streamText`:\n *\n * @example\n * ```ts\n * import { streamText } from \"ai\";\n * import { createOpenRouter, logStream } from \"@merkie/agentic\";\n *\n * const openrouter = createOpenRouter();\n *\n * const result = streamText({\n * model: openrouter(\"openai/gpt-4o-mini\"),\n * prompt: \"Explain quantum tunneling in one paragraph.\",\n * });\n *\n * await logStream(result.fullStream);\n * ```\n */\nexport async function logStream(\n\tstream: AsyncIterable<TextStreamPart<ToolSet>>,\n): Promise<void> {\n\t// Track which \"channel\" (reasoning vs. text) we're currently writing to so we\n\t// only print a header when it changes, and stream deltas onto the same line.\n\tlet active: \"reasoning\" | \"text\" | null = null;\n\tlet openRouterCost = 0;\n\tlet hasOpenRouterCost = false;\n\tlet latestStepUsage: LanguageModelUsage | undefined;\n\tlet contextWindowPromise: Promise<number | null> | undefined;\n\n\tconst endActive = () => {\n\t\tif (active) process.stdout.write(\"\\n\");\n\t\tactive = null;\n\t};\n\n\tconst startContextWindowFetch = (modelId: string | null) => {\n\t\tif (!modelId || contextWindowPromise) return;\n\t\tcontextWindowPromise = getOpenRouterContextLength(modelId);\n\t};\n\n\tfor await (const part of stream) {\n\t\tswitch (part.type) {\n\t\t\tcase \"start-step\":\n\t\t\t\tendActive();\n\t\t\t\tstartContextWindowFetch(getModelId(part));\n\t\t\t\tconsole.log(chalk.dim(\"──────── step ────────\"));\n\t\t\t\tbreak;\n\n\t\t\tcase \"finish-step\": {\n\t\t\t\tlatestStepUsage = part.usage;\n\t\t\t\tstartContextWindowFetch(getModelId(part));\n\t\t\t\tconst cost = getOpenRouterCost(part);\n\t\t\t\tif (cost != null) {\n\t\t\t\t\topenRouterCost += cost;\n\t\t\t\t\thasOpenRouterCost = true;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"reasoning-delta\":\n\t\t\t\tif (active !== \"reasoning\") {\n\t\t\t\t\tendActive();\n\t\t\t\t\tprocess.stdout.write(chalk.magenta.bold(\"🧠 reasoning \"));\n\t\t\t\t\tactive = \"reasoning\";\n\t\t\t\t}\n\t\t\t\tprocess.stdout.write(chalk.magenta(part.text));\n\t\t\t\tbreak;\n\n\t\t\tcase \"text-delta\":\n\t\t\t\tif (active !== \"text\") {\n\t\t\t\t\tendActive();\n\t\t\t\t\tprocess.stdout.write(chalk.white.bold(\"💬 message \"));\n\t\t\t\t\tactive = \"text\";\n\t\t\t\t}\n\t\t\t\tprocess.stdout.write(chalk.white(part.text));\n\t\t\t\tbreak;\n\n\t\t\tcase \"tool-call\":\n\t\t\t\tendActive();\n\t\t\t\tconsole.log(\n\t\t\t\t\tchalk.cyan.bold(`🔧 tool call ${part.toolName}`) +\n\t\t\t\t\t\tchalk.cyan(`(${JSON.stringify(part.input)})`),\n\t\t\t\t);\n\t\t\t\tbreak;\n\n\t\t\tcase \"tool-result\":\n\t\t\t\tendActive();\n\t\t\t\tconsole.log(\n\t\t\t\t\tchalk.green.bold(`✅ tool result ${part.toolName} `) +\n\t\t\t\t\t\tchalk.green(JSON.stringify(part.output)),\n\t\t\t\t);\n\t\t\t\tbreak;\n\n\t\t\tcase \"tool-error\":\n\t\t\t\tendActive();\n\t\t\t\tconsole.log(\n\t\t\t\t\tchalk.red.bold(`❌ tool error ${part.toolName} `) +\n\t\t\t\t\t\tchalk.red(String(part.error)),\n\t\t\t\t);\n\t\t\t\tbreak;\n\n\t\t\tcase \"error\":\n\t\t\t\tendActive();\n\t\t\t\tconsole.log(chalk.red.bold(\"‼️ stream error \"), part.error);\n\t\t\t\tbreak;\n\n\t\t\tcase \"finish\": {\n\t\t\t\tendActive();\n\t\t\t\tconst contextWindowTokens = contextWindowPromise\n\t\t\t\t\t? await contextWindowPromise\n\t\t\t\t\t: null;\n\t\t\t\tconsole.log(\n\t\t\t\t\tchalk.dim(\n\t\t\t\t\t\t`\\n── done (${part.finishReason}) · ${formatContextUsage(latestStepUsage, contextWindowTokens)}${hasOpenRouterCost ? ` · cost ${formatCost(openRouterCost)}` : \"\"} ──`,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n}\n\nexport default logStream;\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@merkie/agentic",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Batteries-included helpers for building LLM-powered apps with the Vercel AI SDK and OpenRouter — automatic cost/usage tracking and pretty stream logging out of the box.",
5
5
  "type": "module",
6
6
  "license": "MIT",