@combycode/llm-sdk 2.0.0 → 2.1.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.
@@ -6,3 +6,22 @@ import type { JsonSchema } from './tools';
6
6
  * reject schemas without this explicit flag. Safe across all providers.
7
7
  */
8
8
  export declare function ensureAdditionalProperties(schema: JsonSchema): JsonSchema;
9
+ /** Whose strict-mode rules to judge a schema against. */
10
+ export type StrictDialect = 'openai' | 'anthropic';
11
+ /** Can this schema satisfy the provider's strict mode AS WRITTEN?
12
+ *
13
+ * Strict mode is worth defaulting to — it is what makes a provider constrain the
14
+ * tool name and arguments DURING generation rather than checking after. But the
15
+ * two providers constrain different things, and a schema that violates either is
16
+ * rejected with a 400, not quietly degraded:
17
+ *
18
+ * openai every property must appear in `required`, at every nesting level
19
+ * anthropic a set of validation keywords is simply unsupported
20
+ *
21
+ * So strict is requested only where it can be honoured. The alternative — rewriting
22
+ * the schema to fit, promoting optional properties to required-and-nullable — changes
23
+ * what the tool actually receives, and the receiving end is the caller's code. */
24
+ export declare function strictSupport(schema: JsonSchema | undefined, dialect: StrictDialect): {
25
+ ok: boolean;
26
+ reason?: string;
27
+ };
@@ -21,11 +21,11 @@ import type { RequestContext } from '../types/request-context';
21
21
  import type { RateLimiterConfig } from './rate-limiter';
22
22
  import type { QueueConfig } from './request-queue';
23
23
  import { QueueState } from './queue-state';
24
- import type { RetryConfig } from './queue-state-config';
24
+ import type { RetryPolicyOverride } from './queue-state-config';
25
25
  import type { ConnectFn, FetchFn, HttpRequest, HttpResponse, RealtimeConnection, SSEEvent, WsRequest } from './types';
26
26
  export interface QueueSettings {
27
27
  limits?: Partial<RateLimiterConfig>;
28
- retry?: Partial<RetryConfig>;
28
+ retry?: RetryPolicyOverride;
29
29
  queue?: Partial<QueueConfig>;
30
30
  }
