@niscorp/signal 0.1.3 → 0.1.5

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 CHANGED
@@ -1,5 +1,23 @@
1
1
  # @niscorp/signal
2
2
 
3
+ ## 0.1.5
4
+
5
+ ### Patch Changes
6
+
7
+ - c952ab0: A request to a model gets the options its maker or provider documents for whatever the caller leaves unset: `modelDefaults`, exported beside `modelRegistry`. gpt-oss on Groq gets temperature 1 and top_p 1, OpenAI's sampling for it (at temperature 0 its reasoning repeats one sentence until the budget runs out), and maxTokens 16,384, because Groq ends a completion at 3,072 tokens unless asked for more and reasoning alone can pass that. The caller's options always win.
8
+
9
+ ## 0.1.4
10
+
11
+ ### Patch Changes
12
+
13
+ - a8b7133: DOCS.md states thirty-nine facts about the API that were written only in source comments, and describes `resolveTransport`.
14
+
15
+ `step()` and `stepStream()` now have their request and result fields described: the shape of a tool-calling transcript, `output.accept` and `output.outputTool`, what `toolCalls[].args` holds, what `usage.reported: false` means, `outcome`, `wire.rung` and `wire.recovered`, and the `reasoning` and `tool_call_delta` events. A new subsection gives `resolveTransport(spec, capabilities)`: every field of the spec and of the result. Also added: what `UNMEASURED_MODEL` assumes, `chatProviderEntry`, that `describe()` makes no request, that an OpenRouter row lists what the proxy accepts, and that `jev-latest` is an alias.
16
+
17
+ No behaviour changed.
18
+
19
+ **What to change:** nothing.
20
+
3
21
  ## 0.1.3
4
22
 
5
23
  ### Patch Changes
package/DOCS.md CHANGED
@@ -37,7 +37,7 @@ Each provider has a capability profile that Signal uses to pick the right strate
37
37
  - **`nativeJsonMode`** — Can it guarantee JSON output?
38
38
  - **`toolsWithStructuredOutput`** — Is `response_format` accepted together with `tools` in one request?
39
39
  - **`validatesToolArgs`** — Does the endpoint validate tool-call arguments server-side and reject the request on a mismatch?
40
- - **`manglesNestedToolArgs`** — Does the model corrupt nested tool-call arguments?
40
+ - **`manglesNestedToolArgs`** — Does the model corrupt nested tool-call arguments? For example, a nested array arrives as a JSON string.
41
41
  - **`multimodal`** — Does it accept image content?
42
42
  - **`supportsEmbedding`** — Can it convert text to vectors?
43
43
 
@@ -146,20 +146,38 @@ Set LLM options: `temperature`, `maxTokens`, `topP`, `stopSequences`, `seed`, `s
146
146
  signal.options({ temperature: 0 })
147
147
  ```
148
148
 
149
+ ### Model defaults
150
+ Some models need options their caller does not know to set. `modelDefaults` (beside `modelRegistry` in `src/registry.ts`, exported) holds them, and a request gets them for whatever the caller leaves unset: the caller's options always win. gpt-oss on Groq gets `temperature: 1` and `topP: 1`, OpenAI's sampling for it (at temperature 0 its reasoning repeats one sentence until the budget runs out), and `maxTokens: 16384`, because Groq ends a completion at 3,072 tokens unless asked for more, and reasoning alone can pass that.
151
+ ```typescript
152
+ createSignal('groq').model('openai/gpt-oss-120b') // temperature 1, top_p 1, max_completion_tokens 16384
153
+ createSignal('groq').model('openai/gpt-oss-120b').options({ maxTokens: 4000 }) // temperature 1, top_p 1, max_completion_tokens 4000
154
+ ```
155
+
149
156
  ### Capabilities — looked up, not configured
150
157
  What a client can do is the provider's registry row (what the ENDPOINT
151
158
  does) joined with the model's registry row (what the MODEL does, as
152
159
  measured by `scripts/probe-model.ts`). There is no override: a fact about
153
160
  a model is a row in `src/registry.ts`. A model with no row resolves to the
