@wrongstack/providers 0.3.2 → 0.3.4

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
@@ -10,7 +10,7 @@ Most providers ride a single declarative `WireFormatConfig` adapter; only the th
10
10
  pnpm add @wrongstack/providers @wrongstack/core
11
11
  ```
12
12
 
13
- `@wrongstack/core` is a peer of every provider — providers depend on the core `Provider` interface, message types, and tool format.
13
+ `@wrongstack/core` provides the shared `Provider` interface, message types, and tool format.
14
14
 
15
15
  ## What's in here
16
16
 
@@ -37,13 +37,16 @@ import { AnthropicProvider } from '@wrongstack/providers';
37
37
 
38
38
  const provider = new AnthropicProvider({
39
39
  apiKey: process.env.ANTHROPIC_API_KEY!,
40
- modelId: 'claude-sonnet-4-6',
41
40
  });
42
41
 
43
- const stream = provider.stream({
44
- messages: [{ role: 'user', content: 'hello' }],
45
- tools: [],
46
- });
42
+ const stream = provider.stream(
43
+ {
44
+ model: 'claude-sonnet-4-6',
45
+ messages: [{ role: 'user', content: 'hello' }],
46
+ maxTokens: 512,
47
+ },
48
+ { signal: new AbortController().signal },
49
+ );
47
50
 
48
51
  for await (const event of stream) {
49
52
  if (event.type === 'text_delta') process.stdout.write(event.text);
@@ -58,39 +61,48 @@ import { OpenAICompatibleProvider } from '@wrongstack/providers';
58
61
  const groq = new OpenAICompatibleProvider({
59
62
  id: 'groq',
60
63
  apiKey: process.env.GROQ_API_KEY!,
61
- baseURL: 'https://api.groq.com/openai/v1',
62
- modelId: 'llama-3.3-70b-versatile',
64
+ baseUrl: 'https://api.groq.com/openai/v1',
63
65
  capabilities: { tools: true, vision: false, maxContext: 128_000 },
64
66
  });
67
+
68
+ const result = await groq.complete(
69
+ {
70
+ model: 'llama-3.3-70b-versatile',
71
+ messages: [{ role: 'user', content: 'hello' }],
72
+ maxTokens: 512,
73
+ },
74
+ { signal: new AbortController().signal },
75
+ );
65
76
  ```
66
77
 
67
78
  ## Wire-format adapter (declarative)
68
79
 
69
- For a new provider that doesn't fit one of the existing presets, write a `WireFormatConfig` and plug it into `WireAdapter`. See [docs/provider-author-guide.md](../../docs/provider-author-guide.md) for the full spec.
80
+ For a new provider that doesn't fit one of the existing presets, write a `WireFormatConfig` and plug it into `WireFormatProvider`. See [docs/provider-author-guide.md](../../docs/provider-author-guide.md) for the full spec.
70
81
 
71
82
  ```ts
72
- import { WireAdapter } from '@wrongstack/providers';
73
- import type { WireFormatConfig } from '@wrongstack/core';
83
+ import { WireFormatProvider, type WireFormatConfig } from '@wrongstack/providers';
74
84
 
75
85
  const myWire: WireFormatConfig = {
76
- family: 'openai',
77
- endpoint: 'https://api.myprovider.com/v1/chat/completions',
78
- authHeader: (key) => ({ 'Authorization': `Bearer ${key}` }),
79
- // tool format, message shape, stream parsing — see WireFormatConfig type
86
+ id: 'myprovider',
87
+ family: 'openai-compatible',
88
+ capabilities: { tools: true, parallelTools: true, vision: false, streaming: true, promptCache: false, systemPrompt: true, jsonMode: false, maxContext: 32_000, cacheControl: 'none' },
89
+ defaultBaseUrl: 'https://api.myprovider.com/v1',
90
+ buildUrl: (baseUrl) => `${baseUrl.replace(/\/+$/, '')}/chat/completions`,
91
+ buildHeaders: (apiKey) => ({ authorization: `Bearer ${apiKey}` }),
92
+ buildBody: (req) => ({ model: req.model, messages: req.messages, max_tokens: req.maxTokens, stream: true }),
93
+ createStreamState: (fallbackModel) => ({ model: fallbackModel, started: false }),
94
+ parseStreamEvent: () => [],
95
+ finalizeStream: () => [{ type: 'message_stop', stopReason: 'end_turn', usage: { input: 0, output: 0 } }],
80
96
  };
81
97
 
82
- const provider = new WireAdapter({
83
- id: 'myprovider',
98
+ const provider = new WireFormatProvider(myWire, {
84
99
  apiKey: '…',
85
- modelId: 'my-model-1',
86
- wire: myWire,
87
- capabilities: { tools: true, maxContext: 32_000 },
88
100
  });
89
101
  ```
