@opencode/ai 0.0.0-dev-20125 → 0.0.0-dev-20127

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.
@@ -55,7 +55,10 @@ export const protocol = Protocol.make({
55
55
  return {
56
56
  ...(yield* OpenAIChat.protocol.body.from(req)),
57
57
  enable_thinking: opts.enableThinking,
58
- thinking_budget: opts.thinkingBudget,
58
+ // Alibaba also rejects an explicit budget that is not below `max_completion_tokens`.
59
+ thinking_budget: opts.thinkingBudget === undefined
60
+ ? undefined
61
+ : ProviderShared.fitThinkingBudget(opts.thinkingBudget, req.generation?.maxTokens),
59
62
  preserve_thinking: opts.preserveThinking,
60
63
  clear_thinking: opts.clearThinking,
61
64
  thinking: opts.thinking,
@@ -22,15 +22,17 @@ export const protocol = Protocol.make({
22
22
  from: Effect.fn("AlibabaMessages.fromRequest")(function* (req) {
23
23
  const opts = yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(Options))(req.providerOptions ?? {});
24
24
  // Model Studio accepts enabled thinking without Anthropic's mandatory token budget.
25
+ const body = yield* AnthropicMessages.protocol.body.from(LLMRequest.update(req, {
26
+ providerOptions: { ...req.providerOptions, thinking: undefined },
27
+ }));
28
+ const budget = opts.thinking?.budgetTokens ?? opts.thinking?.budget_tokens;
25
29
  return {
26
- ...(yield* AnthropicMessages.protocol.body.from(LLMRequest.update(req, {
27
- providerOptions: { ...req.providerOptions, thinking: undefined },
28
- }))),
30
+ ...body,
29
31
  thinking: opts.thinking === undefined
30
32
  ? undefined
31
33
  : {
32
34
  type: opts.thinking.type,
33
- budget_tokens: opts.thinking.budgetTokens ?? opts.thinking.budget_tokens,
35
+ budget_tokens: budget === undefined ? undefined : ProviderShared.fitThinkingBudget(budget, body.max_tokens),
34
36
  },
35
37
  };
36
38
  }),
@@ -20,6 +20,7 @@ const ADAPTER = "anthropic-messages";
20
20
  export const DEFAULT_BASE_URL = "https://api.anthropic.com/v1";
21
21
  export const PATH = "/messages";
22
22
  export const DEFAULT_MAX_TOKENS = 32_000;
23
+ const MIN_THINKING_BUDGET = 1_024;
23
24
  const DEFAULT_EFFORT = "high";
24
25
  const SSE_EVENTS = new Set([
25
26
  "message",
@@ -890,6 +891,13 @@ const applyThinkingBindingDefault = (model, thinking) => {
890
891
  },
891
892
  };
892
893
  };
894
+ // Anthropic also requires an explicit thinking budget below `max_tokens` and at or above its minimum.
895
+ const fitThinking = (thinking, maxTokens) => thinking?.type === "enabled"
896
+ ? {
897
+ ...thinking,
898
+ budget_tokens: ProviderShared.fitThinkingBudget(thinking.budget_tokens, maxTokens, MIN_THINKING_BUDGET),
899
+ }
900
+ : thinking;
893
901
  const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (request) {
894
902
  const options = yield* decodeOptions(request.providerOptions ?? {});
895
903
  const management = options.contextManagement;
@@ -920,6 +928,7 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques
920
928
  yield* Effect.logWarning(`Anthropic Messages: dropped ${breakpoints.dropped} cache breakpoint(s); the API allows at most ${ANTHROPIC_BREAKPOINT_CAP} per request.`);
921
929
  }
922
930
  const output_config = updates.effort === undefined && format === undefined ? undefined : { effort: updates.effort, format };
931
+ const maxTokens = generation?.maxTokens ?? DEFAULT_MAX_TOKENS;
923
932
  const body = {
924
933
  model: request.model.id,
925
934
  system,
@@ -927,12 +936,12 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques
927
936
  tools,
928
937
  tool_choice: toolChoice,
929
938
  stream: true,
930
- max_tokens: generation?.maxTokens ?? DEFAULT_MAX_TOKENS,
939
+ max_tokens: maxTokens,
931
940
  temperature: generation?.temperature,
932
941
  top_p: generation?.topP,
933
942
  top_k: generation?.topK,
934
943
  stop_sequences: generation?.stop,
935
- thinking: applyThinkingBindingDefault(request.model, options.thinking),
944
+ thinking: applyThinkingBindingDefault(request.model, fitThinking(options.thinking, maxTokens)),
936
945
  output_config,
937
946
  // top-level passthrough per SDK MessageCreateParamsBase:4638,4643,4649,4654,4670
938
947
  cache_control: options.cache_control ?? options.cacheControl,
@@ -15,6 +15,8 @@ import { ToolSchemaProjection } from "./utils/tool-schema.js";
15
15
  const ADAPTER = "gemini";
16
16
  // Google documents this sentinel for replaying Gemini 3 function calls after their original signature was lost.
17
17
  const SKIP_THOUGHT_SIGNATURE_VALIDATOR = "skip_thought_signature_validator";
18
+ // Gemini 2.5 rejects a budget under the model's minimum: 512 on Flash-Lite, the highest, and 128 on Pro.
19
+ const MIN_THINKING_BUDGET = 512;
18
20
  export const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
19
21
  // Gemini 3 rejects replayed function calls without a thought signature. Google's SDKs avoid that in normal chats by
20
22
  // retaining complete model responses, but OpenCode reconstructs durable history and may encounter an unsigned call
@@ -366,9 +368,16 @@ const fromRequest = Effect.fn("Gemini.fromRequest")(function* (request) {
366
368
  presencePenalty: generation?.presencePenalty,
367
369
  seed: generation?.seed,
368
370
  stopSequences: generation?.stop,
371
+ // Gemini accepts a budget above `maxOutputTokens`, but thinking then leaves the answer empty.
369
372
  thinkingConfig: options.thinkingConfig === undefined
370
373
  ? undefined
371
- : { ...options.thinkingConfig, includeThoughts: options.thinkingConfig.includeThoughts ?? true },
374
+ : {
375
+ ...options.thinkingConfig,
376
+ includeThoughts: options.thinkingConfig.includeThoughts ?? true,
377
+ thinkingBudget: options.thinkingConfig.thinkingBudget === undefined
378
+ ? undefined
379
+ : ProviderShared.fitThinkingBudget(options.thinkingConfig.thinkingBudget, generation?.maxTokens, MIN_THINKING_BUDGET),
380
+ },
372
381
  };
373
382
  return {
374
383
  cachedContent: options.cachedContent,
@@ -58,6 +58,12 @@ export declare const subtractTokens: (total: number | undefined, subtrahend: num
58
58
  * (e.g. Anthropic, whose `input_tokens` is already non-cached only).
59
59
  */
60
60
  export declare const sumTokens: (...values: ReadonlyArray<number | undefined>) => number | undefined;
61
+ /**
62
+ * Caps an explicit thinking budget at half the output limit. Thinking counts against the output limit, so a budget
63
+ * near it leaves the answer, a tool call, or a summary without room. Smaller budgets, special values such as `-1` and
64
+ * `0`, and requests without an output limit pass through unchanged.
65
+ */
66
+ export declare const fitThinkingBudget: (budget: number, maxTokens: number | undefined, minimum?: number) => number;
61
67
  export declare const eventError: (route: string, message: string, body?: string, cause?: unknown) => AIError;
62
68
  export declare const parseJson: (route: string, input: string, message: string) => Effect.Effect<unknown, AIError, never>;
63
69
  /**
@@ -75,6 +75,12 @@ export const sumTokens = (...values) => {
75
75
  return undefined;
76
76
  return values.reduce((acc, value) => acc + (value ?? 0), 0);
77
77
  };
78
+ /**
79
+ * Caps an explicit thinking budget at half the output limit. Thinking counts against the output limit, so a budget
80
+ * near it leaves the answer, a tool call, or a summary without room. Smaller budgets, special values such as `-1` and
81
+ * `0`, and requests without an output limit pass through unchanged.
82
+ */
83
+ export const fitThinkingBudget = (budget, maxTokens, minimum = 1) => maxTokens === undefined || budget <= maxTokens / 2 ? budget : Math.max(minimum, Math.floor(maxTokens / 2));
78
84
  export const eventError = (route, message, body, cause) => new AIError({
79
85
  reason: new InvalidProviderOutputError({ route, message, body, cause }),
80
86
  });
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-20125",
3
+ "version": "0.0.0-dev-20127",
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-20125",
37
+ "@opencode/http-recorder": "0.0.0-dev-20127",
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-20125",
47
+ "@opencode/schema": "0.0.0-dev-20127",
48
48
  "aws4fetch": "1.0.20",
49
49
  "effect": "4.0.0-rc.112",
50
50
  "google-auth-library": "10.5.0"