154
161
  conservative `UNMEASURED_MODEL`, and `describe().modelKnown` says so.
162
+ `UNMEASURED_MODEL` assumes the least: `nativeJsonSchema: false`,
163
+ `toolsWithStructuredOutput: false`, `manglesNestedToolArgs: true`,
164
+ `multimodal: false`.
155
165
  A custom provider (by `baseUrl`) declares its own `capabilities`.
166
+ `chatProviderEntry(id)` returns a chat provider's registry row, and
167
+ `undefined` for an unknown id and for a decision provider.
156
168
  ```typescript
157
169
  createSignal('groq').describe()
158
170
  // { provider: 'groq', model: 'qwen/qwen3.8-27b', kind: 'chat', modelKnown: true, capabilities: {…} }
159
171
  ```
172
+ `describe()` makes no request and does not need an API key. For a custom
173
+ provider `modelKnown` is `false`, and `model` is `undefined` when none is set.
174
+
160
175
  A `reasoningEffort` the model's row does not list is refused when the
161
176
  client is built (`SignalError`, `E_VALIDATION_FAILED`), not by a provider 400
162
- mid-run. An unmeasured model is not checked.
177
+ mid-run. An unmeasured model is not checked. On OpenRouter a row lists what
178
+ the proxy accepts, which is not what the routed model honours: it passes
179
+ `reasoning_effort` through to non-reasoning models too, so an accepted value
180
+ does not mean the model reasons.
163
181
 
164
182
  ### `.onRetry(handler)`
165
183
  Hook called on each validation failure, including the last one that exhausts the retries.
@@ -221,6 +239,38 @@ Schema validation happens at the end of the stream; when the retries are spent t
221
239
 
222
240
  The low-level primitives `complete()` and `stream()` are built on: one model call, no tool execution, no schema retries. The caller owns the loop. `step()` takes a `StepRequest` (`messages`, `tools`, `toolChoice`, `output`, `responseFormat`, `options`) and returns a `StepResult` (`content`, `toolCalls`, `usage`, `finishReason`, `raw`, and — when the request carried `output.accept` — the routed `outcome` and `wire` report). `stepStream()` yields `text`, `reasoning` and `tool_call_delta` events and a final `done` carrying the `StepResult`.
223
241
 