90
102
 
91
103
  ## Tool input parsing (`parseToolInput`)
92
104
 
93
- All four stream parsers (anthropic / openai / aggregate + the three OpenAI-compatible presets) run tool-call JSON through one canonical helper: [`_tool-input.ts`](src/_tool-input.ts). It guarantees the agent always receives a `Record<string, unknown>` for `tool_use.input`, never a parse-error or `null`. Invalid or non-object inputs are wrapped under `{ __raw: ... }` instead of crashing the provider runner.
105
+ Anthropic/OpenAI-style stream parsers and the aggregate path run tool-call JSON through one canonical helper: [`_tool-input.ts`](src/_tool-input.ts). It guarantees the agent always receives a `Record<string, unknown>` for `tool_use.input`, never a parse-error or `null`. Invalid or non-object inputs are wrapped under `{ __raw: ... }` instead of crashing the provider runner.
94
106
 
95
107
  ## Capabilities
96
108
 
package/dist/index.d.ts CHANGED
@@ -320,16 +320,18 @@ interface MistralStreamState {
320
320
  name?: string;
321
321
  partial: string;
322
322
  emittedStart: boolean;
323
+ emittedArgLength: number;
323
324
  }>;
324
325
  }
325
326
  declare const mistralWireFormat: WireFormatConfig<MistralStreamState>;
326
327
 
