@opencode/ai 0.0.0-dev-20271 → 0.0.0-dev-20276

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.
@@ -13,6 +13,7 @@ import { JsonObject, knownString, optionalArray, optionalNull, ProviderShared }
13
13
  import { classifyProviderFailure } from "../provider-error.js";
14
14
  import { effortUpdate, resolveEffortUpdates } from "../effort-updates.js";
15
15
  import * as Cache from "./utils/cache.js";
16
+ import { claudeVersion, supportsThinkingBlockBinding, THINKING_BINDING_BETA } from "./utils/claude-model.js";
16
17
  import { Lifecycle } from "./utils/lifecycle.js";
17
18
  import { ToolStream } from "./utils/tool-stream.js";
18
19
  const ADAPTER = "anthropic-messages";
@@ -850,20 +851,6 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (re
850
851
  }
851
852
  return messages;
852
853
  });
853
- // Accept gateway namespaces and Vertex suffixes without treating a snapshot date as a minor version.
854
- const claudeVersion = (id) => {
855
- const match = /(?:^|[./])claude-(?<family>[a-z]+)-(?<major>\d+)(?:[.-](?<minor>\d{1,2}))?(?:$|[-:@])/.exec(id.toLowerCase())?.groups;
856
- if (!match)
857
- return undefined;
858
- return { family: match.family, major: Number(match.major), minor: Number(match.minor ?? 0) };
859
- };
860
- const supportsThinkingBlockBinding = (model) => {
861
- const override = model.compatibility?.supportsThinkingBlockBinding;
862
- if (override !== undefined)
863
- return override;
864
- const version = claudeVersion(model.id);
865
- return version !== undefined && (version.major > 5 || (version.major === 5 && version.minor >= 1));
866
- };
867
854
  const supportsEffortUpdates = (model) => {
868
855
  const override = model.compatibility?.supportsEffortUpdates;
869
856
  if (override !== undefined)
@@ -1437,7 +1424,7 @@ function requiredBetaHeaders(body) {
1437
1424
  betas.push("mid-conversation-output-config-2026-07-01");
1438
1425
  const thinking = body.thinking;
1439
1426
  if (thinking && thinking.type !== "disabled" && thinking.block_binding)
1440
- betas.push("thinking-binding-controls-2026-08-01");
1427
+ betas.push(THINKING_BINDING_BETA);
1441
1428
  return betas;
1442
1429
  }
1443
1430
  export const route = Route.make({
@@ -141,7 +141,7 @@ interface ParserState {
141
141
  readonly hasToolCalls: boolean;
142
142
  readonly lifecycle: Lifecycle.State;
143
143
  readonly reasoningSignatures: Readonly<Record<number, string>>;
144
- readonly reasoningRedactedContent: Readonly<Record<number, ReadonlyArray<Uint8Array>>>;
144
+ readonly reasoningRedactedContent: Readonly<Record<number, Uint8Array[]>>;
145
145
  }
146
146
  /**
147
147
  * The Bedrock Converse protocol — request body construction, body schema, and
@@ -9,6 +9,7 @@ import { JsonObject, optionalArray, ProviderShared } from "./shared.js";
9
9
  import { BedrockAuth } from "./utils/bedrock-auth.js";
10
10
  import { BedrockCache } from "./utils/bedrock-cache.js";
11
11
  import { BedrockMedia } from "./utils/bedrock-media.js";
12
+ import { supportsThinkingBlockBinding, THINKING_BINDING_BETA } from "./utils/claude-model.js";
12
13
  import { Lifecycle } from "./utils/lifecycle.js";
13
14
  import { MistralToolID } from "./utils/mistral-tool-id.js";
14
15
  import { ToolStream } from "./utils/tool-stream.js";
@@ -351,18 +352,33 @@ const Options = Schema.Struct({
351
352
  const decodeOptions = ProviderShared.validateWith(Schema.decodeUnknownEffect(Options));
352
353
  // Claude on Bedrock requires the thinking budget below `maxTokens`, with a minimum of 1,024.
353
354
  const MIN_THINKING_BUDGET = 1_024;
355
+ const isThinkingDisabled = Schema.is(Schema.Struct({
356
+ additionalModelRequestFields: Schema.Struct({ thinking: Schema.Struct({ type: Schema.Literal("disabled") }) }),
357
+ }));
358
+ // Claude 5.1+ binds each thinking signature to the prefix above it. Ask Bedrock to drop the affected blocks instead of
359
+ // failing when that prefix changes. `http.body` overlays this field by field, so callers can still override it.
360
+ const applyThinkingBindingDefault = (request, thinking) => {
361
+ if (isThinkingDisabled(request.http?.body))
362
+ return thinking;
363
+ if (!supportsThinkingBlockBinding(request.model))
364
+ return thinking;
365
+ return {
366
+ ...(thinking ?? { type: "adaptive" }),
367
+ block_binding: { prefix_mismatch_behavior: "drop_block" },
368
+ };
369
+ };
354
370
  const fromRequest = Effect.fn("BedrockConverse.fromRequest")(function* (request) {
355
371
  const toolChoice = request.toolChoice ? yield* lowerToolChoice(request.toolChoice) : undefined;
356
372
  const flattened = ProviderShared.flattenToolRequest(request);
357
373
  const generation = request.generation;
358
374
  const options = yield* decodeOptions(request.providerOptions ?? {});
359
375
  const maxTokens = isNova2(request.model) && isHighReasoningEffort(request.http?.body) ? undefined : generation?.maxTokens;
360
- const thinking = options.thinking === undefined
376
+ const thinking = applyThinkingBindingDefault(request, options.thinking === undefined
361
377
  ? undefined
362
378
  : {
363
379
  type: "enabled",
364
380
  budget_tokens: ProviderShared.fitThinkingBudget(options.thinking.budgetTokens, maxTokens, MIN_THINKING_BUDGET),
365
- };
381
+ });
366
382
  // Bedrock-Claude shares Anthropic's 4-breakpoint cap. Spend the budget in
367
383
  // tools → system → messages order to favour the highest-impact prefixes.
368
384
  const breakpoints = BedrockCache.breakpoints(request.model.id);
@@ -407,6 +423,8 @@ const fromRequest = Effect.fn("BedrockConverse.fromRequest")(function* (request)
407
423
  : {
408
424
  ...(generation?.topK === undefined ? {} : { top_k: generation.topK }),
409
425
  ...(thinking === undefined ? {} : { thinking }),
426
+ // Converse takes Anthropic betas in the body, and Bedrock rejects `block_binding` without this one.
427
+ ...(thinking?.block_binding === undefined ? {} : { anthropic_beta: [THINKING_BINDING_BETA] }),
410
428
  },
411
429
  };
412
430
  });
@@ -478,25 +496,29 @@ const step = (state, event) => Effect.gen(function* () {
478
496
  const index = event.contentBlockDelta.contentBlockIndex;
479
497
  const reasoning = event.contentBlockDelta.delta.reasoningContent;
480
498
  const events = [];
481
- const redactedChunks = yield* (() => {
499
+ const redactedChunk = yield* (() => {
482
500
  if (reasoning.redactedContent === undefined)
483
501
  return Effect.succeed(undefined);
484
- return Effect.fromResult(Encoding.decodeBase64(reasoning.redactedContent)).pipe(Effect.map((chunk) => [...(state.reasoningRedactedContent[index] ?? []), chunk]), Effect.mapError((cause) => ProviderShared.eventError(ADAPTER, "Bedrock Converse reasoningContent.redactedContent contains invalid base64 data", undefined, cause)));
502
+ return Effect.fromResult(Encoding.decodeBase64(reasoning.redactedContent)).pipe(Effect.mapError((cause) => ProviderShared.eventError(ADAPTER, "Bedrock Converse reasoningContent.redactedContent contains invalid base64 data", undefined, cause)));
485
503
  })();
486
- const redactedData = redactedChunks === undefined ? reasoning.data : encodeRedactedContent(redactedChunks);
504
+ const redactedChunks = state.reasoningRedactedContent[index] ?? [];
505
+ if (redactedChunk !== undefined)
506
+ redactedChunks.push(redactedChunk);
487
507
  const metadata = (() => {
488
508
  if (reasoning.signature)
489
509
  return providerMetadata(state.providerMetadataKey, { signature: reasoning.signature });
490
- if (redactedData !== undefined)
491
- return providerMetadata(state.providerMetadataKey, { redactedData });
510
+ if (redactedChunk === undefined && reasoning.data !== undefined)
511
+ return providerMetadata(state.providerMetadataKey, { redactedData: reasoning.data });
492
512
  })();
493
513
  const lifecycle = (() => {
494
- if (reasoning.text === undefined && metadata === undefined)
495
- return state.lifecycle;
496
- return Lifecycle.reasoningDelta(state.lifecycle, events, `reasoning-${index}`, reasoning.text ?? "", metadata);
514
+ if (reasoning.text !== undefined || metadata !== undefined)
515
+ return Lifecycle.reasoningDelta(state.lifecycle, events, `reasoning-${index}`, reasoning.text ?? "", metadata);
516
+ if (redactedChunk !== undefined)
517
+ return Lifecycle.reasoningStart(state.lifecycle, events, `reasoning-${index}`);
518
+ return state.lifecycle;
497
519
  })();
498
520
  const reasoningRedactedContent = (() => {
499
- if (redactedChunks !== undefined)
521
+ if (redactedChunk !== undefined)
500
522
  return { ...state.reasoningRedactedContent, [index]: redactedChunks };
501
523
  if (reasoning.data === undefined)
502
524
  return state.reasoningRedactedContent;
@@ -609,7 +631,11 @@ const onHalt = (state) => {
609
631
  return state.finishReason.normalized;
610
632
  })();
611
633
  const events = [];
612
- Lifecycle.finish(state.lifecycle, events, {
634
+ const lifecycle = Object.entries(state.reasoningRedactedContent).reduce((current, [index, chunks]) => {
635
+ const signature = state.reasoningSignatures[Number(index)];
636
+ return Lifecycle.reasoningEnd(current, events, `reasoning-${index}`, providerMetadata(state.providerMetadataKey, signature ? { signature } : { redactedData: encodeRedactedContent(chunks) }));
637
+ }, state.lifecycle);
638
+ Lifecycle.finish(lifecycle, events, {
613
639
  reason: {
614
640
  ...state.finishReason,
615
641
  normalized,
@@ -0,0 +1,8 @@
1
+ import type { LLMRequest } from "../../schema/index.js";
2
+ export declare const THINKING_BINDING_BETA = "thinking-binding-controls-2026-08-01";
3
+ export declare const claudeVersion: (id: string) => {
4
+ family: string;
5
+ major: number;
6
+ minor: number;
7
+ } | undefined;
8
+ export declare const supportsThinkingBlockBinding: (model: LLMRequest["model"]) => boolean;
@@ -0,0 +1,15 @@
1
+ export const THINKING_BINDING_BETA = "thinking-binding-controls-2026-08-01";
2
+ // Accept gateway namespaces and Vertex suffixes without treating a snapshot date as a minor version.
3
+ export const claudeVersion = (id) => {
4
+ const match = /(?:^|[./])claude-(?<family>[a-z]+)-(?<major>\d+)(?:[.-](?<minor>\d{1,2}))?(?:$|[-:@])/.exec(id.toLowerCase())?.groups;
5
+ if (!match)
6
+ return undefined;
7
+ return { family: match.family, major: Number(match.major), minor: Number(match.minor ?? 0) };
8
+ };
9
+ export const supportsThinkingBlockBinding = (model) => {
10
+ const override = model.compatibility?.supportsThinkingBlockBinding;
11
+ if (override !== undefined)
12
+ return override;
13
+ const version = claudeVersion(model.id);
14
+ return version !== undefined && (version.major > 5 || (version.major === 5 && version.minor >= 1));
15
+ };
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-20271",
3
+ "version": "0.0.0-dev-20276",
4
4
  "name": "@opencode/ai",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -34,7 +34,7 @@
34
34
  "devDependencies": {
35
35
  "@clack/prompts": "1.0.0-alpha.1",
36
36
  "@effect/platform-node": "4.0.0-rc.112",
37
- "@opencode/http-recorder": "0.0.0-dev-20271",
37
+ "@opencode/http-recorder": "0.0.0-dev-20276",
38
38
  "@tsconfig/bun": "1.0.9",
39
39
  "@types/bun": "1.4.0",
40
40
  "@typescript/native-preview": "7.0.0-dev.20251207.1",
@@ -44,7 +44,7 @@
44
44
  "@aws-sdk/credential-providers": "3.1057.0",
45
45
  "@smithy/eventstream-codec": "4.2.14",
46
46
  "@smithy/util-utf8": "4.2.2",
47
- "@opencode/schema": "0.0.0-dev-20271",
47
+ "@opencode/schema": "0.0.0-dev-20276",
48
48
  "aws4fetch": "1.0.20",
49
49
  "effect": "4.0.0-rc.112",
50
50
  "google-auth-library": "10.5.0"