242
+ - **`messages`** — an assistant turn carries the calls it made as `toolCalls` (`{ id, name, args }`, `args` a JSON string) and has `content: ''` when it only called tools. Each result goes back as a `tool` message whose `toolCallId` is that call's `id`.
243
+ - **`tools`** — each is `{ name, description, parameters }`; `parameters` is the JSON Schema of the tool's input (`z.toJSONSchema()` of a Zod schema).
244
+ - **`output.accept`** — gates and does not transform: `outcome.value` is the accepted candidate itself, not the schema's parsed output, so a transform or a default in the schema is not applied.
245
+ - **`output.outputTool`** — the name of an exit tool. A call to it is routed as output, never as a tool call.
246
+ - **`responseFormat`** — passed to the provider as given. `step()` does not check it against the capabilities: with `tools` on a model without `toolsWithStructuredOutput`, the provider rejects the request.
247
+ - **`toolCalls`** — a call's `args` is the parsed JSON value, or the provider's string when it does not parse. It is not checked against the tool's `parameters`.
248
+ - **`usage`** — `reported: false` means the provider sent no usage and the numbers are an estimate (about 4 characters per token). A stream does not reliably carry its usage.
249
+ - **`outcome`** — also present, without `output.accept`, on a result recovered from a provider rejection.
250
+ - **`wire`** — `rung` names what produced the accepted output: `parse` for content that was valid JSON as it arrived, `call-args` for a call's arguments, otherwise the repair it took. `recovered` is set when the result was recovered from a provider rejection.
251
+ - **`reasoning` events** — the model's thinking, never part of `content`. Only a provider that streams its reasoning produces them; on Groq and OpenRouter Signal asks for it only when `reasoningEffort` is set.
252
+ - **`tool_call_delta` events** — each carries one fragment of a call's arguments (`argsText`) for the call at `index`. The assembled calls are in the `done` event's `toolCalls`.
253
+
254
+ ### `resolveTransport(spec, capabilities)`
255
+
256
+ For a caller that runs its own loop over `step()`: resolves how an output contract travels — `respond` (the arguments of an exit tool), `native` (`response_format`) or `emit` (message content) — and returns what to put on each request. The `spec`:
257
+
258
+ - **`wire`** — the full schema of the output contract. It is the `response_format` under `native` and the exit tool's params under `respond`. An endpoint with `validatesToolArgs` is never sent it as tool params.
259
+ - **`looseWire`** — the same contract with the payload left untyped. It replaces `wire` as the exit tool's params when the contract has `data` and `wire` has no JSON Schema, has a `$ref` in it, or is longer than 8,192 characters as one.
260
+ - **`hasData`** — whether the contract carries a typed payload (a `data` field).
261
+ - **`forceTool`** — every turn must be a tool call: under `respond` the result carries `toolChoice: 'required'`. It makes `auto` resolve to `respond` where it would resolve to `emit`, and throws `E_VALIDATION_FAILED` with an explicit `emit`.
262
+
263
+ The result:
264
+
265
+ - **`respondDetail`** — under `respond`, how much of the contract the exit tool's params carry: `full` (`wire`), `loose` (`looseWire`) or `permissive` (the endpoint validates tool args, so the params name the envelope fields and constrain nothing).
266
+ - **`respondDescriptor`** — under `respond`, the exit tool to append to the request's `tools`.
267
+ - **`outputToolName`** — under `respond`, the exit tool's name (`RESPOND_TOOL_NAME`). Pass it as `output.outputTool`; the `tool_call_delta` events of a call with that name carry the output as it arrives.
268
+ - **`responseFormat`** — under `native`, the `responseFormat` to send on every request.
269
+ - **`toolChoice`** — `'required'` under `respond` with `forceTool`.
270
+ - **`injectSchemaDoc`** — `true` when the contract has `data` and the transport does not carry its full schema (`emit`, and `respond` with `loose` or `permissive` params): the caller adds its own schema documentation to the prompt.
271
+ - **`finishProtocol`** — the text that tells the model how the run ends under this transport. Add it unchanged as a system message, after any schema documentation.
272
+ - **`corrections`** — the messages to send back when a turn did not finish (`termination`) or its output was invalid (`invalidOutput`). The caller replaces `{issues}` in `invalidOutput` with the evidence.
273
+
224
274
  ### `.count(input)`
225
275
 
226
276
  Rough token count for a string or a message list. A heuristic (about 4 characters per token), not a tokenizer.
@@ -286,7 +336,7 @@ const { response } = await createSignal('groq')
286
336
 
287
337
  1. Zod schema is converted to JSON Schema (draft-07)
288
338
  2. Based on capabilities:
289
- - `nativeJsonSchema` (and, when tools are set, `toolsWithStructuredOutput`) → sends JSON Schema as `response_format`
339
+ - `nativeJsonSchema` (and, when tools are set, `toolsWithStructuredOutput`) → sends JSON Schema as `response_format`, with `strict: false`
290
340
  - otherwise `nativeJsonMode` and no tools → sends `response_format: json_object` + schema in a system message
291
341
  - otherwise → schema in a system message only
292
342
  3. The response is normalized by the wire layer (JSON extraction and repairs, each counted only if the result passes the schema)
@@ -466,7 +516,7 @@ result.decisions.urgency.level; // 0 | 1 | 2 — index into criteria
466
516
 
467
517
  You declare the questions once and never write an output type. Signal derives both halves from them:
468
518
 
469
- - **The result type.** `choice` → the union of the `criteria` keys; `score` → a level index into `criteria`; `noul` → a boolean.
519
+ - **The result type.** `choice` → the union of the `criteria` keys; `score` → a level index into `criteria`; `noul` → a boolean. A `score` question's `criteria` lists its levels lowest first.
470
520
  - **The acceptance gate.** The same derivation at runtime: a choice outside the asked options, a question left unanswered, a distribution over other keys, a probability outside [0, 1] — each fails the call with `E_VALIDATION_FAILED` and the provider's body under `context.raw`.