327
- type BlockKind = 'text' | 'tool_use' | 'unknown';
328
+ type BlockKind = 'text' | 'tool_use' | 'thinking' | 'unknown';
328
329
  interface AnthropicStreamState {
329
330
  model: string;
330
331
  usage: Usage;
331
332
  stopReason: StopReason;
332
333
  started: boolean;
334
+ stopped: boolean;
333
335
  blocks: Map<number, {
334
336
  kind: BlockKind;
335
337
  id?: string;
@@ -347,9 +349,11 @@ interface OpenAIStreamState {
347
349
  textOpen: boolean;
348
350
  thinkingOpen: boolean;
349
351
  toolByIndex: Map<number, {
350
- id: string;
351
- name: string;
352
+ id?: string;
353
+ name?: string;
352
354
  argBuf: string;
355
+ emittedStart: boolean;
356
+ emittedArgLength: number;
353
357
  }>;
354
358
  finalEmitted: boolean;
355
359
  }
package/dist/index.js CHANGED
@@ -554,6 +554,7 @@ async function* parseAnthropicStream(body, fallbackModel) {
554
554
  let usage = { input: 0, output: 0 };
555
555
  let stopReason = "end_turn";
556
556
  let started = false;
557
+ let stopped = false;
557
558
  for await (const msg of parseSSE(body)) {
558
559
  if (!msg.data || msg.data === "[DONE]") continue;
559
560
  const parsed = safeParse(msg.data);
@@ -634,6 +635,7 @@ async function* parseAnthropicStream(body, fallbackModel) {
634
635
  break;
635
636
  }
636
637
  case "message_stop":
638
+ stopped = true;
637
639
  yield { type: "message_stop", stopReason, usage };
638
640
  break;
639
641
  case "error": {
@@ -644,7 +646,7 @@ async function* parseAnthropicStream(body, fallbackModel) {
644
646
  }
645
647
  }
646
648
  }
647
- if (started) {
649
+ if (started && !stopped) {
648
650
  yield { type: "message_stop", stopReason, usage };
649
651
  }
650
652
  }
@@ -1086,17 +1088,33 @@ async function* parseOpenAIStream(body, fallbackModel) {
1086
1088
  for (const tc of choice.delta.tool_calls) {
1087
1089
  const idx = tc.index ?? 0;
1088
1090
  let entry = toolByIndex.get(idx);
1089
- if (!entry && tc.id && tc.function?.name) {
1090
- entry = { id: tc.id, name: tc.function.name, argBuf: "" };
1091
+ if (!entry) {
1092
+ entry = {
1093
+ id: tc.id,
1094
+ name: tc.function?.name,
1095
+ argBuf: "",
1096
+ emittedStart: false,
1097
+ emittedArgLength: 0
1098
+ };
1091
1099
  toolByIndex.set(idx, entry);
1092
- yield { type: "tool_use_start", id: entry.id, name: entry.name };
1100
+ } else {
1101
+ if (tc.id && !entry.id) entry.id = tc.id;
1102
+ if (tc.function?.name && !entry.name) entry.name = tc.function.name;
1093
1103
  }
1094
- if (entry && tc.function?.arguments) {
1104
+ if (tc.function?.arguments) {
1095
1105
  entry.argBuf += tc.function.arguments;
1106
+ }
1107
+ if (!entry.emittedStart && entry.id && entry.name) {
1108
+ entry.emittedStart = true;
1109
+ yield { type: "tool_use_start", id: entry.id, name: entry.name };
1110
+ }
1111
+ if (entry.emittedStart && entry.id && entry.emittedArgLength < entry.argBuf.length) {
1112
+ const partial = entry.argBuf.slice(entry.emittedArgLength);
1113
+ entry.emittedArgLength = entry.argBuf.length;
1096
1114
  yield {
1097
1115
  type: "tool_use_input_delta",
1098
1116
  id: entry.id,
1099
- partial: tc.function.arguments
1117
+ partial
1100
1118
  };
1101
1119
  }
1102
1120
  }
@@ -1106,10 +1124,11 @@ async function* parseOpenAIStream(body, fallbackModel) {
1106
1124
  }
1107
1125
  const u = obj["usage"];
1108
1126
  if (u) {
1109
- const cached = u.prompt_tokens_details?.cached_tokens ?? 0;
1110
- const promptTotal = u.prompt_tokens ?? usage.input + cached;
1127
+ const hasDeepSeekCacheFields = u.prompt_cache_hit_tokens !== void 0 || u.prompt_cache_miss_tokens !== void 0;
1128
+ const cached = u.prompt_tokens_details?.cached_tokens ?? u.prompt_cache_hit_tokens ?? 0;
1129
+ const promptTotal = u.prompt_tokens ?? (hasDeepSeekCacheFields ? (u.prompt_cache_hit_tokens ?? 0) + (u.prompt_cache_miss_tokens ?? 0) : usage.input + cached);
1111
1130
  usage = {
1112
- input: Math.max(0, promptTotal - cached),
1131
+ input: u.prompt_cache_miss_tokens ?? Math.max(0, promptTotal - cached),
1113
1132
  output: u.completion_tokens ?? usage.output,
1114
1133
  cacheRead: cached || usage.cacheRead
1115
1134
  };
@@ -1119,6 +1138,10 @@ async function* parseOpenAIStream(body, fallbackModel) {
1119
1138
  yield { type: "thinking_stop" };
1120
1139
  }
1121
1140
  for (const entry of toolByIndex.values()) {
1141
+ if (!entry.id || !entry.name) continue;
1142
+ if (!entry.emittedStart) {
1143
+ yield { type: "tool_use_start", id: entry.id, name: entry.name };
1144
+ }
1122
1145
  const input = parseToolInput(entry.argBuf);
1123
1146
  yield { type: "tool_use_stop", id: entry.id, input };
1124
1147
  }
@@ -1237,16 +1260,31 @@ var mistralWireFormat = defineWireFormat({
1237
1260
  defaultBaseUrl: "https://api.mistral.ai/v1",
1238
1261
  buildUrl: (base) => `${base.replace(/\/+$/, "")}/chat/completions`,
1239
1262
  buildHeaders: (apiKey) => ({ authorization: `Bearer ${apiKey}` }),
1240
- buildBody: (req) => ({
1241
- model: req.model,
1242
- messages: req.messages,
1243
- max_tokens: req.maxTokens,
1244
- temperature: req.temperature,
1245
- top_p: req.topP,
1246
- stop: req.stopSequences,
1247
- stream: true,
1248
- tools: req.tools
1249
- }),
1263
+ buildBody: (req) => {
1264
+ const body = {
1265
+ model: req.model,
1266
+ messages: messagesToOpenAI(stripCacheControl(req.system), req.messages, {}),
1267
+ max_tokens: req.maxTokens,
1268
+ stream: true
1269
+ };
1270
+ if (req.tools && req.tools.length > 0) {
1271
+ body["tools"] = toolsToOpenAI(req.tools);
1272
+ if (req.toolChoice) {
1273
+ if (typeof req.toolChoice === "string") {
1274
+ body["tool_choice"] = req.toolChoice === "required" ? "required" : req.toolChoice;
1275
+ } else {
1276
+ body["tool_choice"] = {
1277
+ type: "function",
1278
+ function: { name: req.toolChoice.name }
1279
+ };
1280
+ }
1281
+ }
1282
+ }
1283
+ if (req.temperature !== void 0) body["temperature"] = req.temperature;
1284
+ if (req.topP !== void 0) body["top_p"] = req.topP;
1285
+ if (req.stopSequences) body["stop"] = req.stopSequences;
1286
+ return body;
1287
+ },
1250
1288
  createStreamState: (fallbackModel) => ({
1251
1289
  model: fallbackModel,
1252
1290
  started: false,
@@ -1270,25 +1308,38 @@ var mistralWireFormat = defineWireFormat({
1270
1308
  for (const tc of choice?.delta?.tool_calls ?? []) {
1271
1309
  let block = state.toolCalls.get(tc.index);
1272
1310
  if (!block) {
1273
- block = { id: tc.id, name: tc.function?.name, partial: "", emittedStart: false };
1311
+ block = {
1312
+ id: tc.id,
1313
+ name: tc.function?.name,
1314
+ partial: "",
1315
+ emittedStart: false,
1316
+ emittedArgLength: 0
1317
+ };
1274
1318
  state.toolCalls.set(tc.index, block);
1275
1319
  } else {
1276
1320
  if (tc.id && !block.id) block.id = tc.id;
1277
1321
  if (tc.function?.name && !block.name) block.name = tc.function.name;
1278
1322
  }
1323
+ const arg = tc.function?.arguments;
1324
+ if (arg) {
1325
+ block.partial += arg;
1326
+ }
1279
1327
  if (!block.emittedStart && block.id && block.name) {
1280
1328
  block.emittedStart = true;
1281
1329
  out.push({ type: "tool_use_start", id: block.id, name: block.name });
1282
1330
  }
1283
- const arg = tc.function?.arguments;
1284
- if (arg && block.id) {
1285
- block.partial += arg;
1286
- out.push({ type: "tool_use_input_delta", id: block.id, partial: arg });
1331
+ if (block.emittedStart && block.id && block.emittedArgLength < block.partial.length) {
1332
+ const partial = block.partial.slice(block.emittedArgLength);
1333
+ block.emittedArgLength = block.partial.length;
1334
+ out.push({ type: "tool_use_input_delta", id: block.id, partial });
1287
1335
  }
1288
1336
  }
1289
1337
  if (choice?.finish_reason) {
1290
1338
  for (const block of state.toolCalls.values()) {
1291
- if (block.id) {
1339
+ if (block.id && block.name) {
1340
+ if (!block.emittedStart) {
1341
+ out.push({ type: "tool_use_start", id: block.id, name: block.name });
1342
+ }
1292
1343
  out.push({
1293
1344
  type: "tool_use_stop",
1294
1345
  id: block.id,
@@ -1313,13 +1364,21 @@ function mapStopReason(reason) {
1313
1364
  case "tool_calls":
1314
1365
  return "tool_use";
1315
1366
  case "length":
1367
+ case "model_length":
1316
1368
  return "max_tokens";
1317
1369
  case "stop":
1318
- return "stop_sequence";
1370
+ return "end_turn";
1319
1371
  default:
1320
1372
  return "end_turn";
1321
1373
  }
1322
1374
  }
1375
+ function stripCacheControl(system) {
1376
+ if (!system) return void 0;
1377
+ return system.map((b) => {
1378
+ const { cache_control: _cc, ...rest } = b;
1379
+ return rest;
1380
+ });
1381
+ }
1323
1382
 
1324
1383
  // src/presets/anthropic.ts
1325
1384
  init_tool_input();
@@ -1362,6 +1421,7 @@ var anthropicWireFormat = defineWireFormat({
1362
1421
  usage: { input: 0, output: 0 },
1363
1422
  stopReason: "end_turn",
1364
1423
  started: false,
1424
+ stopped: false,
1365
1425
  blocks: /* @__PURE__ */ new Map()
1366
1426
  }),
1367
1427
  parseStreamEvent: (msg, state) => {
@@ -1397,6 +1457,9 @@ var anthropicWireFormat = defineWireFormat({
1397
1457
  }
1398
1458
  } else if (cb?.type === "text") {
1399
1459
  state.blocks.set(index, { kind: "text", partial: "" });
1460
+ } else if (cb?.type === "thinking" || cb?.type === "redacted_thinking") {
1461
+ state.blocks.set(index, { kind: "thinking", partial: "" });
1462
+ out.push({ type: "thinking_start" });
1400
1463
  } else {
1401
1464
  state.blocks.set(index, { kind: "unknown", partial: "" });
1402
1465
  }
@@ -1414,6 +1477,10 @@ var anthropicWireFormat = defineWireFormat({
1414
1477
  block.partial += delta.partial_json;
1415
1478
  out.push({ type: "tool_use_input_delta", id: block.id, partial: delta.partial_json });
1416
1479
  }
1480
+ } else if (delta.type === "thinking_delta" && typeof delta.thinking === "string") {
1481
+ out.push({ type: "thinking_delta", text: delta.thinking });
1482
+ } else if (delta.type === "signature_delta" && typeof delta.signature === "string") {
1483
+ out.push({ type: "thinking_signature", signature: delta.signature });
1417
1484
  }
1418
1485
  break;
1419
1486
  }
@@ -1423,6 +1490,8 @@ var anthropicWireFormat = defineWireFormat({
1423
1490
  if (block?.kind === "tool_use" && block.id) {
1424
1491
  const input = parseToolInput(block.partial);
1425
1492
  out.push({ type: "tool_use_stop", id: block.id, input });
1493
+ } else if (block?.kind === "thinking") {
1494
+ out.push({ type: "thinking_stop" });
1426
1495
  }
1427
1496
  break;
1428
1497
  }
@@ -1438,6 +1507,7 @@ var anthropicWireFormat = defineWireFormat({
1438
1507
  break;
1439
1508
  }
1440
1509
  case "message_stop":
1510
+ state.stopped = true;
1441
1511
  out.push({ type: "message_stop", stopReason: state.stopReason, usage: state.usage });
1442
1512
  break;
1443
1513
  case "error": {
@@ -1450,7 +1520,7 @@ var anthropicWireFormat = defineWireFormat({
1450
1520
  return out;
1451
1521
  },
1452
1522
  finalizeStream: (state) => {
1453
- if (state.started) {
1523
+ if (state.started && !state.stopped) {
1454
1524
  return [{ type: "message_stop", stopReason: state.stopReason, usage: state.usage }];
1455
1525
  }
1456
1526
  return [];
@@ -1474,7 +1544,7 @@ var openaiWireFormat = defineWireFormat({
1474
1544
  buildBody: (req) => {
1475
1545
  const body = {
1476
1546
  model: req.model,
1477
- messages: messagesToOpenAI(stripCacheControl(req.system), req.messages, {}),
1547
+ messages: messagesToOpenAI(stripCacheControl2(req.system), req.messages, {}),
1478
1548
  max_tokens: req.maxTokens,
1479
1549
  stream: true,
1480
1550
  stream_options: { include_usage: true }
@@ -1544,18 +1614,34 @@ var openaiWireFormat = defineWireFormat({
1544
1614
  for (const tc of choice.delta.tool_calls) {
1545
1615
  const idx = tc.index ?? 0;
1546
1616
  let entry = state.toolByIndex.get(idx);
1547
- if (!entry && tc.id && tc.function?.name) {
1548
- entry = { id: tc.id, name: tc.function.name, argBuf: "" };
1617
+ if (!entry) {
1618
+ entry = {
1619
+ id: tc.id,
1620
+ name: tc.function?.name,
1621
+ argBuf: "",
1622
+ emittedStart: false,
1623
+ emittedArgLength: 0
1624
+ };
1549
1625
  state.toolByIndex.set(idx, entry);
1626
+ } else {
1627
+ if (tc.id && !entry.id) entry.id = tc.id;
1628
+ if (tc.function?.name && !entry.name) entry.name = tc.function.name;
1629
+ }
1630
+ if (tc.function?.arguments) {
1631
+ entry.argBuf += tc.function.arguments;
1632
+ }
1633
+ if (!entry.emittedStart && entry.id && entry.name) {
1634
+ entry.emittedStart = true;
1550
1635
  state.textOpen = false;
1551
1636
  out.push({ type: "tool_use_start", id: entry.id, name: entry.name });
1552
1637
  }
1553
- if (entry && tc.function?.arguments) {
1554
- entry.argBuf += tc.function.arguments;
1638
+ if (entry.emittedStart && entry.id && entry.emittedArgLength < entry.argBuf.length) {
1639
+ const partial = entry.argBuf.slice(entry.emittedArgLength);
1640
+ entry.emittedArgLength = entry.argBuf.length;
1555
1641
  out.push({
1556
1642
  type: "tool_use_input_delta",
1557
1643
  id: entry.id,
1558
- partial: tc.function.arguments
1644
+ partial
1559
1645
  });
1560
1646
  }
1561
1647
  }
@@ -1565,10 +1651,11 @@ var openaiWireFormat = defineWireFormat({
1565
1651
  }
1566
1652
  const u = obj["usage"];
1567
1653
  if (u) {
1568
- const cached = u.prompt_tokens_details?.cached_tokens ?? 0;
1569
- const promptTotal = u.prompt_tokens ?? state.usage.input + cached;
1654
+ const hasDeepSeekCacheFields = u.prompt_cache_hit_tokens !== void 0 || u.prompt_cache_miss_tokens !== void 0;
1655
+ const cached = u.prompt_tokens_details?.cached_tokens ?? u.prompt_cache_hit_tokens ?? 0;
1656
+ const promptTotal = u.prompt_tokens ?? (hasDeepSeekCacheFields ? (u.prompt_cache_hit_tokens ?? 0) + (u.prompt_cache_miss_tokens ?? 0) : state.usage.input + cached);
1570
1657
  state.usage = {
1571
- input: Math.max(0, promptTotal - cached),
1658
+ input: u.prompt_cache_miss_tokens ?? Math.max(0, promptTotal - cached),
1572
1659
  output: u.completion_tokens ?? state.usage.output,
1573
1660
  cacheRead: cached || state.usage.cacheRead
1574
1661
  };
@@ -1584,6 +1671,10 @@ var openaiWireFormat = defineWireFormat({
1584
1671
  out.push({ type: "thinking_stop" });
1585
1672
  }
1586
1673
  for (const entry of state.toolByIndex.values()) {
1674
+ if (!entry.id || !entry.name) continue;
1675
+ if (!entry.emittedStart) {
1676
+ out.push({ type: "tool_use_start", id: entry.id, name: entry.name });
1677
+ }
1587
1678
  const input = parseToolInput(entry.argBuf);
1588
1679
  out.push({ type: "tool_use_stop", id: entry.id, input });
1589
1680
  }
@@ -1593,7 +1684,7 @@ var openaiWireFormat = defineWireFormat({
1593
1684
  return out;
1594
1685
  }
1595
1686
  });
1596
- function stripCacheControl(system) {
1687
+ function stripCacheControl2(system) {
1597
1688
  if (!system) return void 0;
1598
1689
  return system.map((b) => {
1599
1690
  const { cache_control: _cc, ...rest } = b;