@countly/ai-sdk-core

中文文档
0.0.6发布于 2026/08/03TypeScriptESM + CJS零依赖近 30 天下载 1.8万

Core utilities for Countly AI SDK

原始使用文档
文档目录12 节
  1. The two rules core enforces
  2. schema_version = 2
  3. What it provides
  4. Token identities
  5. Cost is disjoint
  6. Identity fields
  7. API host
  8. Direct usage
  9. Feedback events
  10. Transport limits
  11. Full documentation
  12. License

@countly/ai-sdk-core

Shared transport, data model, cost calculation, and event builders for the Countly AI SDK.

This is the foundation package. You usually don't install it directly — every adapter pulls it in automatically:

Core owns the wire contract. Adapters only decide what they can observe; core decides what the wire looks like.

The two rules core enforces

Every field on every one of the four keys obeys one of these:

  • RULE A — dimensions are always present on the key that owns them. When an adapter cannot observe one, core writes an explicit sentinel ("unknown", "not_reported") — never a guess, never an absence. Absence of a dimension therefore only ever means wrong key, which the key already told you.
  • RULE B — measures are omitted when they were not measured, and travel with an always-present string marker that says why. No booleans on the wire.

The two markers:

MarkerValuesGoverns
usage_statereported | not_reportedusage_* and every cost_*
cost_pricedpriced | priced_modality_approx | unpriced_model | no_usagecost_*

Consequence to design consumers around: a $0 cost is never emitted to mean "we have no price for this model", and a 0 token count is never emitted to mean "the provider told us nothing". Those cases omit the measure and set the marker. Cost keys are present only when cost_priced is priced or priced_modality_approx.

schema_version = 2

SCHEMA_VERSION is exported and stamped as an integer on every row of every key, so a consumer can range-query it (a semver string cannot). Branch on it, not on sdk_version.

What it provides

  • Unified data model (RawExtractionResult) — OpenTelemetry GenAI-aligned, every field optional: an adapter that cannot observe something omits it and core applies the sentinel or the marker
  • Event builders — [CLY]_llm_interaction (one row per generation), [CLY]_llm_tool_used, [CLY]_llm_tool_usage_parameter, [CLY]_llm_interaction_feedback. All four carry the same envelope
  • Token identities, held by construction rather than by trust (below)
  • Cost calculation — built-in pricing for 47 models with per-model override, decomposed into disjoint components
  • API host classification — classifyApiHostType / extractApiHost / describeApiHost
  • HTTP transport — buffered POST /i, one batch_id idempotency key per batch generated outside the retry loop, request timeout, bounded buffer, unref'd flush timer, dead-letter with retry-on-recovery, onDropped
  • Finish reason normalization — stop | length | tool_calls | content_filter | error | other | not_reported
  • Provider normalization — normalizeProvider reduces "openai.responses" to "openai" (the raw id is preserved in provider_metadata.provider_id)
  • Error categorization — rate_limit | context_length | content_filter | timeout | auth_error | api_error

Token identities

Core derives the sub-buckets rather than trusting them, so these hold on the wire regardless of what an adapter computed:

usage_total  == usage_input + usage_output
usage_input  == usage_input_fresh + usage_cache_read + usage_cache_write + input audio + input image
usage_output == usage_output_text + usage_reasoning + output audio + output image

usage_input is the provider's total input count — inclusive of both cache read and cache write — so it reconciles with provider dashboards. usage_input_fresh is the residual. Reasoning is a subset of output, not an addition to it.

Each sub-bucket is clamped against what the previous ones already claimed, so it can never exceed its total. A clamp means the adapter broke an identity; in debug mode core logs it loudly.

Cost is disjoint

CostResult splits into components in which every token is billed exactly once, so cost_total is their plain sum:

cost_input (fresh input only) + cost_cache_read + cost_cache_write + cost_output (excluding separately-priced reasoning) + cost_reason

cost_cache_saved is reported alongside as what the cache reads saved against the fresh input rate — it is not part of the sum.

Two money bugs this fixes: cache-write tokens were billed on top of an usage_input that already contained them (2.25x instead of 1.25x on Anthropic), and the full output count was charged at the output rate with reasoning then added at reason_per_mtok — overstating every Gemini thinking-model call by up to 2x.

Deleted: cost_cached (never had a producer) → use cost_cache_read / cost_cache_saved.

Identity fields

FieldMeaning
event_iduuid, the only primary key of a row
run_idthe turn/call identity — the join column for all cross-family scoping
prompt_ida foreign key to the parent fact row, not an identity
generation_index0-based position of this generation within the run
run_latency_totalthe turn's end-to-end wall clock (= latency_total for single-call adapters)
parent_event_keywhich row prompt_id points at: "[CLY]_llm_interaction" or "run"