31
31
  export interface NetworkEngineConfig {
@@ -39,6 +39,12 @@ export interface NetworkEngineConfig {
39
39
  connect?: ConnectFn;
40
40
  /** Pre-configured per-queue settings. Looked up by queueName at queue creation. */
41
41
  queues?: Record<string, QueueSettings>;
42
+ /** Retry policy inherited by EVERY queue this engine creates.
43
+ *
44
+ * Retry is a cross-cutting setting, not something a caller should thread through each call, so
45
+ * it is configured once (`createEngine({ retry })`) and applies everywhere. Three layers,
46
+ * narrowest wins: `HttpRequest.retry` > `queues[name].retry` > this. */
47
+ retry?: RetryPolicyOverride;
42
48
  }
43
49
  /** Optional context for fetch/fetchStream. RequestContext + per-call overrides. */
44
50
  export interface FetchOptions {
@@ -52,6 +58,8 @@ export declare class NetworkEngine {
52
58
  readonly hooks: HookBus;
53
59
  private readonly fetchFn;
54
60
  private readonly connectFn;
61
+ /** Engine-wide retry policy, inherited by every queue created from here. */
62
+ private readonly defaultRetry;
55
63
  private readonly settings;
56
64
  private readonly queues;
57
65
  constructor(config?: NetworkEngineConfig);
@@ -22,6 +22,14 @@ export interface BackoffConfig {
22
22
  multiplier: number;
23
23
  jitter: number;
24
24
  }
25
+ /** A retry policy stated as a partial override of another one.
26
+ *
27
+ * `Partial<RetryConfig>` only makes the TOP-level keys optional, so it still demands a complete
28
+ * `backoff` object — which makes "override one knob, keep the rest" inexpressible even though the
29
+ * merge has always supported it. Engine-level and queue-level overrides take this instead. */
30
+ export type RetryPolicyOverride = Omit<Partial<RetryConfig>, 'backoff'> & {
31
+ backoff?: Partial<BackoffConfig>;
32
+ };
25
33
  export interface ErrorRetryConfig {
26
34
  retryable?: boolean;
27
35
  maxRetries?: number;
@@ -34,7 +42,7 @@ export interface QueueStateConfig {
34
42
  fetch: FetchFn;
35
43
  hooks: HookBus;
36
44
  limits: RateLimiterConfig;
37
- retry?: Partial<RetryConfig>;
45
+ retry?: RetryPolicyOverride;
38
46
  queue?: Partial<QueueConfig>;
39
47
  }
40
48
  export declare const Priority: {
@@ -43,4 +51,4 @@ export declare const Priority: {
43
51
  readonly BACKGROUND: 2;
44
52
  readonly LOW: 3;
45
53
  };
46
- export declare function mergeRetry(overrides?: Partial<RetryConfig>): RetryConfig;
54
+ export declare function mergeRetry(overrides?: RetryPolicyOverride): RetryConfig;
@@ -13,6 +13,8 @@ export declare class CostCollector {
13
13
  private budgets;
14
14
  private triggeredThresholds;
15
15
  private _runningTotal;
16
+ /** Models already reported as unpriced, so the warning fires once rather than per call. */
17
+ private warnedUnpriced;
16
18
  private watchedAgents;
17
19
  private unsub;
18
20
  private unsubMedia;
@@ -36,6 +38,16 @@ export declare class CostCollector {
36
38
  import(entries: CostEntry[]): void;
37
39
  private handleMediaGenerated;
38
40
  private handleCompletion;
41
+ /** A total of exactly 0 because the model is not in the catalog reads identically to a
42
+ * total of 0 because the call was free — and it silently under-counts every budget
43
+ * and report built on it. `source: 'unknown'` already records the difference per
44
+ * entry, but nothing aggregated it, so a whole benchmark run once reported $0.00000
45
+ * for a live provider and looked like a free arm.
46
+ *
47
+ * Fires once per provider/model: an unpriced model is a configuration fact, not a
48
+ * per-request event, and repeating it on every call would train the reader to ignore
49
+ * it. Free calls are priced 'calculated' with an explicit note, so they stay silent. */
50
+ private noteIfUnpriced;
39
51
  private checkBudgets;
40
52
  private filterEntries;
41
53
  private groupBy;
@@ -39,4 +39,12 @@ export interface CostSummary {
39
39
  reasoning: number;
40
40
  };
41
41
  entries: number;
42
+ /** How many of `entries` could not be priced — no catalog entry for the model, or no
43
+ * applicable rate. They count as 0 in every field above, so a summary without this
44
+ * number cannot tell "this run was cheap" from "this run was never priced". Free
45
+ * calls are NOT counted here: they are priced, at zero. */
46
+ unpriced: number;
47
+ /** The distinct `provider/model` values behind `unpriced`, so the gap is actionable
48
+ * rather than merely visible. Empty when everything was priced. */
49
+ unpricedModels: string[];
42
50
  }
@@ -21,7 +21,14 @@ export declare class McpResultCache {
21
21
  * different entries. */
22
22
  static key(method: string, params?: unknown): string;
23
23
  get(key: string, now?: number): unknown | undefined;
24
- /** Store only when the server actually asked for it. Returns whether anything was stored. */
24
+ /** Store only when the server actually asked for it. Returns whether anything was stored.
25
+ *
26
+ * A non-positive `ttlMs` is an instruction, not a missing value: the server is saying *do not
27
+ * reuse this*. Any entry already held under that key is dropped, so the next `get` re-fetches.
28
+ * Without the eviction the hint is inert — a server that first said "cache for 60s" and then
29
+ * says "stale now" would keep being answered from the stale entry for the rest of the original
30
+ * TTL. Absent hints are different and must stay different: they carry no instruction, so an
31
+ * existing entry is left alone and pre-2026 servers behave exactly as before. */
25
32
  set(key: string, value: unknown, hints: McpCacheHints | undefined, now?: number): boolean;
26
33
  /** Drop everything — e.g. after a `*_changed` notification says the server moved on. */
27
34
  clear(): void;
@@ -8,6 +8,8 @@ export interface McpToolAdapterOptions {
8
8
  /** Validate `structuredContent` against the tool's `outputSchema`; on mismatch
9
9
  * the tool returns an error string instead of the content. Default false. */
10
10
  validateOutput?: boolean;
11
+ /** Register the tool without declaring it — see `AgentTool.lazy`. */
12
+ lazy?: boolean;
11
13
  }
12
14
  /** Map a `tools/call` result to our tool-result shape: a plain string when the
13
15
  * content is text-only, else a ContentPart[] (images/audio kept as base64).
@@ -14,6 +14,40 @@ export interface JsonRpcResponse {
14
14
  result?: unknown;
15
15
  error?: JsonRpcError;
16
16
  }
17
+ /** Behavioural hints a server publishes about a tool. Advisory and UNVERIFIED — a
18
+ * server asserts them about itself — so they inform a host's UX (confirm before a
19
+ * destructive call) and must never be treated as a security boundary.
20
+ *
21
+ * Open (CONSTITUTION R1): the spec gains hints over time and an unknown one must
22
+ * survive rather than be dropped. */
23
+ export interface McpToolAnnotations {
24
+ title?: string;
25
+ /** The tool does not modify its environment. Default false. */
26
+ readOnlyHint?: boolean;
27
+ /** The tool may perform destructive updates rather than only additive ones. */
28
+ destructiveHint?: boolean;
29
+ /** Repeated calls with the same arguments have no additional effect. */
30
+ idempotentHint?: boolean;
31
+ /** The tool touches an open world (the internet) rather than a closed one. */
32
+ openWorldHint?: boolean;
33
+ [hint: string]: unknown;
34
+ }
35
+ /** An icon a host may render beside the tool. Display only. */
36
+ export interface McpToolIcon {
37
+ src: string;
38
+ mimeType?: string;
39
+ sizes?: string[];
40
+ theme?: 'light' | 'dark' | (string & {});
41
+ }
42
+ /** How the tool must be invoked.
43
+ *
44
+ * `taskSupport: 'required'` means the client MUST call it as a task rather than a
45
+ * plain `tools/call`. Task invocation is not implemented here, so such a tool
46
+ * cannot be called through this client — carrying the field lets a caller SEE
47
+ * that instead of discovering it as a server error. Absent means `'forbidden'`. */
48
+ export interface McpToolExecution {
49
+ taskSupport?: 'required' | 'optional' | 'forbidden' | (string & {});
50
+ }
17
51
  export interface McpToolDef {
18
52
  name: string;
19
53
  description?: string;
@@ -22,6 +56,15 @@ export interface McpToolDef {
22
56
  inputSchema: Record<string, unknown>;
23
57
  /** JSON Schema for the tool's `structuredContent` output (optional). */
24
58
  outputSchema?: Record<string, unknown>;
59
+ /** Advisory behavioural hints. HOST-facing, never sent to the model: no
60
+ * provider's function-tool schema has a field that could carry them. */
61
+ annotations?: McpToolAnnotations;
62
+ /** Display icons for a host UI. */
63
+ icons?: McpToolIcon[];
64
+ /** Invocation requirements; see {@link McpToolExecution}. */
65
+ execution?: McpToolExecution;
66
+ /** Spec-defined passthrough metadata. */
67
+ _meta?: Record<string, unknown>;
25
68
  }
26
69
  /** A content block in a `tools/call` result. Open union — unknown types ignored. */
27
70
  export type McpContentBlock = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@combycode/llm-sdk",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Unified, pluggable AI SDK for accessing the LLMs of every major provider (Anthropic, OpenAI, Google, xAI, OpenRouter) through one API. Cross-environment: Node, Bun, and the browser.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -42,7 +42,10 @@
42
42
  "lint:fix": "biome lint --write src tests",
43
43
  "format": "biome format --write src tests",
44
44
  "check": "biome check src tests",
45
- "check:fix": "biome check --write src tests"
45
+ "check:fix": "biome check --write src tests",
46
+ "gate": "node ../../quality-gate/gate.mjs",
47
+ "gate:selftest": "node ../../quality-gate/selftest.mjs",
48
+ "gate:snapshot": "node ../../quality-gate/gate.mjs --only api-snapshot --update"
46
49
  },
47
50
  "devDependencies": {
48
51
  "@biomejs/biome": "^2.4.13",