471
521
 
472
522
  There is no repair and no correction retry. A decision model writes no bytes to repair and cannot be told what it got wrong.
@@ -485,6 +535,8 @@ if (result.calibrated) {
485
535
  }
486
536
  ```
487
537
 
538
+ On a decision provider `level` is the most probable level and `answer` is `noul >= 0.5`.
539
+
488
540
  On a chat provider `decide()` still works: the same questions run through structured output (native grammar where it exists, the repair ladder, correction retries) and come back as picks alone with `calibrated: false`. Signal does not invent a `1.0` — it would clear every threshold. Emulation is one to two orders of magnitude slower; a caller on a latency budget checks first:
489
541
 
490
542
  ```typescript
@@ -503,6 +555,8 @@ controller.abort(); // tears down the HTTP request
503
555
 
504
556
  A provider is one of two kinds. A **decision provider** (`typesafe`) has `decide()` and nothing else — `complete`, `stream`, `step`, `stepStream` and `embed` fail with `E_VERB_NOT_SUPPORTED` before any request is built. A **chat provider** has every verb, `decide()` included, by emulation.
505
557
 
558
+ `typesafe`'s default model `jev-latest` is an alias for the provider's current version; `meta.model` is the version the provider reports having answered. A caller that records calibration against a version pins it with `.model('jev-1.13.0')`.
559
+
506
560
  A decision provider by base URL — self-hosted, or the fake a check runs — names its protocol:
507
561
 
