@opencode-ai/ai 0.0.0-beta-18311 → 0.0.0-beta-18371

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
  }),
@@ -495,14 +495,14 @@ export declare const Event: Schema.StructWithRest<Schema.Struct<{
495
495
  readonly item_id: Schema.optional<Schema.String>;
496
496
  readonly output_index: Schema.optional<Schema.Number>;
497
497
  readonly summary_index: Schema.optional<Schema.Number>;
498
- readonly item: Schema.optional<Schema.StructWithRest<Schema.Struct<{
498
+ readonly item: Schema.optional<Schema.NullOr<Schema.StructWithRest<Schema.Struct<{
499
499
  readonly type: Schema.String;
500
500
  readonly id: Schema.optional<Schema.String>;
501
501
  readonly call_id: Schema.optional<Schema.String>;
502
502
  readonly name: Schema.optional<Schema.String>;
503
503
  readonly arguments: Schema.optional<Schema.String>;
504
504
  readonly encrypted_content: Schema.optional<Schema.NullOr<Schema.String>>;
505
- }>, readonly [Schema.$Record<Schema.String, Schema.Unknown>]>>;
505
+ }>, readonly [Schema.$Record<Schema.String, Schema.Unknown>]>>>;
506
506
  readonly response: Schema.optional<Schema.StructWithRest<Schema.Struct<{
507
507
  readonly id: Schema.optional<Schema.String>;
508
508
  readonly service_tier: Schema.optional<Schema.NullOr<Schema.String>>;
@@ -1165,7 +1165,7 @@ export declare const protocol: Protocol<{
1165
1165
  readonly arguments?: string | undefined;
1166
1166
  readonly encrypted_content?: string | null | undefined;
1167
1167
  readonly call_id?: string | undefined;
1168
- } | undefined;
1168
+ } | null | undefined;
1169
1169
  readonly delta?: string | undefined;
1170
1170
  readonly response?: {
1171
1171
  readonly [x: string]: unknown;
@@ -242,7 +242,8 @@ export const Event = Schema.StructWithRest(Schema.Struct({
242
242
  item_id: Schema.optional(Schema.String),
243
243
  output_index: Schema.optional(Schema.Number),
244
244
  summary_index: Schema.optional(Schema.Number),
245
- item: Schema.optional(StreamItem),
245
+ // OutputItemAdded/Done permit a null item in the Open Responses OpenAPI schema.
246
+ item: optionalNull(StreamItem),
246
247
  response: Schema.optional(Schema.StructWithRest(Schema.Struct({
247
248
  id: Schema.optional(Schema.String),
248
249
  service_tier: optionalNull(Schema.String),
@@ -598,7 +599,7 @@ const mapFinishReason = (event, hasFunctionCall) => {
598
599
  export const providerMetadata = (state, metadata) => ({
599
600
  [state.providerMetadataKey]: metadata,
600
601
  });
601
- const isReasoningItem = (item) => item.type === "reasoning" && typeof item.id === "string" && item.id.length > 0;
602
+ const isReasoningItem = (item) => item.type === "reasoning" && typeof item.id === "string";
602
603
  const NO_EVENTS = [];
603
604
  // `response.completed` / `response.incomplete` are clean finishes that emit a
604
605
  // `finish` event; `response.failed` and `error` are hard failures. All four end
@@ -669,7 +670,7 @@ const reasoningMetadata = (state, item) => providerMetadata(state, { itemId: ite
669
670
  // best-effort, not guaranteed.
670
671
  const onOutputItemAdded = (state, event) => {
671
672
  const item = event.item;
672
- if (item?.type === "message" && item.id) {
673
+ if (item?.type === "message" && item.id !== undefined) {
673
674
  const phase = messagePhase(item.phase);
674
675
  return [
675
676
  {
@@ -701,7 +702,7 @@ const onOutputItemAdded = (state, event) => {
701
702
  if (item?.type !== "function_call" || !item.call_id)
702
703
  return [state, NO_EVENTS];
703
704
  const id = item.id ?? item.call_id;
704
- const metadata = item.id ? providerMetadata(state, { itemId: item.id }) : undefined;
705
+ const metadata = item.id !== undefined ? providerMetadata(state, { itemId: item.id }) : undefined;
705
706
  const events = [];
706
707
  const lifecycle = Lifecycle.stepStart(state.lifecycle, events);
707
708
  return [
@@ -719,7 +720,7 @@ const onOutputItemAdded = (state, event) => {
719
720
  ];
720
721
  };
721
722
  const onReasoningSummaryPartAdded = (state, event) => {
722
- if (!event.item_id || event.summary_index === undefined)
723
+ if (event.item_id === undefined || event.summary_index === undefined)
723
724
  return [state, NO_EVENTS];
724
725
  const item = state.reasoningItems[event.item_id];
725
726
  if (!item)
@@ -749,7 +750,7 @@ const onReasoningSummaryPartAdded = (state, event) => {
749
750
  ];
750
751
  };
751
752
  const onReasoningSummaryPartDone = (state, event) => {
752
- if (!event.item_id || event.summary_index === undefined)
753
+ if (event.item_id === undefined || event.summary_index === undefined)
753
754
  return [state, NO_EVENTS];
754
755
  const item = state.reasoningItems[event.item_id];
755
756
  if (!item)
@@ -772,7 +773,7 @@ const onReasoningSummaryPartDone = (state, event) => {
772
773
  ];
773
774
  };
774
775
  const onFunctionCallArgumentsDelta = Effect.fn("OpenResponses.onFunctionCallArgumentsDelta")(function* (state, event) {
775
- if (!event.item_id)
776
+ if (event.item_id === undefined)
776
777
  return [state, NO_EVENTS];
777
778
  const tool = state.tools[event.item_id];
778
779
  if (!tool)
@@ -800,7 +801,7 @@ const onOutputItemDone = Effect.fn("OpenResponses.onOutputItemDone")(function* (
800
801
  const item = event.item;
801
802
  if (!item)
802
803
  return [state, NO_EVENTS];
803
- if (item.type === "message" && item.id) {
804
+ if (item.type === "message" && item.id !== undefined) {
804
805
  const itemPhase = messagePhase(item.phase);
805
806
  const phase = itemPhase === undefined ? state.messagePhases[item.id] : itemPhase;
806
807
  const events = [];
@@ -826,7 +827,7 @@ const onOutputItemDone = Effect.fn("OpenResponses.onOutputItemDone")(function* (
826
827
  : ToolStream.start(state.tools, id, {
827
828
  id: item.call_id,
828
829
  name: item.name,
829
- providerMetadata: item.id ? providerMetadata(state, { itemId: item.id }) : undefined,
830
+ providerMetadata: item.id !== undefined ? providerMetadata(state, { itemId: item.id }) : undefined,
830
831
  });
831
832
  const result = item.arguments === undefined
832
833
  ? yield* ToolStream.finish(state.id, tools, id)
@@ -874,7 +875,7 @@ const onResponseFinish = Effect.fn("OpenResponses.onResponseFinish")(function* (
874
875
  const reconciled = event.type === "response.completed"
875
876
  ? yield* Effect.reduce(event.response?.output ?? [], () => [state, NO_EVENTS], ([current, events], item) => {
876
877
  const id = item.id ?? (item.type === "function_call" ? item.call_id : undefined);
877
- if (!id ||
878
+ if (id === undefined ||
878
879
  ((item.type !== "function_call" || !current.tools[id]) &&
879
880
  (item.type !== "reasoning" || !current.reasoningItems[id])))
880
881
  return Effect.succeed([current, events]);
@@ -945,11 +946,12 @@ export const providerFailure = (id, event, fallback) => {
945
946
  };
946
947
  const providerError = (state, event, fallback) => providerFailure(state.id, event, fallback);
947
948
  export const step = (state, input) => {
948
- const event = input.item_id && outputItemID(state, input) !== input.item_id
949
+ // The OpenAPI requires string IDs but imposes no minLength; empty is not missing.
950
+ const event = input.item_id !== undefined && outputItemID(state, input) !== input.item_id
949
951
  ? { ...input, item_id: outputItemID(state, input) }
950
952
  : input;
951
953
  if (event.type === "response.output_text.delta" || event.type === "response.output_text.done") {
952
- if (!event.item_id)
954
+ if (event.item_id === undefined)
953
955
  return ProviderShared.eventError(state.id, `${event.type} is missing item_id`);
954
956
  return Effect.succeed(event.type === "response.output_text.delta"
955
957
  ? onOutputTextDelta(state, event, event.item_id)
@@ -957,46 +959,46 @@ export const step = (state, input) => {
957
959
  }
958
960
  if (event.type === "response.refusal.delta" || event.type === "response.refusal.done") {
959
961
  const value = event.type === "response.refusal.delta" ? event.delta : event.refusal;
960
- if (!event.item_id || typeof value !== "string")
962
+ if (event.item_id === undefined || typeof value !== "string")
961
963
  return ProviderShared.eventError(state.id, `${event.type} is malformed`);
962
964
  return Effect.succeed(event.type === "response.refusal.delta"
963
965
  ? onOutputTextDelta(state, event, event.item_id)
964
966
  : onOutputTextDone(state, { ...event, text: value }, event.item_id));
965
967
  }
966
968
  if (event.type === "response.reasoning.delta" || event.type === "response.reasoning_summary_text.delta") {
967
- if (!event.item_id)
969
+ if (event.item_id === undefined)
968
970
  return ProviderShared.eventError(state.id, `${event.type} is missing item_id`);
969
971
  return Effect.succeed(onReasoningDelta(state, event, event.item_id));
970
972
  }
971
973
  if (event.type === "response.reasoning.done" ||
972
974
  event.type === "response.reasoning_summary_text.done" ||
973
975
  event.type === "response.reasoning_text.done") {
974
- if (!event.item_id)
976
+ if (event.item_id === undefined)
975
977
  return ProviderShared.eventError(state.id, `${event.type} is missing item_id`);
976
978
  return Effect.succeed(onReasoningDone(state, event, event.item_id));
977
979
  }
978
980
  if (event.type === "response.reasoning_summary_part.added")
979
- return event.item_id
981
+ return event.item_id !== undefined
980
982
  ? Effect.succeed(onReasoningSummaryPartAdded(state, event))
981
983
  : ProviderShared.eventError(state.id, `${event.type} is missing item_id`);
982
984
  if (event.type === "response.reasoning_summary_part.done")
983
- return event.item_id
985
+ return event.item_id !== undefined
984
986
  ? Effect.succeed(onReasoningSummaryPartDone(state, event))
985
987
  : ProviderShared.eventError(state.id, `${event.type} is missing item_id`);
986
988
  if (event.type === "response.output_item.added") {
987
- if (event.item?.type === "message" && !event.item.id)
989
+ if (event.item?.type === "message" && event.item.id === undefined)
988
990
  return ProviderShared.eventError(state.id, `${event.type} message is missing id`);
989
991
  const id = event.item?.id ?? (event.item?.type === "function_call" ? event.item.call_id : undefined);
990
- return Effect.succeed(onOutputItemAdded(event.output_index !== undefined && id
992
+ return Effect.succeed(onOutputItemAdded(event.output_index !== undefined && id !== undefined
991
993
  ? { ...state, outputItems: { ...state.outputItems, [event.output_index]: id } }
992
994
  : state, event));
993
995
  }
994
996
  if (event.type === "response.function_call_arguments.delta" || event.type === "response.function_call_arguments.done")
995
- return event.item_id
997
+ return event.item_id !== undefined
996
998
  ? onFunctionCallArgumentsDelta(state, event)
997
999
  : ProviderShared.eventError(state.id, `${event.type} is missing item_id`);
998
1000
  if (event.type === "response.output_item.done") {
999
- if (event.item?.type === "message" && !event.item.id)
1001
+ if (event.item?.type === "message" && event.item.id === undefined)
1000
1002
  return ProviderShared.eventError(state.id, `${event.type} message is missing id`);
1001
1003
  return onOutputItemDone(state, event);
1002
1004
  }
@@ -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: [],
@@ -386,7 +386,7 @@ export declare const protocol: Protocol<{
386
386
  readonly arguments?: string | undefined;
387
387
  readonly encrypted_content?: string | null | undefined;
388
388
  readonly call_id?: string | undefined;
389
- } | undefined;
389
+ } | null | undefined;
390
390
  readonly delta?: string | undefined;
391
391
  readonly response?: {
392
392
  readonly [x: string]: unknown;
@@ -147,7 +147,7 @@ const HOSTED_TOOLS = {
147
147
  };
148
148
  const step = (state, event) => {
149
149
  if (event.type === "response.reasoning_text.delta")
150
- return event.item_id
150
+ return event.item_id !== undefined
151
151
  ? Effect.succeed(OpenResponses.onReasoningDelta(state, event, OpenResponses.outputItemID(state, event) ?? event.item_id))
152
152
  : ProviderShared.eventError(ADAPTER, `${event.type} is missing item_id`);
153
153
  if (event.type === "response.output_item.done" && event.item && ResponsesHostedTools.isItem(event.item, HOSTED_TOOLS))
@@ -187,7 +187,7 @@ export declare const protocol: Protocol<{
187
187
  readonly arguments?: string | undefined;
188
188
  readonly encrypted_content?: string | null | undefined;
189
189
  readonly call_id?: string | undefined;
190
- } | undefined;
190
+ } | null | undefined;
191
191
  readonly delta?: string | undefined;
192
192
  readonly response?: {
193
193
  readonly [x: string]: unknown;
@@ -1,2 +1,2 @@
1
- export { chatModel as model } from "../amazon-bedrock-mantle.js";
1
+ export { responsesModel as model } from "../amazon-bedrock-mantle.js";
2
2
  export type { Settings } from "../amazon-bedrock-mantle.js";
@@ -1 +1 @@
1
- export { chatModel as model } from "../amazon-bedrock-mantle.js";
1
+ export { responsesModel as model } from "../amazon-bedrock-mantle.js";
@@ -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) => {
@@ -47,7 +48,7 @@ export const configure = (input = {}) => {
47
48
  .model({ id: modelID });
48
49
  return {
49
50
  id,
50
- model: chat,
51
+ model: responses,
51
52
  chat,
52
53
  responses,
53
54
  configure,
@@ -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,
@@ -71,4 +73,4 @@ const config = (settings) => {
71
73
  };
72
74
  export const chatModel = (modelID, settings) => configure(config(settings)).chat(modelID);
73
75
  export const responsesModel = (modelID, settings) => configure(config(settings)).responses(modelID);
74
- export const model = chatModel;
76
+ export const model = responsesModel;
@@ -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,
@@ -6,14 +6,6 @@ export class Service extends Context.Service()("@opencode/AI/RequestExecutor") {
6
6
  }
7
7
  const headerDetails = (headers) => Object.fromEntries(Object.entries(headers).map(([name, value]) => [name, String(value)]));
8
8
  const normalizedHeaders = (headers) => Object.fromEntries(Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value]));
9
- const requestId = (headers) => {
10
- return (headers["x-request-id"] ??
11
- headers["request-id"] ??
12
- headers["x-amzn-requestid"] ??
13
- headers["x-amz-request-id"] ??
14
- headers["x-goog-request-id"] ??
15
- headers["cf-ray"]);
16
- };
17
9
  const retryAfterMs = (headers) => {
18
10
  const millis = Number(headers["retry-after-ms"]);
19
11
  if (Number.isFinite(millis))
@@ -95,7 +87,6 @@ const responseHttp = (input) => new HttpContext({
95
87
  request: requestDetails(input.request),
96
88
  response: responseDetails(input.response),
97
89
  ...input.body,
98
- requestId: input.requestId,
99
90
  rateLimit: input.rateLimit,
100
91
  });
101
92
  const statusError = (request) => (response) => Effect.gen(function* () {
@@ -118,7 +109,6 @@ const statusError = (request) => (response) => Effect.gen(function* () {
118
109
  request,
119
110
  response,
120
111
  body: details,
121
- requestId: requestId(headers),
122
112
  rateLimit,
123
113
  }),
124
114
  }),
@@ -146,7 +136,6 @@ export const classifyHttpFailure = (input) => {
146
136
  ? undefined
147
137
  : new HttpResponseDetails({ status: input.status, headers: headerDetails(Headers.fromInput(headers)) }),
148
138
  ...details,
149
- requestId: requestId(headers),
150
139
  rateLimit,
151
140
  }),
152
141
  });
@@ -28,7 +28,6 @@ declare const HttpContext_base: Schema.Class<HttpContext, Schema.Struct<{
28
28
  readonly response: Schema.optional<typeof HttpResponseDetails>;
29
29
  readonly body: Schema.optional<Schema.String>;
30
30
  readonly bodyTruncated: Schema.optional<Schema.Boolean>;
31
- readonly requestId: Schema.optional<Schema.String>;
32
31
  readonly rateLimit: Schema.optional<typeof HttpRateLimitDetails>;
33
32
  }>, {}>;
34
33
  export declare class HttpContext extends HttpContext_base {
@@ -26,7 +26,6 @@ export class HttpContext extends Schema.Class("AI.HttpContext")({
26
26
  response: Schema.optional(HttpResponseDetails),
27
27
  body: Schema.optional(Schema.String),
28
28
  bodyTruncated: Schema.optional(Schema.Boolean),
29
- requestId: Schema.optional(Schema.String),
30
29
  rateLimit: Schema.optional(HttpRateLimitDetails),
31
30
  }) {
32
31
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
- "version": "0.0.0-beta-18311",
3
+ "version": "0.0.0-beta-18371",
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-beta-18311",
33
+ "@opencode-ai/http-recorder": "0.0.0-beta-18371",
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-beta-18311",
42
+ "@opencode-ai/schema": "0.0.0-beta-18371",
43
43
  "aws4fetch": "1.0.20",
44
44
  "effect": "4.0.0-rc.111",
45
45
  "google-auth-library": "10.5.0"