@combycode/llm-sdk 2.0.1 → 2.2.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.
@@ -50,17 +50,26 @@ type InferParam<S> = S extends 'string' ? string : S extends 'number' | 'integer
50
50
  } ? number : S extends {
51
51
  type: 'boolean';
52
52
  } ? boolean : unknown;
53
- type InferArgs<P extends Record<string, ParamSpec>> = {
54
- [K in keyof P]: InferParam<P[K]>;
53
+ /** Keys named in `optional` are optional HERE TOO. Typing them as always-present is
54
+ * a lie the compiler then helps enforce: `args.unit.toUpperCase()` typechecks and
55
+ * throws at runtime whenever the model omits the argument — which, for an argument
56
+ * declared optional, is the expected case rather than the edge one. */
57
+ type InferArgs<P extends Record<string, ParamSpec>, O extends keyof P = never> = {
58
+ [K in Exclude<keyof P, O>]: InferParam<P[K]>;
59
+ } & {
60
+ [K in Extract<O, keyof P>]?: InferParam<P[K]>;
55
61
  };
56
- export interface DefineToolInput<P extends Record<string, ParamSpec>> {
62
+ export interface DefineToolInput<P extends Record<string, ParamSpec>, O extends keyof P & string = never> {
57
63
  name: string;
58
64
  description: string;
59
65
  /** Object spec — keys are arg names. All keys are treated as required by
60
66
  * default; mark optional ones via `optional: ['x']`. */
61
67
  params: P;
62
- optional?: ReadonlyArray<keyof P & string>;
63
- execute: (args: InferArgs<P>, context: ToolExecutionContext) => Promise<string | ContentPart[]> | string | ContentPart[];
68
+ optional?: readonly O[];
69
+ /** Register the tool without declaring it the model finds it via `tool_search` and
70
+ * calls it through `call_tool`. See `AgentTool.lazy`. */
71
+ lazy?: boolean;
72
+ execute: (args: InferArgs<P, O>, context: ToolExecutionContext) => Promise<string | ContentPart[]> | string | ContentPart[];
64
73
  }
65
- export declare function defineTool<P extends Record<string, ParamSpec>>(input: DefineToolInput<P>): AgentTool;
74
+ export declare function defineTool<P extends Record<string, ParamSpec>, const O extends keyof P & string = never>(input: DefineToolInput<P, O>): AgentTool;
66
75
  export {};
@@ -18,11 +18,13 @@
18
18
  import { AgentBus } from '../bus/agent-bus';
19
19
  import { HookBus } from '../bus/hook-bus';
20
20
  import type { ProviderName } from '../llm/types/provider';
21
- import { NetworkEngine } from '../network/engine';
21
+ import { NetworkEngine, type QueueSettings } from '../network/engine';
22
+ import type { RetryPolicyOverride } from '../network/queue-state-config';
22
23
  import type { EngineConnect, EngineFetch, EngineFetchStream, FetchFn } from '../network/types';
23
24
  import { Cache } from '../plugins/cache/cache';
24
25
  import { CostCollector } from '../plugins/cost-collector/collector';
25
26
  import { ModelCatalog } from '../plugins/model-catalog/catalog';
27
+ import { TelemetryAdapter, type TelemetryAdapterOptions } from '../plugins/telemetry/telemetry';
26
28
  import type { Persistence } from '../plugins/persistence/types';
27
29
  export interface EngineHandle {
28
30
  /** Trace session id — minted once for this engine (the holder), shared by
@@ -53,6 +55,10 @@ export interface EngineHandle {
53
55
  /** CostCollector — subscribes to onCompletion + onMediaGenerated and
54
56
  * prices via catalog. Call `engine.cost.total()` for a running tally. */
55
57
  cost: CostCollector;
58
+ /** TelemetryAdapter — present only when `telemetry` was configured, because an
59
+ * unwanted one would sit there accumulating spans for a process that never reads
60
+ * them. Subscribe with `engine.telemetry.onTrace(...)`, or use it directly. */
61
+ telemetry: TelemetryAdapter | null;
56
62
  /** API keys per provider. Helpers (createLLM, createAgent,
57
63
  * createMediaOutput, complete) read these to wire LLM clients without
58
64
  * the caller passing apiKey explicitly. */
@@ -94,6 +100,35 @@ export interface EngineConfig {
94
100
  /** Per-provider API keys. Helpers consult this when no apiKey is passed
95
101
  * alongside `model: 'provider/...'`. */
96
102
  apiKeys?: Partial<Record<ProviderName, string>>;
103
+ /** Observability. Omitted → no adapter is built and nothing is collected.
104
+ *
105
+ * ```ts
106
+ * createEngine({
107
+ * telemetry: {
108
+ * types: ['agent', 'tool'], // http/llm detail stays out
109
+ * content: 'none', // conversation text off by default
110
+ * sample: 0.05, // per trace, not per span
111
+ * onTrace: (e) => myPipeline.push(e),
112
+ * },
113
+ * });
114
+ * ```
115
+ */
116
+ telemetry?: TelemetryAdapterOptions;
117
+ /** Retry policy for every request this engine makes.
118
+ *
119
+ * Retry is a cross-cutting concern, so it is configured once here rather than threaded through
120
+ * each call. Anything omitted falls back to the built-in policy (`DEFAULT_RETRY`).
121
+ *
122
+ * ```ts
123
+ * createEngine({ retry: { maxRetries: 5, backoff: { initialMs: 200, maxMs: 8_000 } } });
124
+ * ```
125
+ *
126
+ * Three layers, narrowest wins: `HttpRequest.retry` (one request) > `queues[name].retry`
127
+ * (one provider queue) > this (everything). */
128
+ retry?: RetryPolicyOverride;
129
+ /** Per-queue overrides, keyed by queue name (`provider/model` unless routed otherwise).
130
+ * Use when one provider needs a different policy from the rest. */
131
+ queues?: Record<string, QueueSettings>;
97
132
  /** Register this engine as the default for `coreRegistry.get()` (used by
98
133
  * helpers when the caller doesn't pass an explicit `engine`). Defaults to
99
134
  * `true` so `createEngine({ ... })` followed by helper calls just works.
@@ -40,6 +40,15 @@ export interface ConnectMcpOptions {
40
40
  roots?: McpRoot[] | (() => McpRoot[] | Promise<McpRoot[]>);
41
41
  /** Validate tool `structuredContent` against the tool's `outputSchema`. */
42
42
  validateOutput?: boolean;
43
+ /** Register this server's tools WITHOUT declaring them: the model finds them with
44
+ * `tool_search` and calls them through `call_tool`. Exposure only — every tool is
45
+ * still registered, namespaced and collision-checked exactly as today.
46
+ *
47
+ * This is the common case for the feature, since an MCP server is where a large tool
48
+ * block usually comes from. Measured over 308 tools: identical correctness, −72% cost
49
+ * per task on claude-haiku-4.5 and −97% on gpt-5.4-nano, for one extra round trip.
50
+ * Not worth it for a small server — see `AgentTool.lazy`. */
51
+ lazy?: boolean;
43
52
  /** Send a `ping` every N ms to keep the connection alive (0 = off).
44
53
  * Ignored on a 2026-07-28 session, where `ping` no longer exists. */
45
54
  keepAliveMs?: number;
@@ -9,5 +9,17 @@
9
9
  *
10
10
  * The moderations endpoint is FREE; an honest-zero cost entry is always emitted
11
11
  * so the cost ledger has a record of each call. HTTP flows through engine.fetch. */
12
- import type { ModerateOptions, ModerationResult } from './moderate-types';
12
+ import type { ModerateOptions, ModerationContentPart, ModerationResult } from './moderate-types';
13
+ /** One input in, one result out: a single string, or one content-part array that
14
+ * together forms a single multimodal item. */
15
+ export declare function moderate(opts: ModerateOptions & {
16
+ input: string | ModerationContentPart[];
17
+ }): Promise<ModerationResult>;
18
+ /** Many inputs in, one result each, in the same order. */
19
+ export declare function moderate(opts: ModerateOptions & {
20
+ input: string[] | ModerationContentPart[][];
21
+ }): Promise<ModerationResult[]>;
22
+ /** Fallback for a caller holding the wide `ModerateOptions['input']` union: the
23
+ * arity is only knowable at runtime, so the union comes back. Kept so existing
24
+ * code that already narrows the result keeps compiling. */
13
25
  export declare function moderate(opts: ModerateOptions): Promise<ModerationResult | ModerationResult[]>;
@@ -15,6 +15,7 @@
15
15
  * LLMClient.complete. Either way the helper destroys its created client
16
16
  * before returning so callers don't leak. */
17
17
  import type { AgentTool } from '../agent/types';
18
+ import type { CacheConfig } from '../llm/types/request';
18
19
  import type { LLMClientConfig } from '../llm/client-config';
19
20
  import type { AudioOptions } from '../llm/types/audio';
20
21
  import type { ContentPart, Message } from '../llm/types/messages';
@@ -51,6 +52,12 @@ export interface CompleteOptions {
51
52
  /** Generation control. */
52
53
  maxTokens?: number;
53
54
  temperature?: number;
55
+ /** Top-k sampling. Emitted only where the wire accepts it (Anthropic, Google, xAI,
56
+ * OpenRouter) and dropped for OpenAI, which defines no top-k. */
57
+ topK?: number;
58
+ /** Best-effort deterministic sampling. Dropped on Anthropic, which has no seed.
59
+ * Determinism is never guaranteed. */
60
+ seed?: number;
54
61
  structured?: {
55
62
  schema: Record<string, unknown>;
56
63
  name?: string;
@@ -63,6 +70,13 @@ export interface CompleteOptions {
63
70
  /** Service tier for this call. Also settable as a `model:tier` suffix (e.g.
64
71
  * `anthropic/claude-opus-4.8:priority`); an explicit value here wins. */
65
72
  serviceTier?: ServiceTier;
73
+ /** Provider prompt caching: `'auto'`, `'off'`, or which segments to mark
74
+ * (`{ system: true, tools: true }`).
75
+ *
76
+ * It matters most exactly where this helper is convenient — a long system
77
+ * prompt or a large tool block, both of which sit at the front of the request
78
+ * and are the cheapest part to cache. */
79
+ cache?: CacheConfig;
66
80
  /** Optional engine to use. Falls back to coreRegistry default. */
67
81
  engine?: EngineHandle;
68
82
  /** Provider-specific request options (e.g. `{ openrouter: { models: [...] } }`). */
@@ -83,6 +97,11 @@ export interface CompleteResult<T = unknown> {
83
97
  * otherwise `undefined`. The generic on `complete<T>(...)` types this. */
84
98
  parsed?: T;
85
99
  response: CompletionResponse;
100
+ /** In-band provider failure. Some providers report a failed generation IN the
101
+ * response rather than by throwing (OpenAI Responses `status: 'failed'`, Google
102
+ * Interactions likewise), so a caller that only catches sees empty text and no
103
+ * exception — a failure indistinguishable from a successful empty answer. */
104
+ error?: CompletionResponse['error'];
86
105
  /** Fetch a hosted-tool output file (from `response.files`): bytes (`Blob`) +
87
106
  * `name` / `mimeType` / `size` — bound to the SAME model + key this call used. */
88
107
  retrieveFile(file: FileOutput): Promise<RetrievedFile>;