@opencode-ai/ai 0.0.0-dev-18326 → 0.0.0-dev-18332

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.
@@ -603,6 +603,7 @@ export declare const protocol: Protocol<{
603
603
  } | undefined;
604
604
  readonly content_block?: unknown;
605
605
  }, {
606
+ providerMetadataKey: string;
606
607
  tools: Partial<Record<number, ToolStream.PendingTool>>;
607
608
  reasoningSignatures: {};
608
609
  lifecycle: Lifecycle.State;
@@ -313,18 +313,18 @@ const cacheControl = (breakpoints, cache) => {
313
313
  breakpoints.remaining -= 1;
314
314
  return Cache.ttlBucket(cache.ttlSeconds) === "1h" ? EPHEMERAL_1H : EPHEMERAL_5M;
315
315
  };
316
- const anthropicMetadata = (metadata) => ({ anthropic: metadata });
317
- const signatureFromMetadata = (metadata) => {
318
- const anthropic = metadata?.anthropic;
319
- if (!ProviderShared.isRecord(anthropic))
316
+ const providerMetadata = (key, metadata) => ({ [key]: metadata });
317
+ const signatureFromMetadata = (metadata, key) => {
318
+ const provider = metadata?.[key];
319
+ if (!ProviderShared.isRecord(provider))
320
320
  return undefined;
321
- return typeof anthropic.signature === "string" ? anthropic.signature : undefined;
321
+ return typeof provider.signature === "string" ? provider.signature : undefined;
322
322
  };
323
- const redactedDataFromMetadata = (metadata) => {
324
- const anthropic = metadata?.anthropic;
325
- if (!ProviderShared.isRecord(anthropic))
323
+ const redactedDataFromMetadata = (metadata, key) => {
324
+ const provider = metadata?.[key];
325
+ if (!ProviderShared.isRecord(provider))
326
326
  return undefined;
327
- return typeof anthropic.redactedData === "string" ? anthropic.redactedData : undefined;
327
+ return typeof provider.redactedData === "string" ? provider.redactedData : undefined;
328
328
  };
329
329
  const lowerTool = (breakpoints, tool, inputSchema) => ({
330
330
  name: tool.name,
@@ -379,13 +379,13 @@ const serverToolResultType = (name) => {
379
379
  return "web_fetch_tool_result";
380
380
  return undefined;
381
381
  };
382
- const lowerServerToolResult = Effect.fn("AnthropicMessages.lowerServerToolResult")(function* (part) {
382
+ const lowerServerToolResult = Effect.fn("AnthropicMessages.lowerServerToolResult")(function* (part, providerMetadataKey) {
383
383
  const wireType = serverToolResultType(part.name);
384
384
  if (!wireType)
385
385
  return yield* invalid(`Anthropic Messages does not know how to round-trip server tool result for ${part.name}`);
386
386
  // Prefer the provider-owned replay payload; fall back to the result value for
387
387
  // histories constructed directly from provider events.
388
- const payload = part.providerMetadata?.anthropic?.["result"] ?? part.result.value;
388
+ const payload = part.providerMetadata?.[providerMetadataKey]?.["result"] ?? part.result.value;
389
389
  return {
390
390
  type: wireType,
391
391
  tool_use_id: scrubToolCallID(part.id),
@@ -658,6 +658,7 @@ const lowerNativeSystemUpdate = Effect.fn("AnthropicMessages.lowerNativeSystemUp
658
658
  });
659
659
  const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (request, breakpoints) {
660
660
  const messages = [];
661
+ const providerMetadataKey = request.model.route.providerMetadataKey ?? String(request.model.provider);
661
662
  for (const [index, message] of request.messages.entries()) {
662
663
  if (message.role === "system") {
663
664
  if (splitsLocalToolResults(request.messages, index))
@@ -701,8 +702,8 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (re
701
702
  if (part.type === "reasoning") {
702
703
  // A signature marks visible thinking; only signature-less parts carrying
703
704
  // redactedData round-trip as opaque redacted_thinking blocks.
704
- const signature = part.encrypted ?? signatureFromMetadata(part.providerMetadata);
705
- const redactedData = redactedDataFromMetadata(part.providerMetadata);
705
+ const signature = part.encrypted ?? signatureFromMetadata(part.providerMetadata, providerMetadataKey);
706
+ const redactedData = redactedDataFromMetadata(part.providerMetadata, providerMetadataKey);
706
707
  if (signature === undefined && redactedData !== undefined) {
707
708
  content.push({ type: "redacted_thinking", data: redactedData });
708
709
  continue;
@@ -732,7 +733,7 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (re
732
733
  continue;
733
734
  }
734
735
  if (part.type === "tool-result" && part.providerExecuted) {
735
- content.push(yield* lowerServerToolResult(part));
736
+ content.push(yield* lowerServerToolResult(part, providerMetadataKey));
736
737
  continue;
737
738
  }
738
739
  return yield* invalid(`Anthropic Messages assistant messages only support text, reasoning, and tool-call content for now`);
@@ -897,7 +898,7 @@ const mapFinishReason = (reason) => {
897
898
  // inclusive `inputTokens` the rest of the contract expects. Extended
898
899
  // thinking tokens are included in `output_tokens`; newer responses also
899
900
  // expose that subset through `output_tokens_details.thinking_tokens`.
900
- const mapUsage = (usage) => {
901
+ const mapUsage = (usage, providerMetadataKey) => {
901
902
  if (!usage)
902
903
  return undefined;
903
904
  const nonCached = usage.input_tokens ?? undefined;
@@ -912,7 +913,7 @@ const mapUsage = (usage) => {
912
913
  cacheWriteInputTokens: cacheWrite,
913
914
  reasoningTokens: usage.output_tokens_details?.thinking_tokens,
914
915
  totalTokens: ProviderShared.totalTokens(inputTokens, usage.output_tokens, undefined),
915
- providerMetadata: { anthropic: usage },
916
+ providerMetadata: { [providerMetadataKey]: usage },
916
917
  });
917
918
  };
918
919
  // Anthropic emits usage on `message_start` and again on `message_delta` — the
@@ -920,7 +921,7 @@ const mapUsage = (usage) => {
920
921
  // field prefers `right` when defined, falls back to `left`. `inputTokens` is
921
922
  // recomputed from the merged breakdown so the inclusive total stays
922
923
  // consistent with `nonCached + cacheRead + cacheWrite`.
923
- const mergeUsage = (left, right) => {
924
+ const mergeUsage = (left, right, providerMetadataKey) => {
924
925
  if (!left)
925
926
  return right;
926
927
  if (!right)
@@ -940,7 +941,8 @@ const mergeUsage = (left, right) => {
940
941
  reasoningTokens,
941
942
  totalTokens: ProviderShared.totalTokens(inputTokens, outputTokens, undefined),
942
943
  providerMetadata: {
943
- anthropic: mergeJsonRecords(left.providerMetadata?.["anthropic"], right.providerMetadata?.["anthropic"]) ?? {},
944
+ [providerMetadataKey]: mergeJsonRecords(left.providerMetadata?.[providerMetadataKey], right.providerMetadata?.[providerMetadataKey]) ??
945
+ {},
944
946
  },
945
947
  });
946
948
  };
@@ -955,7 +957,7 @@ const SERVER_TOOL_RESULT_NAMES = {
955
957
  web_fetch_tool_result: "web_fetch",
956
958
  };
957
959
  const isServerToolResultType = (type) => type in SERVER_TOOL_RESULT_NAMES;
958
- const serverToolResultEvent = (block) => {
960
+ const serverToolResultEvent = (block, providerMetadataKey) => {
959
961
  if (!block.type || !isServerToolResultType(block.type))
960
962
  return undefined;
961
963
  const errorPayload = typeof block.content === "object" && block.content !== null && "type" in block.content
@@ -969,13 +971,13 @@ const serverToolResultEvent = (block) => {
969
971
  providerExecuted: true,
970
972
  // The complete payload is irreducible provider replay state: subsequent
971
973
  // stateless requests must round-trip the typed result block verbatim.
972
- providerMetadata: anthropicMetadata({ blockType: block.type, result: block.content }),
974
+ providerMetadata: providerMetadata(providerMetadataKey, { blockType: block.type, result: block.content }),
973
975
  });
974
976
  };
975
977
  const NO_EVENTS = [];
976
978
  const onMessageStart = (state, event) => {
977
- const usage = mapUsage(event.message?.usage);
978
- return [usage ? { ...state, usage: mergeUsage(state.usage, usage) } : state, NO_EVENTS];
979
+ const usage = mapUsage(event.message?.usage, state.providerMetadataKey);
980
+ return [usage ? { ...state, usage: mergeUsage(state.usage, usage, state.providerMetadataKey) } : state, NO_EVENTS];
979
981
  };
980
982
  const onContentBlockStart = (state, event) => {
981
983
  const block = event.content_block;
@@ -1021,13 +1023,15 @@ const onContentBlockStart = (state, event) => {
1021
1023
  if (block.type === "thinking" && block.thinking !== undefined) {
1022
1024
  const events = [];
1023
1025
  const id = `reasoning-${event.index ?? 0}`;
1024
- const providerMetadata = block.signature === undefined ? undefined : anthropicMetadata({ signature: block.signature });
1025
- const lifecycle = Lifecycle.reasoningStart(state.lifecycle, events, id, providerMetadata);
1026
+ const metadata = block.signature === undefined
1027
+ ? undefined
1028
+ : providerMetadata(state.providerMetadataKey, { signature: block.signature });
1029
+ const lifecycle = Lifecycle.reasoningStart(state.lifecycle, events, id, metadata);
1026
1030
  return [
1027
1031
  {
1028
1032
  ...state,
1029
1033
  lifecycle: block.thinking
1030
- ? Lifecycle.reasoningDelta(lifecycle, events, id, block.thinking, providerMetadata)
1034
+ ? Lifecycle.reasoningDelta(lifecycle, events, id, block.thinking, metadata)
1031
1035
  : lifecycle,
1032
1036
  reasoningSignatures: event.index === undefined || block.signature === undefined
1033
1037
  ? state.reasoningSignatures
@@ -1044,12 +1048,12 @@ const onContentBlockStart = (state, event) => {
1044
1048
  return [
1045
1049
  {
1046
1050
  ...state,
1047
- lifecycle: Lifecycle.reasoningStart(state.lifecycle, events, `reasoning-${event.index ?? 0}`, anthropicMetadata({ redactedData: block.data })),
1051
+ lifecycle: Lifecycle.reasoningStart(state.lifecycle, events, `reasoning-${event.index ?? 0}`, providerMetadata(state.providerMetadataKey, { redactedData: block.data })),
1048
1052
  },
1049
1053
  events,
1050
1054
  ];
1051
1055
  }
1052
- const result = serverToolResultEvent(block);
1056
+ const result = serverToolResultEvent(block, state.providerMetadataKey);
1053
1057
  if (!result)
1054
1058
  return [state, NO_EVENTS];
1055
1059
  const events = [];
@@ -1114,14 +1118,14 @@ const onContentBlockStop = Effect.fn("AnthropicMessages.onContentBlockStop")(fun
1114
1118
  const signature = state.reasoningSignatures[event.index];
1115
1119
  const lifecycle = resultEvents.length
1116
1120
  ? Lifecycle.stepStart(state.lifecycle, events)
1117
- : Lifecycle.reasoningEnd(Lifecycle.textEnd(state.lifecycle, events, `text-${event.index}`), events, `reasoning-${event.index}`, signature === undefined ? undefined : anthropicMetadata({ signature }));
1121
+ : Lifecycle.reasoningEnd(Lifecycle.textEnd(state.lifecycle, events, `text-${event.index}`), events, `reasoning-${event.index}`, signature === undefined ? undefined : providerMetadata(state.providerMetadataKey, { signature }));
1118
1122
  events.push(...resultEvents);
1119
1123
  const reasoningSignatures = { ...state.reasoningSignatures };
1120
1124
  delete reasoningSignatures[event.index];
1121
1125
  return [{ ...state, lifecycle, tools: result.tools, reasoningSignatures }, events];
1122
1126
  });
1123
1127
  const onMessageDelta = (state, event) => {
1124
- const usage = mergeUsage(state.usage, mapUsage(event.usage));
1128
+ const usage = mergeUsage(state.usage, mapUsage(event.usage, state.providerMetadataKey), state.providerMetadataKey);
1125
1129
  return [
1126
1130
  {
1127
1131
  ...state,
@@ -1133,7 +1137,7 @@ const onMessageDelta = (state, event) => {
1133
1137
  },
1134
1138
  providerMetadata: event.delta?.stop_sequence === null || event.delta?.stop_sequence === undefined
1135
1139
  ? undefined
1136
- : anthropicMetadata({ stopSequence: event.delta.stop_sequence }),
1140
+ : providerMetadata(state.providerMetadataKey, { stopSequence: event.delta.stop_sequence }),
1137
1141
  },
1138
1142
  },
1139
1143
  NO_EVENTS,
@@ -1246,7 +1250,8 @@ export const protocol = Protocol.make({
1246
1250
  },
1247
1251
  stream: {
1248
1252
  event: Protocol.jsonEvent(AnthropicEvent),
1249
- initial: () => ({
1253
+ initial: (request) => ({
1254
+ providerMetadataKey: request.model.route.providerMetadataKey ?? String(request.model.provider),
1250
1255
  tools: ToolStream.empty(),
1251
1256
  reasoningSignatures: {},
1252
1257
  lifecycle: Lifecycle.initial(),
@@ -127,6 +127,7 @@ declare const BedrockConverseBody: Schema.Struct<{
127
127
  }>;
128
128
  export type BedrockConverseBody = Schema.Schema.Type<typeof BedrockConverseBody>;
129
129
  interface ParserState {
130
+ readonly providerMetadataKey: string;
130
131
  readonly tools: ToolStream.State<number>;
131
132
  readonly pendingFinish: {
132
133
  readonly reason: FinishReasonDetails;
@@ -181,15 +181,17 @@ const lowerToolChoice = (toolChoice) => ProviderShared.matchToolChoice("Bedrock
181
181
  required: () => ({ any: {} }),
182
182
  tool: (name) => ({ tool: { name } }),
183
183
  });
184
- const bedrockMetadata = (metadata) => ({ bedrock: metadata });
185
- const reasoningSignature = (part) => {
186
- const bedrock = part.providerMetadata?.bedrock;
184
+ const providerMetadata = (key, metadata) => ({ [key]: metadata });
185
+ const reasoningSignature = (part, providerMetadataKey) => {
186
+ const metadata = part.providerMetadata?.[providerMetadataKey];
187
187
  return (part.encrypted ??
188
- (ProviderShared.isRecord(bedrock) && typeof bedrock.signature === "string" ? bedrock.signature : undefined));
188
+ (ProviderShared.isRecord(metadata) && typeof metadata.signature === "string" ? metadata.signature : undefined));
189
189
  };
190
- const reasoningRedactedData = (part) => {
191
- const bedrock = part.providerMetadata?.bedrock;
192
- return ProviderShared.isRecord(bedrock) && typeof bedrock.redactedData === "string" ? bedrock.redactedData : undefined;
190
+ const reasoningRedactedData = (part, providerMetadataKey) => {
191
+ const metadata = part.providerMetadata?.[providerMetadataKey];
192
+ return ProviderShared.isRecord(metadata) && typeof metadata.redactedData === "string"
193
+ ? metadata.redactedData
194
+ : undefined;
193
195
  };
194
196
  const lowerToolCall = (part) => ({
195
197
  toolUse: {
@@ -230,6 +232,7 @@ const lowerToolResult = Effect.fn("BedrockConverse.lowerToolResult")(function* (
230
232
  });
231
233
  const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (request, breakpoints) {
232
234
  const messages = [];
235
+ const providerMetadataKey = request.model.route.providerMetadataKey ?? String(request.model.provider);
233
236
  for (const message of request.messages) {
234
237
  if (message.role === "system") {
235
238
  const part = yield* ProviderShared.wrappedSystemUpdate("Bedrock Converse", message);
@@ -276,8 +279,8 @@ const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (requ
276
279
  continue;
277
280
  }
278
281
  if (part.type === "reasoning") {
279
- const signature = reasoningSignature(part);
280
- const redactedData = reasoningRedactedData(part);
282
+ const signature = reasoningSignature(part, providerMetadataKey);
283
+ const redactedData = reasoningRedactedData(part, providerMetadataKey);
281
284
  if (signature === undefined && redactedData !== undefined) {
282
285
  content.push({ reasoningContent: { redactedContent: redactedData } });
283
286
  continue;
@@ -371,7 +374,7 @@ const mapFinishReason = (reason) => {
371
374
  };
372
375
  // AWS reports inputTokens separately from cache reads and writes.
373
376
  // Bedrock does not break reasoning out of outputTokens for current models.
374
- const mapUsage = (usage) => {
377
+ const mapUsage = (usage, providerMetadataKey) => {
375
378
  if (!usage)
376
379
  return undefined;
377
380
  const inputTokens = ProviderShared.sumTokens(usage.inputTokens, usage.cacheReadInputTokens, usage.cacheWriteInputTokens);
@@ -382,7 +385,7 @@ const mapUsage = (usage) => {
382
385
  cacheReadInputTokens: usage.cacheReadInputTokens,
383
386
  cacheWriteInputTokens: usage.cacheWriteInputTokens,
384
387
  totalTokens: ProviderShared.totalTokens(inputTokens, usage.outputTokens, usage.totalTokens),
385
- providerMetadata: { bedrock: usage },
388
+ providerMetadata: { [providerMetadataKey]: usage },
386
389
  });
387
390
  };
388
391
  const step = (state, event) => Effect.gen(function* () {
@@ -423,13 +426,13 @@ const step = (state, event) => Effect.gen(function* () {
423
426
  const reasoning = event.contentBlockDelta.delta.reasoningContent;
424
427
  const events = [];
425
428
  const redactedData = reasoning.redactedContent ?? reasoning.data;
426
- const providerMetadata = reasoning.signature
427
- ? bedrockMetadata({ signature: reasoning.signature })
429
+ const metadata = reasoning.signature
430
+ ? providerMetadata(state.providerMetadataKey, { signature: reasoning.signature })
428
431
  : redactedData !== undefined
429
- ? bedrockMetadata({ redactedData })
432
+ ? providerMetadata(state.providerMetadataKey, { redactedData })
430
433
  : undefined;
431
- const lifecycle = reasoning.text !== undefined || providerMetadata !== undefined
432
- ? Lifecycle.reasoningDelta(state.lifecycle, events, `reasoning-${index}`, reasoning.text ?? "", providerMetadata)
434
+ const lifecycle = reasoning.text !== undefined || metadata !== undefined
435
+ ? Lifecycle.reasoningDelta(state.lifecycle, events, `reasoning-${index}`, reasoning.text ?? "", metadata)
433
436
  : state.lifecycle;
434
437
  return [
435
438
  {
@@ -460,7 +463,7 @@ const step = (state, event) => Effect.gen(function* () {
460
463
  const lifecycle = resultEvents.length
461
464
  ? Lifecycle.stepStart(state.lifecycle, events)
462
465
  : Lifecycle.reasoningEnd(Lifecycle.textEnd(state.lifecycle, events, `text-${index}`), events, `reasoning-${index}`, state.reasoningSignatures[index]
463
- ? bedrockMetadata({ signature: state.reasoningSignatures[index] })
466
+ ? providerMetadata(state.providerMetadataKey, { signature: state.reasoningSignatures[index] })
464
467
  : undefined);
465
468
  events.push(...resultEvents);
466
469
  return [
@@ -491,7 +494,7 @@ const step = (state, event) => Effect.gen(function* () {
491
494
  ];
492
495
  }
493
496
  if (event.metadata) {
494
- const usage = mapUsage(event.metadata.usage) ?? state.pendingFinish?.usage;
497
+ const usage = mapUsage(event.metadata.usage, state.providerMetadataKey) ?? state.pendingFinish?.usage;
495
498
  return [
496
499
  {
497
500
  ...state,
@@ -548,7 +551,8 @@ export const protocol = Protocol.make({
548
551
  },
549
552
  stream: {
550
553
  event: BedrockEvent,
551
- initial: () => ({
554
+ initial: (request) => ({
555
+ providerMetadataKey: request.model.route.providerMetadataKey ?? String(request.model.provider),
552
556
  tools: ToolStream.empty(),
553
557
  pendingFinish: undefined,
554
558
  hasToolCalls: false,
@@ -197,6 +197,7 @@ export declare const protocol: Protocol<{
197
197
  } | null | undefined;
198
198
  }, {
199
199
  route: string;
200
+ providerMetadataKey: string;
200
201
  hasToolCalls: boolean;
201
202
  lifecycle: Lifecycle.State;
202
203
  }>;
@@ -197,19 +197,20 @@ const lowerUserPart = Effect.fn("Gemini.lowerUserPart")(function* (part) {
197
197
  const media = ProviderShared.normalizeMedia(part);
198
198
  return { inlineData: { mimeType: media.mime, data: media.base64 } };
199
199
  });
200
- const googleMetadata = (metadata) => ({ google: metadata });
201
- const thoughtSignature = (providerMetadata) => {
202
- const google = providerMetadata?.google;
203
- return ProviderShared.isRecord(google) && typeof google.thoughtSignature === "string"
204
- ? google.thoughtSignature
200
+ const providerMetadata = (key, metadata) => ({ [key]: metadata });
201
+ const thoughtSignature = (metadata, key) => {
202
+ const value = metadata?.[key];
203
+ return ProviderShared.isRecord(value) && typeof value.thoughtSignature === "string"
204
+ ? value.thoughtSignature
205
205
  : undefined;
206
206
  };
207
- const lowerToolCall = (part, omitIds) => ({
207
+ const lowerToolCall = (part, omitIds, metadataKey) => ({
208
208
  functionCall: { ...(omitIds ? {} : { id: part.id }), name: part.name, args: part.input },
209
- thoughtSignature: thoughtSignature(part.providerMetadata),
209
+ thoughtSignature: thoughtSignature(part.providerMetadata, metadataKey),
210
210
  });
211
211
  const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request) {
212
212
  const contents = [];
213
+ const metadataKey = request.model.route.providerMetadataKey ?? String(request.model.provider);
213
214
  const omitCallIds = omitsFunctionCallIds(request.model.id);
214
215
  const legacyToolMedia = routesLegacyToolMedia(request.model.id);
215
216
  let pendingMedia;
@@ -251,15 +252,19 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request) {
251
252
  if (!ProviderShared.supportsContent(part, ["text", "reasoning", "tool-call"]))
252
253
  return yield* ProviderShared.unsupportedContent("Gemini", "assistant", ["text", "reasoning", "tool-call"]);
253
254
  if (part.type === "text") {
254
- parts.push({ text: part.text, thoughtSignature: thoughtSignature(part.providerMetadata) });
255
+ parts.push({ text: part.text, thoughtSignature: thoughtSignature(part.providerMetadata, metadataKey) });
255
256
  continue;
256
257
  }
257
258
  if (part.type === "reasoning") {
258
- parts.push({ text: part.text, thought: true, thoughtSignature: thoughtSignature(part.providerMetadata) });
259
+ parts.push({
260
+ text: part.text,
261
+ thought: true,
262
+ thoughtSignature: thoughtSignature(part.providerMetadata, metadataKey),
263
+ });
259
264
  continue;
260
265
  }
261
266
  if (part.type === "tool-call") {
262
- const lowered = lowerToolCall(part, omitCallIds);
267
+ const lowered = lowerToolCall(part, omitCallIds, metadataKey);
263
268
  const signature = lowered.thoughtSignature;
264
269
  parts.push({
265
270
  ...lowered,
@@ -396,7 +401,7 @@ const fromRequest = Effect.fn("Gemini.fromRequest")(function* (request) {
396
401
  // `cachedContentTokenCount` subset. `candidatesTokenCount` is *exclusive*
397
402
  // of `thoughtsTokenCount` — visible-only, not a total — so we sum the two
398
403
  // to produce the inclusive `outputTokens` the rest of the contract expects.
399
- const mapUsage = (usage) => {
404
+ const mapUsage = (usage, metadataKey) => {
400
405
  if (!usage)
401
406
  return undefined;
402
407
  // Explicit provider nulls decode as `null`; normalize to `undefined` so the
@@ -418,7 +423,7 @@ const mapUsage = (usage) => {
418
423
  cacheReadInputTokens: cached,
419
424
  reasoningTokens: thoughts,
420
425
  totalTokens: ProviderShared.totalTokens(promptTokens, outputTokens, usage.totalTokenCount ?? undefined),
421
- providerMetadata: { google: usage },
426
+ providerMetadata: providerMetadata(metadataKey, usage),
422
427
  });
423
428
  };
424
429
  const mapFinishReason = (finishReason, hasToolCalls) => {
@@ -458,16 +463,18 @@ const finish = (state) => {
458
463
  const events = [];
459
464
  let lifecycle = state.lifecycle;
460
465
  if (state.reasoningSignature !== undefined)
461
- lifecycle = Lifecycle.reasoningEnd(lifecycle, events, "reasoning-0", googleMetadata({ thoughtSignature: state.reasoningSignature }));
466
+ lifecycle = Lifecycle.reasoningEnd(lifecycle, events, "reasoning-0", providerMetadata(state.providerMetadataKey, { thoughtSignature: state.reasoningSignature }));
462
467
  if (state.textSignature !== undefined)
463
- lifecycle = Lifecycle.textEnd(lifecycle, events, "text-0", googleMetadata({ thoughtSignature: state.textSignature }));
468
+ lifecycle = Lifecycle.textEnd(lifecycle, events, "text-0", providerMetadata(state.providerMetadataKey, { thoughtSignature: state.textSignature }));
464
469
  Lifecycle.finish(lifecycle, events, {
465
470
  reason: {
466
471
  normalized: promptBlockReason === undefined ? mapFinishReason(finishReason, state.hasToolCalls) : "content-filter",
467
472
  raw: finishReason,
468
473
  },
469
474
  usage: state.usage,
470
- providerMetadata: state.promptFeedback === undefined ? undefined : googleMetadata({ promptFeedback: state.promptFeedback }),
475
+ providerMetadata: state.promptFeedback === undefined
476
+ ? undefined
477
+ : providerMetadata(state.providerMetadataKey, { promptFeedback: state.promptFeedback }),
471
478
  });
472
479
  return events;
473
480
  };
@@ -475,7 +482,9 @@ const step = (state, event) => {
475
482
  const nextState = {
476
483
  ...state,
477
484
  promptFeedback: event.promptFeedback ?? state.promptFeedback,
478
- usage: event.usageMetadata ? (mapUsage(event.usageMetadata) ?? state.usage) : state.usage,
485
+ usage: event.usageMetadata
486
+ ? (mapUsage(event.usageMetadata, state.providerMetadataKey) ?? state.usage)
487
+ : state.usage,
479
488
  };
480
489
  const candidate = event.candidates?.[0];
481
490
  if (!candidate?.content)
@@ -510,11 +519,13 @@ const step = (state, event) => {
510
519
  textSignature = signature;
511
520
  if ("text" in part && part.text.length > 0) {
512
521
  if (part.thought) {
513
- lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", part.text, signature ? googleMetadata({ thoughtSignature: signature }) : undefined);
522
+ lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", part.text, signature ? providerMetadata(state.providerMetadataKey, { thoughtSignature: signature }) : undefined);
514
523
  continue;
515
524
  }
516
- lifecycle = Lifecycle.reasoningEnd(lifecycle, events, "reasoning-0", reasoningSignature ? googleMetadata({ thoughtSignature: reasoningSignature }) : undefined);
517
- lifecycle = Lifecycle.textDelta(lifecycle, events, "text-0", part.text, textSignature ? googleMetadata({ thoughtSignature: textSignature }) : undefined);
525
+ lifecycle = Lifecycle.reasoningEnd(lifecycle, events, "reasoning-0", reasoningSignature
526
+ ? providerMetadata(state.providerMetadataKey, { thoughtSignature: reasoningSignature })
527
+ : undefined);
528
+ lifecycle = Lifecycle.textDelta(lifecycle, events, "text-0", part.text, textSignature ? providerMetadata(state.providerMetadataKey, { thoughtSignature: textSignature }) : undefined);
518
529
  textSignature = undefined;
519
530
  continue;
520
531
  }
@@ -529,14 +540,16 @@ const step = (state, event) => {
529
540
  if (supplied !== undefined)
530
541
  seenCallIds.add(supplied);
531
542
  const id = supplied !== undefined && !duplicate ? supplied : `tool_${crypto.randomUUID().replaceAll("-", "")}`;
532
- lifecycle = Lifecycle.reasoningEnd(lifecycle, events, "reasoning-0", reasoningSignature ? googleMetadata({ thoughtSignature: reasoningSignature }) : undefined);
543
+ lifecycle = Lifecycle.reasoningEnd(lifecycle, events, "reasoning-0", reasoningSignature
544
+ ? providerMetadata(state.providerMetadataKey, { thoughtSignature: reasoningSignature })
545
+ : undefined);
533
546
  lifecycle = Lifecycle.stepStart(lifecycle, events);
534
547
  events.push(LLMEvent.toolCall({
535
548
  id,
536
549
  name: part.functionCall.name,
537
550
  input,
538
551
  providerMetadata: part.thoughtSignature
539
- ? googleMetadata({ thoughtSignature: part.thoughtSignature })
552
+ ? providerMetadata(state.providerMetadataKey, { thoughtSignature: part.thoughtSignature })
540
553
  : undefined,
541
554
  }));
542
555
  hasToolCalls = true;
@@ -572,6 +585,7 @@ export const protocol = Protocol.make({
572
585
  event: Protocol.jsonEvent(GeminiEvent),
573
586
  initial: (request) => ({
574
587
  route: `${request.model.provider}/${request.model.route.id}`,
588
+ providerMetadataKey: request.model.route.providerMetadataKey ?? String(request.model.provider),
575
589
  hasToolCalls: false,
576
590
  lifecycle: Lifecycle.initial(),
577
591
  }),
@@ -269,6 +269,7 @@ interface PendingToolDelta {
269
269
  readonly input: string;
270
270
  }
271
271
  export interface ParserState {
272
+ readonly providerMetadataKey: string;
272
273
  readonly tools: ToolStream.State<number>;
273
274
  readonly pendingTools: Partial<Record<number, PendingToolDelta>>;
274
275
  readonly toolCallEvents: ReadonlyArray<LLMEvent>;
@@ -207,16 +207,16 @@ const lowerMedia = Effect.fn("OpenAIChat.lowerMedia")(function* (part) {
207
207
  return { type: "image_url", image_url: { url: media.dataUrl } };
208
208
  });
209
209
  const openAICompatibleReasoningContent = (native) => isRecord(native) && typeof native.reasoning_content === "string" ? native.reasoning_content : undefined;
210
- const reasoningField = (part) => {
211
- const field = part.providerMetadata?.openai?.reasoningField;
210
+ const reasoningField = (part, providerMetadataKey) => {
211
+ const field = part.providerMetadata?.[providerMetadataKey]?.reasoningField;
212
212
  return typeof field === "string" ? field : undefined;
213
213
  };
214
- const reasoningDetails = (parts, native) => {
214
+ const reasoningDetails = (parts, native, providerMetadataKey) => {
215
215
  const observed = parts.flatMap((part) => {
216
- const details = part.providerMetadata?.openai?.reasoningDetails;
216
+ const details = part.providerMetadata?.[providerMetadataKey]?.reasoningDetails;
217
217
  return Array.isArray(details) ? details : [];
218
218
  });
219
- if (parts.some((part) => Array.isArray(part.providerMetadata?.openai?.reasoningDetails)))
219
+ if (parts.some((part) => Array.isArray(part.providerMetadata?.[providerMetadataKey]?.reasoningDetails)))
220
220
  return observed;
221
221
  if (isRecord(native) && Array.isArray(native.reasoning_details))
222
222
  return native.reasoning_details;
@@ -262,10 +262,12 @@ const lowerAssistantMessage = Effect.fn("OpenAIChat.lowerAssistantMessage")(func
262
262
  }
263
263
  }
264
264
  const text = reasoning.map((part) => part.text).join("");
265
- const details = reasoningDetails(reasoning, message.native?.openaiCompatible);
266
- const observedField = reasoning.map(reasoningField).find((value) => value !== undefined);
265
+ const details = reasoningDetails(reasoning, message.native?.openaiCompatible, options.providerMetadataKey);
266
+ const observedField = reasoning
267
+ .map((part) => reasoningField(part, options.providerMetadataKey))
268
+ .find((value) => value !== undefined);
267
269
  const nativeReasoning = openAICompatibleReasoningContent(message.native?.openaiCompatible);
268
- const fullyStructured = reasoning.every((part) => Array.isArray(part.providerMetadata?.openai?.reasoningDetails));
270
+ const fullyStructured = reasoning.every((part) => Array.isArray(part.providerMetadata?.[options.providerMetadataKey]?.reasoningDetails));
269
271
  const field = (() => {
270
272
  if (configuredField !== undefined && (requireReasoning || reasoning.length > 0 || nativeReasoning !== undefined))
271
273
  return configuredField;
@@ -359,6 +361,7 @@ const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request,
359
361
  const mistral = ["mistral", "devstral", "codestral", "pixtral", "mixtral"].some((family) => modelID.includes(family));
360
362
  const lowering = {
361
363
  ...options,
364
+ providerMetadataKey: request.model.route.providerMetadataKey ?? String(request.model.provider),
362
365
  toolCallID: (id) => {
363
366
  if (mistral)
364
367
  return id
@@ -644,7 +647,7 @@ const mapFinishReason = Effect.fn("OpenAIChat.mapFinishReason")(function* (event
644
647
  // Providers differ on cache-hit location: OpenAI uses
645
648
  // `prompt_tokens_details.cached_tokens`, DeepSeek uses
646
649
  // `prompt_cache_hit_tokens`, and Zai uses top-level `cached_tokens`.
647
- const mapUsage = (usage) => {
650
+ const mapUsage = (usage, providerMetadataKey) => {
648
651
  if (!usage)
649
652
  return undefined;
650
653
  const input = usage.prompt_tokens ?? undefined;
@@ -664,7 +667,7 @@ const mapUsage = (usage) => {
664
667
  cacheWriteInputTokens: cacheWrite,
665
668
  reasoningTokens: reasoning,
666
669
  totalTokens: ProviderShared.totalTokens(input, output, usage.total_tokens ?? undefined),
667
- providerMetadata: { openai: usage },
670
+ providerMetadata: { [providerMetadataKey]: usage },
668
671
  });
669
672
  };
670
673
  const toolIndexByID = (tools, pendingTools, id) => {
@@ -725,8 +728,8 @@ const conflictingReasoningTextDetails = (previous, current) => conflictingDetail
725
728
  conflictingDetailValue(previous.format, current.format) ||
726
729
  (Boolean(previous.signature) && Boolean(current.signature) && previous.signature !== current.signature);
727
730
  const conflictingDetailValue = (previous, current) => previous !== undefined && previous !== null && current !== undefined && current !== null && previous !== current;
728
- const reasoningMetadata = (field, details) => ({
729
- openai: {
731
+ const reasoningMetadata = (providerMetadataKey, field, details) => ({
732
+ [providerMetadataKey]: {
730
733
  ...(field ? { reasoningField: field } : {}),
731
734
  ...(details ? { reasoningDetails: details } : {}),
732
735
  },
@@ -751,7 +754,9 @@ const step = (state, event) => Effect.gen(function* () {
751
754
  // Moonshot (and a few other OpenAI-compatible providers) attach usage to
752
755
  // `choice.usage` instead of the top-level `usage` field.
753
756
  const choiceUsage = choice?.usage;
754
- const usage = mapUsage(event.usage) ?? (choiceUsage ? mapUsage(choiceUsage) : undefined) ?? state.usage;
757
+ const usage = mapUsage(event.usage, state.providerMetadataKey) ??
758
+ (choiceUsage ? mapUsage(choiceUsage, state.providerMetadataKey) : undefined) ??
759
+ state.usage;
755
760
  const rawFinishReason = choice?.finish_reason;
756
761
  const finishReason = rawFinishReason
757
762
  ? {
@@ -782,7 +787,7 @@ const step = (state, event) => Effect.gen(function* () {
782
787
  if (detailDelta !== undefined)
783
788
  appendReasoningDetails(state.reasoningDetails, detailDelta);
784
789
  const reasoningDetailsObserved = state.reasoningDetailsObserved || detailDelta !== undefined;
785
- const deltaMetadata = reasoningMetadata(reasoningField);
790
+ const deltaMetadata = reasoningMetadata(state.providerMetadataKey, reasoningField);
786
791
  const text = detailDelta?.length ? (detailText(detailDelta) ?? reasoning?.text) : reasoning?.text;
787
792
  if (text !== undefined)
788
793
  lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", text, deltaMetadata);
@@ -792,11 +797,11 @@ const step = (state, event) => Effect.gen(function* () {
792
797
  lifecycle = Lifecycle.reasoningStart(lifecycle, events, "reasoning-0", deltaMetadata);
793
798
  const reasoningEmitted = state.reasoningEmitted || lifecycle.reasoning.has("reasoning-0");
794
799
  if (delta?.content) {
795
- lifecycle = Lifecycle.reasoningEnd(lifecycle, events, "reasoning-0", reasoningMetadata(reasoningField, reasoningDetailsObserved ? state.reasoningDetails : undefined));
800
+ lifecycle = Lifecycle.reasoningEnd(lifecycle, events, "reasoning-0", reasoningMetadata(state.providerMetadataKey, reasoningField, reasoningDetailsObserved ? state.reasoningDetails : undefined));
796
801
  lifecycle = Lifecycle.textDelta(lifecycle, events, "text-0", delta.content);
797
802
  }
798
803
  if (delta?.refusal) {
799
- lifecycle = Lifecycle.reasoningEnd(lifecycle, events, "reasoning-0", reasoningMetadata(reasoningField, reasoningDetailsObserved ? state.reasoningDetails : undefined));
804
+ lifecycle = Lifecycle.reasoningEnd(lifecycle, events, "reasoning-0", reasoningMetadata(state.providerMetadataKey, reasoningField, reasoningDetailsObserved ? state.reasoningDetails : undefined));
800
805
  lifecycle = Lifecycle.textDelta(lifecycle, events, "text-0", delta.refusal);
801
806
  }
802
807
  // Compatible providers may omit indexes. Prefer durable identity, then use
@@ -841,6 +846,7 @@ const step = (state, event) => Effect.gen(function* () {
841
846
  : undefined;
842
847
  return [
843
848
  {
849
+ providerMetadataKey: state.providerMetadataKey,
844
850
  tools: finished?.tools ?? tools,
845
851
  pendingTools,
846
852
  toolCallEvents: finished?.events ?? state.toolCallEvents,
@@ -880,9 +886,9 @@ const finishEvents = Effect.fn("OpenAIChat.finishEvents")(function* (state) {
880
886
  normalized: state.finishReason.normalized === "stop" && hasToolCalls ? "tool-calls" : state.finishReason.normalized,
881
887
  }
882
888
  : { normalized: hasToolCalls ? "tool-calls" : "stop" };
883
- const metadata = reasoningMetadata(state.reasoningField, state.reasoningDetailsObserved ? state.reasoningDetails : undefined);
889
+ const metadata = reasoningMetadata(state.providerMetadataKey, state.reasoningField, state.reasoningDetailsObserved ? state.reasoningDetails : undefined);
884
890
  const started = state.reasoningDetailsObserved && !state.reasoningEmitted
885
- ? Lifecycle.reasoningStart(state.lifecycle, events, "reasoning-0", reasoningMetadata(state.reasoningField))
891
+ ? Lifecycle.reasoningStart(state.lifecycle, events, "reasoning-0", reasoningMetadata(state.providerMetadataKey, state.reasoningField))
886
892
  : state.lifecycle;
887
893
  const ended = Lifecycle.reasoningEnd(started, events, "reasoning-0", metadata);
888
894
  const lifecycle = toolCallEvents.length ? Lifecycle.stepStart(ended, events) : ended;
@@ -908,6 +914,7 @@ export const protocol = Protocol.make({
908
914
  stream: {
909
915
  event: Protocol.jsonEvent(OpenAIChatEvent),
910
916
  initial: (request) => ({
917
+ providerMetadataKey: request.model.route.providerMetadataKey ?? String(request.model.provider),
911
918
  tools: ToolStream.empty(),
912
919
  pendingTools: {},
913
920
  toolCallEvents: [],
@@ -17,6 +17,7 @@ export interface Settings extends ProviderPackage.Settings {
17
17
  readonly baseURL?: string;
18
18
  readonly credentials?: Credentials;
19
19
  readonly region?: string;
20
+ readonly topP?: number;
20
21
  readonly providerOptions?: OpenAIProviderOptionsInput;
21
22
  }
22
23
  export declare const routes: (Route<{
@@ -9,7 +9,7 @@ export const id = ProviderID.make("amazon-bedrock");
9
9
  const responsesRoute = Route.make({
10
10
  id: "bedrock-mantle-responses",
11
11
  provider: id,
12
- providerMetadataKey: OpenAIResponses.route.providerMetadataKey,
12
+ providerMetadataKey: "mantle",
13
13
  protocol: OpenAIResponses.protocol,
14
14
  endpoint: OpenAIResponses.route.endpoint,
15
15
  auth: OpenAIResponses.route.auth,
@@ -19,6 +19,7 @@ const responsesRoute = Route.make({
19
19
  const chatRoute = OpenAIChat.route.with({
20
20
  id: "bedrock-mantle-chat",
21
21
  provider: id,
22
+ providerMetadataKey: "mantle",
22
23
  });
23
24
  export const routes = [responsesRoute, chatRoute];
24
25
  const configuredRoute = (route, input) => {
@@ -63,6 +64,7 @@ const config = (settings) => {
63
64
  apiKey: settings.auth === "sigv4" ? undefined : settings.apiKey,
64
65
  baseURL: settings.baseURL,
65
66
  credentials: settings.credentials,
67
+ generation: settings.topP === undefined ? undefined : { topP: settings.topP },
66
68
  headers: settings.headers === undefined ? undefined : { ...settings.headers },
67
69
  http: settings.body === undefined ? undefined : { body: { ...settings.body } },
68
70
  providerOptions: settings.providerOptions,
@@ -10,6 +10,7 @@ const configuredRoute = (input) => {
10
10
  return BedrockConverse.route.with({
11
11
  ...rest,
12
12
  provider: id,
13
+ providerMetadataKey: "bedrock",
13
14
  endpoint: { baseURL: baseURL ?? bedrockBaseURL(resolvedRegion) },
14
15
  auth: apiKey === undefined ? BedrockConverse.sigV4Auth(credentials) : Auth.bearer(apiKey),
15
16
  });
@@ -5,6 +5,7 @@ export const id = ProviderID.make("google-vertex");
5
5
  const route = OpenAICompatibleChat.route.with({
6
6
  id: "google-vertex-chat",
7
7
  provider: id,
8
+ providerMetadataKey: "vertex",
8
9
  });
9
10
  export const routes = [route];
10
11
  const configuredRoute = (input) => {
@@ -5,6 +5,7 @@ export const id = ProviderID.make("google-vertex");
5
5
  const route = OpenAICompatibleResponses.route.with({
6
6
  id: "google-vertex-responses",
7
7
  provider: id,
8
+ providerMetadataKey: "vertex",
8
9
  providerOptions: { store: false },
9
10
  });
10
11
  export const routes = [route];
@@ -38,7 +38,7 @@ const protocol = {
38
38
  const route = Route.make({
39
39
  id: "google-vertex-gemini",
40
40
  provider: id,
41
- providerMetadataKey: "google",
41
+ providerMetadataKey: "vertex",
42
42
  protocol,
43
43
  endpoint: Endpoint.path(({ request }) => {
44
44
  const model = String(request.model.id);
@@ -84,6 +84,7 @@ const bodyOptions = (input) => {
84
84
  export const route = Route.make({
85
85
  id: ADAPTER,
86
86
  provider: profile.provider,
87
+ providerMetadataKey: "openrouter",
87
88
  protocol,
88
89
  endpoint: Endpoint.path("/chat/completions", { baseURL: profile.baseURL }),
89
90
  framing: Framing.sse,
@@ -49,6 +49,7 @@ export interface RouteDefaultsInput {
49
49
  export interface RoutePatch<Body, Prepared> extends RouteDefaultsInput {
50
50
  readonly id?: string;
51
51
  readonly provider?: string | ProviderID;
52
+ readonly providerMetadataKey?: string;
52
53
  readonly auth?: Auth.Definition;
53
54
  readonly transport?: Transport<Body, Prepared, unknown>;
54
55
  readonly endpoint?: EndpointPatch<Body>;
@@ -98,11 +98,15 @@ function makeFromTransport(input) {
98
98
  defaults: routeInput.defaults ?? {},
99
99
  body: protocol.body,
100
100
  with: (patch) => {
101
- const { id, provider, auth, transport, endpoint, ...defaults } = patch;
101
+ const { id, provider, providerMetadataKey, auth, transport, endpoint, ...defaults } = patch;
102
102
  return build({
103
103
  ...routeInput,
104
104
  id: id ?? routeInput.id,
105
105
  provider: provider ?? routeInput.provider,
106
+ providerMetadataKey: providerMetadataKey ??
107
+ (provider !== undefined && String(provider) !== String(routeInput.provider)
108
+ ? String(provider)
109
+ : routeInput.providerMetadataKey),
106
110
  auth: auth ?? routeInput.auth,
107
111
  endpoint: endpoint ? Endpoint.merge(routeInput.endpoint, endpoint) : routeInput.endpoint,
108
112
  transport: transport ?? routeInput.transport,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
- "version": "0.0.0-dev-18326",
3
+ "version": "0.0.0-dev-18332",
4
4
  "name": "@opencode-ai/ai",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -30,7 +30,7 @@
30
30
  "devDependencies": {
31
31
  "@clack/prompts": "1.0.0-alpha.1",
32
32
  "@effect/platform-node": "4.0.0-rc.111",
33
- "@opencode-ai/http-recorder": "0.0.0-dev-18326",
33
+ "@opencode-ai/http-recorder": "0.0.0-dev-18332",
34
34
  "@tsconfig/bun": "1.0.9",
35
35
  "@types/bun": "1.3.13",
36
36
  "@typescript/native-preview": "7.0.0-dev.20251207.1",
@@ -39,7 +39,7 @@
39
39
  "dependencies": {
40
40
  "@smithy/eventstream-codec": "4.2.14",
41
41
  "@smithy/util-utf8": "4.2.2",
42
- "@opencode-ai/schema": "0.0.0-dev-18326",
42
+ "@opencode-ai/schema": "0.0.0-dev-18332",
43
43
  "aws4fetch": "1.0.20",
44
44
  "effect": "4.0.0-rc.111",
45
45
  "google-auth-library": "10.5.0"