run_id resolves first-match-wins: raw.run_id → EventContext.run_id → EventContext.prompt_id (the host's getPromptId()) → a generated uuid.

API host

import { describeApiHost } from "@countly/ai-sdk-core";

const raw = { ...describeApiHost(client.baseURL) };
// → { api_host_type: "gateway", api_host: "ai-gateway.count.ly" }

api_host_type ∈ vendor_direct | azure_openai | gateway | local_infra | unknown. api_host is the hostname only — scheme, credentials, port, path and query are stripped and never emitted, because a provider path routinely carries api keys and deployment ids.

Deleted: api_base_url (raw URL — credential-leak risk).

Direct usage

If you're building a custom integration not covered by an existing adapter:

import {
  resolveConfig,
  createTransport,
  buildAllEvents,
  generatePromptId,
  describeApiHost,
} from "@countly/ai-sdk-core";

const config = resolveConfig({
  appKey: "YOUR_APP_KEY",
  url: "https://your-countly-server.com",
});
const transport = createTransport(config);

const raw = {
  provider: "my-llm",
  model: "my-model",
  ...describeApiHost("https://llm.internal.example.com/v1"),
  usage_input: 100,   // TOTAL input, including any cache buckets
  usage_output: 50,   // TOTAL output, including any reasoning
  latency_total: 1200,
  status: "success" as const,
};

const events = buildAllEvents(raw, config, {
  prompt_id: generatePromptId(), // becomes run_id
  sdk_adapter: "custom",
});
transport.enqueue(events);

Two things to know when writing an adapter:

  • Omit, never fabricate. Leave model/provider/status absent and core writes "unknown". Leave latency_total absent rather than sending 0. Leave RawToolCall.status absent when you only saw the model request the tool — core emits "unknown" rather than a fabricated success.
  • saw_generation: false suppresses the interaction row entirely. Use it for a trace that contained no generation: tool rows still emit, scoped by run_id. This is how "a trace with no generation emits nothing" is implemented.

Feedback events

Feedback is not auto-collected — wire it from your UI's thumbs-up/down handler. Use createFeedbackTracker (also re-exported by every adapter package) with the id captured via the onPrompt config callback:

import { createFeedbackTracker } from "@countly/ai-sdk-core";

const feedback = createFeedbackTracker(
  { appKey: "YOUR_APP_KEY", url: "https://your-countly-server.com" },
  { sdk_adapter: "openai" }, // stamp which adapter produced the rated interaction
);

// Rate the whole turn (PromptInfo.prompt_id === PromptInfo.run_id):
feedback.track({ prompt_id: info.prompt_id, rating: "thumbs_up", deviceId: userId });

// Rate ONE generation of a multi-step turn:
feedback.track({ prompt_id: info.event_id, run_id: info.run_id, rating: "thumbs_down" });

parent_event_key records which of the two you did, and rating_kind (binary | score | category | other) is derived so a numeric score can no longer be averaged into a thumbs-up rate.

For custom integrations that need full control, the lower-level buildFeedbackEvent + createTransport primitives remain available.

Transport limits

ConfigDefaultWhy it exists
flushInterval10_000 mstimer is unref'd, so a CLI or serverless handler can still exit
maxBatchSize20flushes are serialised; shutdown() loops until the buffer is empty
maxBufferSize10_000an observability SDK must not OOM the app it instruments — oldest dropped first
requestTimeout15_000 mswithout it a hung socket blocks flush() and therefore shutdown() forever
textPreviewLength200 (max 8192)UX knob for previews
paramValueLength1024 (max 8192)ingest-safety invariant: one multi-MB tool argument dead-letters the whole batch
onDropped(events, reason)—the only way a host learns its counts are under-reported (send_failed | buffer_full)

transport.stats() returns { buffered, deadLettered, dropped }.

Truncation is visible: truncateText cuts on a code-point boundary (a slice by code unit split surrogate pairs and produced invalid UTF-8), appends …, and the row carries a *_state field set to complete or truncated.

timestamp is in milliseconds and hour/dow are UTC, computed once per turn so every row of a turn is byte-identical. They used to come from the container's local timezone, which disagreed with the UTC epoch in the same row.

Full documentation

See the Countly AI SDK repository for the complete wire format, the adapter capability matrix, observability levels (0/1/2), per-user attribution via AsyncLocalStorage, privacy controls, and Countly plugin integration (Drill, Funnels, Cohorts, APM, Crash Analytics).

License

MIT

版本与发布记录

@countly/ai-sdk-core 共发布 6 个版本,首次发布于 2026/05/12;过去 12 个月发布了 6 个版本。

版本发布日期距今
0.0.6latest2026/08/033 个月前
0.0.52026/07/233 个月前
0.0.42026/07/163 个月前
0.0.32026/07/084 个月前
0.0.22026/05/125 个月前
0.0.12026/05/125 个月前

关于 @countly/ai-sdk-core 的常见问题

@countly/ai-sdk-core 是什么?

Core utilities for Countly AI SDK

如何安装 @countly/ai-sdk-core?

在项目目录运行 npm install @countly/ai-sdk-core,或使用 pnpm add / yarn add / bun add。

@countly/ai-sdk-core 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@countly/ai-sdk-core 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@countly/ai-sdk-core 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js ^20.19.0 || >=22.12.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@countly/ai-sdk-core 可以商用吗?

@countly/ai-sdk-core 使用 未声明 许可。package.json 没有声明许可证。没有许可证的代码默认保留全部权利,商用前请联系作者或查看仓库中的 LICENSE 文件。以上为概括,不构成法律意见,请以许可原文为准。

@countly/ai-sdk-core 的下载量有多少?

根据 npm 官方下载统计,@countly/ai-sdk-core 在 2026/10/11 – 2026/11/09 这 30 天内被下载 17,626 次,日均约 588 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@countly/ai-sdk-core 还在维护吗?

最新版本 0.0.6 发布于 2026/08/03(3 个月前);至今共发布 6 个版本,其中过去 12 个月发布了 6 个;首次发布于 2026/05/12。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。