@opencode-ai/ai 0.0.0-dev-18234 → 0.0.0-dev-18237

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.
@@ -612,9 +612,13 @@ const endsInServerToolUse = (message) => {
612
612
  const last = message.content.at(-1);
613
613
  return message.role === "assistant" && last?.type === "tool-call" && last.providerExecuted === true;
614
614
  };
615
- const canUseNativeSystemUpdate = (messages, index) => {
616
- const previous = messages[index - 1];
617
- const next = messages[index + 1];
615
+ const canUseNativeSystemUpdate = (request, index) => {
616
+ const previous = request.messages[index - 1];
617
+ const next = request.messages[index + 1];
618
+ // Vertex currently rejects/404s for a system message after local tool results,
619
+ // so fold it into the user tool-result turn across continuations and history.
620
+ if (request.model.route.id === "google-vertex-messages" && previous?.role === "tool")
621
+ return false;
618
622
  return (previous !== undefined &&
619
623
  previous.role !== "system" &&
620
624
  (previous.role === "user" || previous.role === "tool" || endsInServerToolUse(previous)) &&
@@ -650,7 +654,7 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (re
650
654
  if (message.role === "system") {
651
655
  if (splitsLocalToolResults(request.messages, index))
652
656
  return yield* invalid("Anthropic Messages system updates cannot split a local tool call from its tool result");
653
- if (supportsNativeSystemUpdates(request) && canUseNativeSystemUpdate(request.messages, index)) {
657
+ if (supportsNativeSystemUpdates(request) && canUseNativeSystemUpdate(request, index)) {
654
658
  messages.push(yield* lowerNativeSystemUpdate(message, breakpoints));
655
659
  continue;
656
660
  }
@@ -703,26 +703,24 @@ const onOutputItemAdded = (state, event) => {
703
703
  events,
704
704
  ];
705
705
  }
706
- if (item?.type !== "function_call" || !item.id)
706
+ if (item?.type !== "function_call" || !item.call_id)
707
707
  return [state, NO_EVENTS];
708
- const metadata = providerMetadata(state, { itemId: item.id });
708
+ const id = item.id ?? item.call_id;
709
+ const metadata = item.id ? providerMetadata(state, { itemId: item.id }) : undefined;
709
710
  const events = [];
710
711
  const lifecycle = Lifecycle.stepStart(state.lifecycle, events);
711
712
  return [
712
713
  {
713
714
  ...state,
714
715
  lifecycle,
715
- tools: ToolStream.start(state.tools, item.id, {
716
- id: item.call_id ?? item.id,
716
+ tools: ToolStream.start(state.tools, id, {
717
+ id: item.call_id,
717
718
  name: item.name ?? "",
718
719
  input: item.arguments ?? "",
719
720
  providerMetadata: metadata,
720
721
  }),
721
722
  },
722
- [
723
- ...events,
724
- LLMEvent.toolInputStart({ id: item.call_id ?? item.id, name: item.name ?? "", providerMetadata: metadata }),
725
- ],
723
+ [...events, LLMEvent.toolInputStart({ id: item.call_id, name: item.name ?? "", providerMetadata: metadata })],
726
724
  ];
727
725
  };
728
726
  const onReasoningSummaryPartAdded = (state, event) => {
@@ -825,18 +823,19 @@ const onOutputItemDone = Effect.fn("OpenResponses.onOutputItemDone")(function* (
825
823
  ];
826
824
  }
827
825
  if (item.type === "function_call") {
828
- if (!item.id || !item.call_id || !item.name)
826
+ if (!item.call_id || !item.name)
829
827
  return [state, NO_EVENTS];
830
- const tools = state.tools[item.id]
828
+ const id = item.id ?? item.call_id;
829
+ const tools = state.tools[id]
831
830
  ? state.tools
832
- : ToolStream.start(state.tools, item.id, {
831
+ : ToolStream.start(state.tools, id, {
833
832
  id: item.call_id,
834
833
  name: item.name,
835
- providerMetadata: providerMetadata(state, { itemId: item.id }),
834
+ providerMetadata: item.id ? providerMetadata(state, { itemId: item.id }) : undefined,
836
835
  });
837
836
  const result = item.arguments === undefined
838
- ? yield* ToolStream.finish(state.id, tools, item.id)
839
- : yield* ToolStream.finishWithInput(state.id, tools, item.id, item.arguments);
837
+ ? yield* ToolStream.finish(state.id, tools, id)
838
+ : yield* ToolStream.finishWithInput(state.id, tools, id, item.arguments);
840
839
  const events = [];
841
840
  const resultEvents = result.events ?? [];
842
841
  const lifecycle = resultEvents.length ? Lifecycle.stepStart(state.lifecycle, events) : state.lifecycle;
@@ -879,9 +878,10 @@ const onOutputItemDone = Effect.fn("OpenResponses.onOutputItemDone")(function* (
879
878
  const onResponseFinish = Effect.fn("OpenResponses.onResponseFinish")(function* (state, event) {
880
879
  const reconciled = event.type === "response.completed"
881
880
  ? yield* Effect.reduce(event.response?.output ?? [], () => [state, NO_EVENTS], ([current, events], item) => {
882
- if (!item.id ||
883
- ((item.type !== "function_call" || !current.tools[item.id]) &&
884
- (item.type !== "reasoning" || !current.reasoningItems[item.id])))
881
+ const id = item.id ?? (item.type === "function_call" ? item.call_id : undefined);
882
+ if (!id ||
883
+ ((item.type !== "function_call" || !current.tools[id]) &&
884
+ (item.type !== "reasoning" || !current.reasoningItems[id])))
885
885
  return Effect.succeed([current, events]);
886
886
  return onOutputItemDone(current, { type: "response.output_item.done", item }).pipe(Effect.map(([next, emitted]) => [next, [...events, ...emitted]]));
887
887
  })
@@ -991,8 +991,9 @@ export const step = (state, input) => {
991
991
  if (event.type === "response.output_item.added") {
992
992
  if (event.item?.type === "message" && !event.item.id)
993
993
  return ProviderShared.eventError(state.id, `${event.type} message is missing id`);
994
- return Effect.succeed(onOutputItemAdded(event.output_index !== undefined && event.item?.id
995
- ? { ...state, outputItems: { ...state.outputItems, [event.output_index]: event.item.id } }
994
+ const id = event.item?.id ?? (event.item?.type === "function_call" ? event.item.call_id : undefined);
995
+ return Effect.succeed(onOutputItemAdded(event.output_index !== undefined && id
996
+ ? { ...state, outputItems: { ...state.outputItems, [event.output_index]: id } }
996
997
  : state, event));
997
998
  }
998
999
  if (event.type === "response.function_call_arguments.delta" || event.type === "response.function_call_arguments.done")
@@ -241,7 +241,7 @@ const lowerUserMessage = Effect.fn("OpenAIChat.lowerUserMessage")(function* (mes
241
241
  };
242
242
  return { role: "user", content };
243
243
  });
244
- const lowerAssistantMessage = Effect.fn("OpenAIChat.lowerAssistantMessage")(function* (message, configuredField, options = {}) {
244
+ const lowerAssistantMessage = Effect.fn("OpenAIChat.lowerAssistantMessage")(function* (message, configuredField, requireReasoning, options) {
245
245
  const content = [];
246
246
  const reasoning = [];
247
247
  const toolCalls = [];
@@ -267,22 +267,22 @@ const lowerAssistantMessage = Effect.fn("OpenAIChat.lowerAssistantMessage")(func
267
267
  const nativeReasoning = openAICompatibleReasoningContent(message.native?.openaiCompatible);
268
268
  const fullyStructured = reasoning.every((part) => Array.isArray(part.providerMetadata?.openai?.reasoningDetails));
269
269
  const field = (() => {
270
- if (configuredField !== undefined)
270
+ if (configuredField !== undefined && (requireReasoning || reasoning.length > 0 || nativeReasoning !== undefined))
271
271
  return configuredField;
272
272
  if (reasoning.length === 0)
273
- return undefined;
273
+ return requireReasoning ? "reasoning_content" : undefined;
274
274
  if (observedField !== undefined)
275
275
  return observedField;
276
276
  if (nativeReasoning !== undefined)
277
277
  return "reasoning_content";
278
- if (!fullyStructured)
278
+ if (!fullyStructured || requireReasoning)
279
279
  return "reasoning_content";
280
280
  })();
281
281
  const reasoningText = (() => {
282
282
  if (configuredField !== undefined)
283
- return reasoning.length === 0 ? (nativeReasoning ?? "") : text;
283
+ return reasoning.length === 0 ? (nativeReasoning ?? (requireReasoning ? "" : undefined)) : text;
284
284
  if (reasoning.length === 0)
285
- return nativeReasoning;
285
+ return nativeReasoning ?? (requireReasoning ? "" : undefined);
286
286
  return text;
287
287
  })();
288
288
  const cached = message.content.findLast((part) => "cache" in part && part.cache !== undefined);
@@ -326,11 +326,11 @@ const lowerToolMessages = Effect.fn("OpenAIChat.lowerToolMessages")(function* (m
326
326
  }
327
327
  return { messages, images };
328
328
  });
329
- const lowerMessage = Effect.fn("OpenAIChat.lowerMessage")(function* (message, reasoningField, options = {}) {
329
+ const lowerMessage = Effect.fn("OpenAIChat.lowerMessage")(function* (message, reasoningField, requireReasoning, options) {
330
330
  if (message.role === "user")
331
331
  return [yield* lowerUserMessage(message, options)];
332
332
  if (message.role === "assistant")
333
- return [yield* lowerAssistantMessage(message, reasoningField, options)];
333
+ return [yield* lowerAssistantMessage(message, reasoningField, requireReasoning, options)];
334
334
  return (yield* lowerToolMessages(message, options)).messages;
335
335
  });
336
336
  const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request, options) {
@@ -350,6 +350,12 @@ const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request,
350
350
  : [{ role: "system", content: ProviderShared.joinText(request.system) }];
351
351
  const messages = [...system];
352
352
  const modelID = request.model.id.toLowerCase();
353
+ const requireReasoning = request.model.compatibility?.requireReasoning ??
354
+ (request.model.compatibility?.reasoningField !== undefined ||
355
+ request.model.provider === "deepseek" ||
356
+ request.model.route.endpoint.baseURL?.toLowerCase().includes("deepseek.com") ||
357
+ modelID.includes("deepseek"));
358
+ const reasoningField = request.model.compatibility?.reasoningField;
353
359
  const mistral = ["mistral", "devstral", "codestral", "pixtral", "mixtral"].some((family) => modelID.includes(family));
354
360
  const lowering = {
355
361
  ...options,
@@ -427,7 +433,7 @@ const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request,
427
433
  continue;
428
434
  }
429
435
  flushImages();
430
- messages.push(...(yield* lowerMessage(message, request.model.compatibility?.reasoningField, lowering)));
436
+ messages.push(...(yield* lowerMessage(message, reasoningField, requireReasoning, lowering)));
431
437
  }
432
438
  flushImages();
433
439
  return messages;
@@ -72,6 +72,8 @@ export type LanguageModelMaxTokensFieldCompatibility = Schema.Schema.Type<typeof
72
72
  declare const LanguageModelCompatibility_base: Schema.Class<LanguageModelCompatibility, Schema.Struct<{
73
73
  readonly toolSchema: Schema.optional<Schema.Literals<readonly ["gemini", "moonshot"]>>;
74
74
  readonly reasoningField: Schema.optional<Schema.String>;
75
+ /** Require every assistant message to include its reasoning field, even when empty. */
76
+ readonly requireReasoning: Schema.optional<Schema.Boolean>;
75
77
  readonly maxTokensField: Schema.optional<Schema.Literals<readonly ["max_completion_tokens", "max_tokens"]>>;
76
78
  readonly requireFinishReason: Schema.optional<Schema.Boolean>;
77
79
  readonly requireAssistantAfterTool: Schema.optional<Schema.Boolean>;
@@ -99,6 +99,8 @@ export const LanguageModelMaxTokensFieldCompatibility = Schema.Literals(["max_co
99
99
  export class LanguageModelCompatibility extends Schema.Class("LLM.LanguageModelCompatibility")({
100
100
  toolSchema: Schema.optional(LanguageModelToolSchemaCompatibility),
101
101
  reasoningField: Schema.optional(Schema.String),
102
+ /** Require every assistant message to include its reasoning field, even when empty. */
103
+ requireReasoning: Schema.optional(Schema.Boolean),
102
104
  maxTokensField: Schema.optional(LanguageModelMaxTokensFieldCompatibility),
103
105
  requireFinishReason: Schema.optional(Schema.Boolean),
104
106
  requireAssistantAfterTool: Schema.optional(Schema.Boolean),
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-18234",
3
+ "version": "0.0.0-dev-18237",
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-18234",
33
+ "@opencode-ai/http-recorder": "0.0.0-dev-18237",
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-18234",
42
+ "@opencode-ai/schema": "0.0.0-dev-18237",
43
43
  "aws4fetch": "1.0.20",
44
44
  "effect": "4.0.0-rc.111",
45
45
  "google-auth-library": "10.5.0"