508
562
  ```typescript
package/dist/index.cjs CHANGED
@@ -52,17 +52,11 @@ var providerRegistry = {
52
52
  endpoint: {
53
53
  nativeTools: true,
54
54
  nativeJsonMode: true,
55
- // Groq validates tool args server-side (tool_use_failed 400s).
56
55
  validatesToolArgs: true,
57
56
  supportsEmbedding: false
58
57
  },
59
58
  adapter: "openai-compatible",
60
- // tool_use_failed / json_validate_failed 400s carry the model's
61
- // attempt in failed_generation — recover and route it.
62
59
  wire: ["failed-generation"],
63
- // Reasoning models stream their trace only when asked to parse it out;
64
- // `reasoning_format` is rejected on the non-reasoning ones, which is why
65
- // this is opt-in (see reasoningRequest above).
66
60
  reasoningRequest: { reasoning_format: "parsed" }
67
61
  },
68
62
  openai: {
@@ -77,8 +71,6 @@ var providerRegistry = {
77
71
  supportsEmbedding: true
78
72
  },
79
73
  adapter: "openai-compatible",
80
- // 4o-era models occasionally emit JSONL where one value was asked
81
- // for; the acceptance gate decides, so this can never misfire.
82
74
  wire: ["jsonl-lines"]
83
75
  },
84
76
  openrouter: {
@@ -93,8 +85,6 @@ var providerRegistry = {
93
85
  supportsEmbedding: false
94
86
  },
95
87
  adapter: "openai-compatible",
96
- // OpenRouter unifies reasoning across the models it routes; `enabled` turns
97
- // the trace on for those that have one and is ignored by those that don't.
98
88
  reasoningRequest: { reasoning: { enabled: true } }
99
89
  },
100
90
  anthropic: {
@@ -127,97 +117,78 @@ var providerRegistry = {
127
117
  id: "typesafe",
128
118
  baseUrl: "https://api.typesafe.ai/v1",
129
119
  envKey: "TYPESAFE_API_KEY",
130
- // The moving alias. A caller that records calibration against a model pins
131
- // a version (`jev-1.13.0`) with `.model()`.
132
120
  defaultModel: "jev-latest",
133
121
  adapter: "systemone"
134
122
  }
135
123
  };
136
124
  var modelRegistry = {
137
- // ── groq ──
138
125
  "groq/qwen/qwen3.8-27b": {
139
126
  capabilities: { nativeJsonSchema: false, toolsWithStructuredOutput: false, manglesNestedToolArgs: true, multimodal: true },
140
- // nested: 3 of 20 trials failed
141
127
  reasoningEfforts: ["none", "default", "low", "medium", "high"],
142
128
  verified: "2026-09-24"
143
129
  },
144
130
  "groq/openai/gpt-oss-120b": {
145
131
  capabilities: { nativeJsonSchema: true, toolsWithStructuredOutput: false, manglesNestedToolArgs: true, multimodal: false },
146
- // nested: 1 of 20 trials failed
147
132
  reasoningEfforts: ["low", "medium", "high"],
148
133
  verified: "2026-09-24"
149
134
  },
150
135
  "groq/openai/gpt-oss-20b": {
151
136
  capabilities: { nativeJsonSchema: true, toolsWithStructuredOutput: false, manglesNestedToolArgs: true, multimodal: false },
152
- // nested: 6 of 20 trials failed
153
137
  reasoningEfforts: ["low", "medium", "high"],
154
138
  verified: "2026-09-24"
155
139
  },
156
- // ── openai ──
157
140
  "openai/gpt-4o": {
158
141
  capabilities: { nativeJsonSchema: true, toolsWithStructuredOutput: true, manglesNestedToolArgs: false, multimodal: true },
159
- // nested: 0 of 20 trials failed
160
142
  reasoningEfforts: [],
161
143
  verified: "2026-09-24"
162
144
  },
163
145
  "openai/gpt-4o-mini": {
164
146
  capabilities: { nativeJsonSchema: true, toolsWithStructuredOutput: true, manglesNestedToolArgs: false, multimodal: true },
165
- // nested: 0 of 20 trials failed
166
147
  reasoningEfforts: [],
167
148
  verified: "2026-09-24"
168
149
  },
169
150
  "openai/gpt-4.1-mini": {
170
151
  capabilities: { nativeJsonSchema: true, toolsWithStructuredOutput: true, manglesNestedToolArgs: false, multimodal: true },
171
- // nested: 0 of 20 trials failed
172
152
  reasoningEfforts: [],
173
153
  verified: "2026-09-24"
174
154
  },
175
- // ── openrouter ──
176
155
  "openrouter/qwen/qwen3.8-27b": {
177
156
  capabilities: { nativeJsonSchema: true, toolsWithStructuredOutput: true, manglesNestedToolArgs: false, multimodal: true },
178
- // nested: 0 of 20 trials failed
179
157
  reasoningEfforts: ["none", "minimal", "low", "medium", "high", "xhigh", "max"],
180
158
  verified: "2026-09-24"
181
159
  },
182
160
  "openrouter/openai/gpt-4o": {
183
161
  capabilities: { nativeJsonSchema: true, toolsWithStructuredOutput: true, manglesNestedToolArgs: false, multimodal: true },
184
- // nested: 0 of 20 trials failed
185
162
  reasoningEfforts: ["none", "minimal", "low", "medium", "high", "xhigh", "max"],
186
163
  verified: "2026-09-24"
187
164
  },
188
165
  "openrouter/openai/gpt-4o-mini": {
189
166
  capabilities: { nativeJsonSchema: true, toolsWithStructuredOutput: true, manglesNestedToolArgs: false, multimodal: true },
190
- // nested: 0 of 20 trials failed
191
167
  reasoningEfforts: ["none", "minimal", "low", "medium", "high", "xhigh", "max"],
192
168
  verified: "2026-09-24"
193
169
  },
194
170
  "openrouter/openai/gpt-oss-120b": {
195
171
  capabilities: { nativeJsonSchema: false, toolsWithStructuredOutput: true, manglesNestedToolArgs: false, multimodal: false },
196
- // nested: 0 of 20 trials failed
197
172
  reasoningEfforts: ["minimal", "low", "medium", "high", "xhigh", "max"],
198
173
  verified: "2026-09-24"
199
174
  },
200
175
  "openrouter/z-ai/glm-5.2": {
201
176
  capabilities: { nativeJsonSchema: true, toolsWithStructuredOutput: true, manglesNestedToolArgs: true, multimodal: false },
202
- // nested: 1 of 20 trials failed
203
177
  reasoningEfforts: ["none", "minimal", "low", "medium", "high", "xhigh", "max"],
204
178
  verified: "2026-09-24"
205
179
  },
206
180
  "openrouter/openai/gpt-5.6-luna": {
207
181
  capabilities: { nativeJsonSchema: true, toolsWithStructuredOutput: true, manglesNestedToolArgs: false, multimodal: true },
208
- // nested: 0 of 20 trials failed
209
182
  reasoningEfforts: ["none", "minimal", "low", "medium", "high", "xhigh", "max"],
210
183
  verified: "2026-09-24"
211
184
  },
212
185
  "openrouter/openai/gpt-oss-20b": {
213
186
  capabilities: { nativeJsonSchema: false, toolsWithStructuredOutput: true, manglesNestedToolArgs: false, multimodal: false },
214
- // nested: 0 of 20 trials failed
215
187
  reasoningEfforts: ["minimal", "low", "medium", "high", "xhigh", "max"],
216
188
  verified: "2026-09-24"
217
189
  },
218
190
  "openrouter/meta-llama/llama-3.3-70b-instruct": {
219
191
  capabilities: { nativeJsonSchema: false, toolsWithStructuredOutput: true, manglesNestedToolArgs: true, multimodal: false },
220
- // nested: 9 of 20 trials failed
221
192
  reasoningEfforts: ["none", "minimal", "low", "medium", "high", "xhigh", "max"],
222
193
  verified: "2026-09-24"
223
194
  }
@@ -230,6 +201,11 @@ var UNMEASURED_MODEL = {
230
201
  };
231
202
  var modelKey = (provider, model) => `${provider}/${model}`;
232
203
  var modelEntry = (provider, model) => modelRegistry[modelKey(provider, model)];
204
+ var GPT_OSS_ON_GROQ = { temperature: 1, topP: 1, maxTokens: 16384 };
205
+ var modelDefaults = {
206
+ "groq/openai/gpt-oss-120b": GPT_OSS_ON_GROQ,
207
+ "groq/openai/gpt-oss-20b": GPT_OSS_ON_GROQ
208
+ };
233
209
  var resolveApiKey = (envKey, explicitKey) => explicitKey ?? (typeof process !== "undefined" ? process.env?.[envKey] : void 0);
234
210
  var chatProviderEntry = (id) => {
235
211
  const entry = providerRegistry[id];
@@ -381,9 +357,8 @@ var createOpenAICompatibleAdapter = async (config) => {
381
357
  async function* chatStream(request, options) {
382
358
  const streamParams = {
383
359
  ...buildParams(request),
384
- // Ask for the reasoning trace only when the caller wants reasoning — the
385
- // opt-in that keeps `reasoning_format` off a plain Groq call. Stream-only:
386
- // the non-stream `chat` path never sees these, so its content is unmoved.
360
+ // Only when the caller set reasoningEffort: Groq answers `reasoning_format` with a 400
361
+ // on its non-reasoning models.
387
362
  ...request.options?.reasoningEffort !== void 0 && config.reasoningRequest !== void 0 ? config.reasoningRequest : {},
388
363
  stream: true,
389
364
  stream_options: { include_usage: true }
@@ -709,8 +684,6 @@ async function* executeStepStream(config) {
709
684
  contentBuffer += delta.text;
710
685
  yield { type: "text", text: delta.text };
711
686
  break;
712
- // Reasoning is passed straight through and deliberately NOT added to
713
- // contentBuffer: it is the model thinking, not its answer.
714
687
  case "reasoning":
715
688
  yield { type: "reasoning", text: delta.text };
716
689
  break;
@@ -1343,6 +1316,10 @@ var modelRowOf = (config) => {
1343
1316
  const model = config.model ?? entry.defaultModel;
1344
1317
  return { provider: config.provider, model, row: modelEntry(config.provider, model) };
1345
1318
  };
1319
+ var defaultsOf = (config) => {
1320
+ const resolved = modelRowOf(config);
1321
+ return resolved === void 0 ? void 0 : modelDefaults[modelKey(resolved.provider, resolved.model)];
1322
+ };
1346
1323
  var resolveCapabilities = (config) => {
1347
1324
  if (typeof config.provider === "string") {
1348
1325
  const entry = providerRegistry[config.provider];
@@ -1389,10 +1366,7 @@ var buildMessages = (config, input) => {
1389
1366
  var recoveredStepResult = (recovered, routed, raw, sent) => ({
1390
1367
  content: "",
1391
1368
  toolCalls: routed.outcome.kind === "tool_calls" ? routed.outcome.calls : [],
1392
- // A rejected generation costs tokens — the provider processed the request and
1393
- // billed for it — and the 400 body carries none. The prompt is known exactly
1394
- // and the model's attempt is in the rejection, so this is countable rather
1395
- // than lost. Marked unreported, because it is a reckoning.
1369
+ // The provider bills a rejected generation and its 400 carries no usage.
1396
1370
  usage: estimateUsage(sent, recovered.rejection.argsText, routed.outcome.kind === "tool_calls" ? routed.outcome.calls : []),
1397
1371
  finishReason: "error_recovered",
1398
1372
  raw,
@@ -1411,6 +1385,7 @@ var createSignalFromConfig = (config) => {
1411
1385
  let cachedAdapterKey;
1412
1386
  const wireStrategies = resolveWireStrategies(wireIdsOf(config));
1413
1387
  assertEffort(config, config.options?.reasoningEffort);
1388
+ const defaults = defaultsOf(config);
1414
1389
  const getAdapter = async () => {
1415
1390
  const resolved = resolveProvider(config);
1416
1391
  const key = `${resolved.adapterType}:${resolved.baseUrl}:${resolved.apiKey}`;
@@ -1447,12 +1422,8 @@ var createSignalFromConfig = (config) => {
1447
1422
  ...providerTools && { tools: providerTools },
1448
1423
  ...request.toolChoice !== void 0 && { toolChoice: request.toolChoice },
1449
1424
  ...request.responseFormat !== void 0 && { responseFormat: request.responseFormat },
1450
- // The client's own options are the floor; a request may override them
1451
- // field by field. `complete`/`stream` already run on config.options —
1452
- // step used to drop them, so a configured client behaved differently
1453
- // depending on which door the caller came through.
1454
- ...(config.options !== void 0 || request.options !== void 0) && {
1455
- options: { ...config.options, ...request.options }
1425
+ ...(defaults !== void 0 || config.options !== void 0 || request.options !== void 0) && {
1426
+ options: { ...defaults, ...config.options, ...request.options }
1456
1427
  }
1457
1428
  };
1458
1429
  assertEffort(config, providerRequest.options?.reasoningEffort);
@@ -1485,8 +1456,7 @@ var createSignalFromConfig = (config) => {
1485
1456
  const result = {
1486
1457
  content: response.content,
1487
1458
  toolCalls,
1488
- // The non-streaming body always carries usage, so a zero here is a real
1489
- // zero rather than a missing frame.
1459
+ // A non-streaming body always carries usage, so a zero here is a reported zero.
1490
1460
  usage: { ...response.usage, reported: true },
1491
1461
  finishReason: response.finishReason,
1492
1462
  raw: response.raw
@@ -1507,7 +1477,7 @@ var createSignalFromConfig = (config) => {
1507
1477
  const adapter = await getChatAdapter(verb);
1508
1478
  const resolved = resolveProvider(config);
1509
1479
  const declared = new Set((request.tools ?? []).map((tool) => tool.name));
1510
- const merged = config.options !== void 0 || request.options !== void 0 ? { ...request, options: { ...config.options, ...request.options } } : request;
1480
+ const merged = defaults !== void 0 || config.options !== void 0 || request.options !== void 0 ? { ...request, options: { ...defaults, ...config.options, ...request.options } } : request;
1511
1481
  assertEffort(config, merged.options?.reasoningEffort);
1512
1482
  try {
1513
1483
  for await (const event of executeStepStream({
@@ -1582,8 +1552,7 @@ var createSignalFromConfig = (config) => {
1582
1552
  decisions: acceptUncalibrated(request.questions, result.response, result.meta.provider.raw),
1583
1553
  meta: {
1584
1554
  model: result.meta.model,
1585
- // complete() sums what providers reported with what it had to estimate
1586
- // and does not say which, so this total cannot be vouched for as measured.
1555
+ // complete() adds reported usage to estimated usage and does not say which.
1587
1556
  usage: { ...result.meta.usage, reported: false },
1588
1557
  durationMs: Date.now() - started,
1589
1558
  provider: { raw: result.meta.provider.raw }
@@ -1591,7 +1560,6 @@ var createSignalFromConfig = (config) => {
1591
1560
  };
1592
1561
  };
1593
1562
  return {
1594
- // Builder methods
1595
1563
  apiKey: (key) => fork({ apiKey: key }),
1596
1564
  model: (m) => fork({ model: m }),
1597
1565
  systemPrompt: (prompt) => fork({ systemPrompt: prompt }),
@@ -1621,7 +1589,6 @@ var createSignalFromConfig = (config) => {
1621
1589
  modelKnown: false
1622
1590
  };
1623
1591
  },
1624
- // Execution — thin wrappers over the step core (src/complete.ts).
1625
1592
  complete: async (input) => runComplete(
1626
1593
  {
1627
1594
  messages: buildMessages(config, input),
@@ -1647,10 +1614,8 @@ var createSignalFromConfig = (config) => {
1647
1614
  },
1648
1615
  { stepStream: stepStreamAs("stream"), model: resolveProvider(config).model, capabilities: resolveCapabilities(config) }
1649
1616
  ),
1650
- // ─── Low-level primitives — the execution core itself ────
1651
1617
  step,
1652
1618
  stepStream,
1653
- // ─── Embedding ────────────────────────────────────────────
1654
1619
  embed: (async (input, options) => {
1655
1620
  const adapter = await getChatAdapter("embed");
1656
1621
  if (!adapter.embed) {
@@ -1670,7 +1635,6 @@ var createSignalFromConfig = (config) => {
1670
1635
  }
1671
1636
  return response.embeddings;
1672
1637
  }),
1673
- // ─── Decisions ────────────────────────────────────────────
1674
1638
  decide: async (request) => {
1675
1639
  assertQuestions(request.questions);
1676
1640
  const started = Date.now();
@@ -1678,7 +1642,6 @@ var createSignalFromConfig = (config) => {
1678
1642
  const model = resolveProvider(config).model;
1679
1643
  return adapter.kind === "decisions" ? decideCalibrated(adapter, model, request, started) : decideEmulated(model, request, started);
1680
1644
  },
1681
- // ─── Low-level: token counting (heuristic) ───────────────
1682
1645
  count: async (input) => {
1683
1646
  const CHARS_PER_TOKEN2 = 4;
1684
1647
  if (typeof input === "string") {
@@ -1786,8 +1749,7 @@ var resolveTransport = (spec, caps) => {
1786
1749
  responseMode: spec.responseMode,
1787
1750
  responseFormat: {
1788
1751
  type: "json_schema",
1789
- // strict:false — strict mode rejects open records and several
1790
- // keywords; non-strict still constrains and never hard-fails.
1752
+ // Strict mode rejects open records and several keywords; non-strict still constrains.
1791
1753
  jsonSchema: { name: "envelope", strict: false, schema: serialized }
1792
1754
  },
1793
1755
  injectSchemaDoc: injectSchemaDoc2,
@@ -1836,6 +1798,7 @@ exports.deepDecodeJsonish = deepDecodeJsonish;
1836
1798
  exports.defineTool = defineTool;
1837
1799
  exports.extractJson = extractJson;
1838
1800
  exports.isTruncatedJson = isTruncatedJson;
1801
+ exports.modelDefaults = modelDefaults;
1839
1802
  exports.modelEntry = modelEntry;
1840
1803
  exports.modelKey = modelKey;
1841
1804
  exports.modelRegistry = modelRegistry;