@opencode-ai/ai 0.0.0-beta-18148 → 0.0.0-beta-18219

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.
@@ -5,7 +5,7 @@ import { Auth } from "../route/auth.js";
5
5
  import { Endpoint } from "../route/endpoint.js";
6
6
  import { HttpTransport } from "../route/transport/index.js";
7
7
  import { Protocol } from "../route/protocol.js";
8
- import { AIError, LLMEvent, Usage, } from "../schema/index.js";
8
+ import { AIError, InvalidProviderOutputReason, LLMEvent, ProviderInternalReason, UnknownProviderReason, Usage, } from "../schema/index.js";
9
9
  import { classifyProviderFailure } from "../provider-error.js";
10
10
  import { isRecord, JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js";
11
11
  import { OpenAIOptions } from "./utils/openai-options.js";
@@ -167,15 +167,15 @@ const OpenAIChatChoice = Schema.StructWithRest(Schema.Struct({
167
167
  // Moonshot streams usage on `choice.usage` instead of top-level `usage`.
168
168
  usage: optionalNull(OpenAIChatUsage),
169
169
  }), [Schema.Record(Schema.String, Schema.Unknown)]);
170
- const OpenAIChatError = Schema.Struct({
170
+ const OpenAIChatError = Schema.StructWithRest(Schema.Struct({
171
171
  code: optionalNull(Schema.Union([Schema.String, Schema.Number])),
172
172
  message: Schema.String,
173
- });
174
- export const OpenAIChatEvent = Schema.Struct({
173
+ }), [Schema.Record(Schema.String, Schema.Unknown)]);
174
+ export const OpenAIChatEvent = Schema.StructWithRest(Schema.Struct({
175
175
  choices: optionalNull(Schema.Array(OpenAIChatChoice)),
176
176
  usage: optionalNull(OpenAIChatUsage),
177
177
  error: optionalNull(OpenAIChatError),
178
- });
178
+ }), [Schema.Record(Schema.String, Schema.Unknown)]);
179
179
  const lowerTool = (tool, inputSchema, options, supportsStrictMode) => ({
180
180
  type: "function",
181
181
  function: {
@@ -349,13 +349,22 @@ const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request,
349
349
  ]
350
350
  : [{ role: "system", content: ProviderShared.joinText(request.system) }];
351
351
  const messages = [...system];
352
+ const requireAssistantAfterTool = request.model.compatibility?.requireAssistantAfterTool ??
353
+ ["mistral", "devstral", "codestral", "pixtral", "mixtral"].some((family) => request.model.id.toLowerCase().includes(family));
354
+ const bridgeTools = () => {
355
+ if (requireAssistantAfterTool && messages.at(-1)?.role === "tool")
356
+ messages.push({ role: "assistant", content: "Done." });
357
+ };
352
358
  const pendingImages = [];
353
359
  const flushImages = () => {
354
360
  if (pendingImages.length === 0)
355
361
  return;
362
+ bridgeTools();
356
363
  messages.push({ role: "user", content: pendingImages.splice(0) });
357
364
  };
358
365
  for (const message of request.messages) {
366
+ if (message.role === "user")
367
+ bridgeTools();
359
368
  if (message.role === "system") {
360
369
  const part = yield* ProviderShared.wrappedSystemUpdate("OpenAI Chat", message);
361
370
  if (pendingImages.length > 0) {
@@ -396,6 +405,8 @@ const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request,
396
405
  : { role: "user", content: part.text });
397
406
  continue;
398
407
  }
408
+ if (message.role === "assistant" && message.content.every((part) => part.type === "text" && part.text.trim() === ""))
409
+ continue;
399
410
  if (message.role === "tool") {
400
411
  const lowered = yield* lowerToolMessages(message, options);
401
412
  messages.push(...lowered.messages);
@@ -517,7 +528,7 @@ const detectZaiToolStream = (provider, baseURL, modelID) => {
517
528
  };
518
529
  const lowerOptions = (request, supportsStore) => {
519
530
  const options = OpenAIOptions.resolve(request);
520
- const cacheKey = ProviderShared.clampPromptCacheKey(request.promptCacheKey);
531
+ const cacheKey = ProviderShared.promptCacheKey(request);
521
532
  return {
522
533
  ...(supportsStore && options.store !== undefined ? { store: options.store } : {}),
523
534
  // For providers that support `store`, ensure stateless `store:false` is sent
@@ -577,19 +588,32 @@ export const fromRequest = Effect.fn("OpenAIChat.fromRequest")(function* (reques
577
588
  // Streaming parsers are small state machines: every event returns a new state
578
589
  // plus the common `LLMEvent`s produced by that event. Tool calls are accumulated
579
590
  // because OpenAI streams JSON arguments across multiple deltas.
580
- const mapFinishReason = (reason) => {
581
- if (reason === "stop")
582
- return "stop";
583
- if (reason === "length")
584
- return "length";
585
- if (reason === "content_filter")
586
- return "content-filter";
587
- if (reason === "function_call" || reason === "tool_calls")
588
- return "tool-calls";
589
- if (reason === "error")
590
- return "error";
591
- return "unknown";
592
- };
591
+ const finishReasonError = (event, reason) => new AIError({
592
+ module: ADAPTER,
593
+ method: "stream",
594
+ body: ProviderShared.encodeJson(event),
595
+ reason,
596
+ });
597
+ const mapFinishReason = Effect.fn("OpenAIChat.mapFinishReason")(function* (event, reason) {
598
+ switch (reason) {
599
+ case "error":
600
+ return yield* finishReasonError(event, new UnknownProviderReason({ message: "Provider reported an error (finish_reason: error)" }));
601
+ case "network_error":
602
+ return yield* finishReasonError(event, new ProviderInternalReason({ message: "Provider reported a network error (finish_reason: network_error)" }));
603
+ case "stop":
604
+ case "end":
605
+ return "stop";
606
+ case "length":
607
+ return "length";
608
+ case "content_filter":
609
+ return "content-filter";
610
+ case "function_call":
611
+ case "tool_calls":
612
+ return "tool-calls";
613
+ default:
614
+ return "unknown";
615
+ }
616
+ });
593
617
  // OpenAI Chat reports `prompt_tokens` (inclusive total) with a
594
618
  // cached-read and cache-write subsets, and `completion_tokens` (inclusive
595
619
  // total) with a `reasoning_tokens` subset. We pass the inclusive totals
@@ -686,16 +710,20 @@ const reasoningMetadata = (field, details) => ({
686
710
  },
687
711
  });
688
712
  const step = (state, event) => Effect.gen(function* () {
689
- if (event.error)
713
+ if (event.error) {
714
+ const body = ProviderShared.encodeJson(event);
690
715
  return yield* new AIError({
691
716
  module: ADAPTER,
692
717
  method: "stream",
718
+ body,
693
719
  reason: classifyProviderFailure({
694
720
  message: event.error.message,
695
721
  code: event.error.code === undefined || event.error.code === null ? undefined : String(event.error.code),
696
722
  status: typeof event.error.code === "number" ? event.error.code : undefined,
723
+ rawBody: body,
697
724
  }),
698
725
  });
726
+ }
699
727
  const events = [];
700
728
  const choice = event.choices?.[0];
701
729
  // Moonshot (and a few other OpenAI-compatible providers) attach usage to
@@ -703,8 +731,11 @@ const step = (state, event) => Effect.gen(function* () {
703
731
  const choiceUsage = choice?.usage;
704
732
  const usage = mapUsage(event.usage) ?? (choiceUsage ? mapUsage(choiceUsage) : undefined) ?? state.usage;
705
733
  const rawFinishReason = choice?.finish_reason;
706
- const finishReason = rawFinishReason !== undefined && rawFinishReason !== null
707
- ? { normalized: mapFinishReason(rawFinishReason), raw: choice?.native_finish_reason ?? rawFinishReason }
734
+ const finishReason = rawFinishReason
735
+ ? {
736
+ normalized: yield* mapFinishReason(event, rawFinishReason),
737
+ raw: choice?.native_finish_reason ?? rawFinishReason,
738
+ }
708
739
  : state.finishReason;
709
740
  const delta = choice?.delta;
710
741
  const toolDeltas = delta?.tool_calls ?? [];
@@ -721,7 +752,7 @@ const step = (state, event) => Effect.gen(function* () {
721
752
  toolDeltas.some((tool) => Boolean(tool.id) || Boolean(tool.function?.name) || Boolean(tool.function?.arguments));
722
753
  if (state.finishReason !== undefined) {
723
754
  if (hasLateContent)
724
- return yield* ProviderShared.eventError(ADAPTER, "OpenAI Chat received content after the finish reason");
755
+ return yield* ProviderShared.eventError(ADAPTER, "OpenAI Chat received content after the finish reason", ProviderShared.encodeJson(event));
725
756
  return [{ ...state, usage }, events];
726
757
  }
727
758
  const reasoningField = state.reasoningField ?? reasoning?.field;
@@ -773,14 +804,14 @@ const step = (state, event) => Effect.gen(function* () {
773
804
  }
774
805
  const result = ToolStream.appendOrStart(ADAPTER, tools, index, { id: id || undefined, name: name || undefined, text }, "OpenAI Chat tool call delta is missing id or name");
775
806
  if (ToolStream.isError(result))
776
- return yield* result;
807
+ return yield* ProviderShared.eventError(ADAPTER, result.reason.message, ProviderShared.encodeJson(event));
777
808
  tools = result.tools;
778
809
  if (result.events.length)
779
810
  lifecycle = Lifecycle.stepStart(lifecycle, events);
780
811
  events.push(...result.events);
781
812
  }
782
813
  if (finishReason !== undefined && state.finishReason === undefined && Object.keys(pendingTools).length > 0)
783
- return yield* ProviderShared.eventError(ADAPTER, "OpenAI Chat tool call delta is missing id or name");
814
+ return yield* ProviderShared.eventError(ADAPTER, "OpenAI Chat tool call delta is missing id or name", ProviderShared.encodeJson(event));
784
815
  // Finalize accumulated tool inputs eagerly when finish_reason arrives so
785
816
  // valid calls and malformed local calls settle independently.
786
817
  const finished = finishReason !== undefined && state.finishReason === undefined && Object.keys(tools).length > 0
@@ -800,14 +831,25 @@ const step = (state, event) => Effect.gen(function* () {
800
831
  reasoningEmitted,
801
832
  latestToolIndex,
802
833
  nextToolIndex,
834
+ requireFinishReason: state.requireFinishReason,
803
835
  },
804
836
  events,
805
837
  ];
806
838
  });
807
- const finishEvents = (state) => {
839
+ const finishEvents = Effect.fn("OpenAIChat.finishEvents")(function* (state) {
840
+ if (state.finishReason === undefined && state.requireFinishReason)
841
+ return yield* new AIError({
842
+ module: ADAPTER,
843
+ method: "stream",
844
+ reason: new InvalidProviderOutputReason({
845
+ classification: "incomplete-stream",
846
+ message: "OpenAI Chat stream ended without finish_reason",
847
+ route: ADAPTER,
848
+ }),
849
+ });
808
850
  const events = [];
809
851
  const toolCallEvents = state.finishReason === undefined && Object.keys(state.tools).length > 0
810
- ? Effect.runSync(ToolStream.finishAll(ADAPTER, state.tools)).events
852
+ ? (yield* ToolStream.finishAll(ADAPTER, state.tools)).events
811
853
  : state.toolCallEvents;
812
854
  const hasToolCalls = toolCallEvents.length > 0;
813
855
  const reason = state.finishReason
@@ -815,7 +857,7 @@ const finishEvents = (state) => {
815
857
  ...state.finishReason,
816
858
  normalized: state.finishReason.normalized === "stop" && hasToolCalls ? "tool-calls" : state.finishReason.normalized,
817
859
  }
818
- : { normalized: hasToolCalls ? "tool-calls" : "unknown" };
860
+ : { normalized: hasToolCalls ? "tool-calls" : "stop" };
819
861
  const metadata = reasoningMetadata(state.reasoningField, state.reasoningDetailsObserved ? state.reasoningDetails : undefined);
820
862
  const started = state.reasoningDetailsObserved && !state.reasoningEmitted
821
863
  ? Lifecycle.reasoningStart(state.lifecycle, events, "reasoning-0", reasoningMetadata(state.reasoningField))
@@ -825,7 +867,7 @@ const finishEvents = (state) => {
825
867
  events.push(...toolCallEvents);
826
868
  Lifecycle.finish(lifecycle, events, { reason, usage: state.usage });
827
869
  return events;
828
- };
870
+ });
829
871
  // =============================================================================
830
872
  // Protocol And OpenAI Route
831
873
  // =============================================================================
@@ -853,6 +895,7 @@ export const protocol = Protocol.make({
853
895
  reasoningDetailsObserved: false,
854
896
  reasoningEmitted: false,
855
897
  nextToolIndex: 0,
898
+ requireFinishReason: request.model.compatibility?.requireFinishReason ?? true,
856
899
  }),
857
900
  step,
858
901
  onHalt: finishEvents,
@@ -13,5 +13,6 @@ export const route = Route.make({
13
13
  protocol: OpenResponses.protocol,
14
14
  endpoint: Endpoint.path(OpenResponses.PATH),
15
15
  transport: OpenResponses.httpTransport,
16
+ defaults: { providerOptions: { store: false, include: ["reasoning.encrypted_content"] } },
16
17
  });
17
18
  export * as OpenAICompatibleResponses from "./openai-compatible-responses.js";
@@ -286,6 +286,15 @@ export declare const protocol: Protocol<{
286
286
  readonly response?: {
287
287
  readonly [x: string]: unknown;
288
288
  readonly id?: string | undefined;
289
+ readonly output?: readonly {
290
+ readonly [x: string]: unknown;
291
+ readonly type: string;
292
+ readonly id?: string | undefined;
293
+ readonly name?: string | undefined;
294
+ readonly arguments?: string | undefined;
295
+ readonly encrypted_content?: string | null | undefined;
296
+ readonly call_id?: string | undefined;
297
+ }[] | undefined;
289
298
  readonly error?: {
290
299
  readonly type?: string | null | undefined;
291
300
  readonly code?: string | null | undefined;
@@ -309,9 +318,11 @@ export declare const protocol: Protocol<{
309
318
  readonly reason?: string | undefined;
310
319
  } | null | undefined;
311
320
  } | undefined;
321
+ readonly arguments?: string | undefined;
312
322
  readonly param?: string | null | undefined;
313
323
  readonly status_code?: unknown;
314
324
  readonly item_id?: string | undefined;
325
+ readonly output_index?: number | undefined;
315
326
  readonly summary_index?: number | undefined;
316
327
  }, OpenResponses.ParserState>;
317
328
  export declare const httpTransport: HttpTransport.HttpJsonTransport<{
@@ -139,7 +139,7 @@ const HOSTED_TOOLS = {
139
139
  const step = (state, event) => {
140
140
  if (event.type === "response.reasoning_text.delta")
141
141
  return event.item_id
142
- ? Effect.succeed(OpenResponses.onReasoningDelta(state, event, event.item_id))
142
+ ? Effect.succeed(OpenResponses.onReasoningDelta(state, event, OpenResponses.outputItemID(state, event) ?? event.item_id))
143
143
  : ProviderShared.eventError(ADAPTER, `${event.type} is missing item_id`);
144
144
  if (event.type === "response.output_item.done" && event.item && ResponsesHostedTools.isItem(event.item, HOSTED_TOOLS))
145
145
  return ResponsesHostedTools.onDone(state, event.item, HOSTED_TOOLS);
@@ -176,6 +176,6 @@ export const route = Route.make({
176
176
  endpoint,
177
177
  auth,
178
178
  transport,
179
- defaults: { providerOptions: { store: false } },
179
+ defaults: { providerOptions: { store: false, include: ["reasoning.encrypted_content"] } },
180
180
  });
181
181
  export * as OpenAIResponses from "./openai-responses.js";
@@ -11,7 +11,7 @@ export declare const JsonObject: Schema.$Record<Schema.String, Schema.Unknown>;
11
11
  export declare const optionalArray: <const S extends Schema.Top>(schema: S) => Schema.optional<Schema.$Array<S>>;
12
12
  export declare const optionalNull: <const S extends Schema.Top>(schema: S) => Schema.optional<Schema.NullOr<S>>;
13
13
  export declare const OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH = 64;
14
- export declare const clampPromptCacheKey: (key: string | undefined) => string | undefined;
14
+ export declare const promptCacheKey: (request: LLMRequest) => string | undefined;
15
15
  /**
16
16
  * Streaming tool-call accumulator. Adapters that build a tool call across
17
17
  * multiple `tool-input-delta` chunks store the partial JSON input string here
@@ -16,12 +16,12 @@ export const optionalNull = (schema) => Schema.optional(Schema.NullOr(schema));
16
16
  export const OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH = 64;
17
17
  // OpenAI limits `prompt_cache_key` to 64 chars; DeepSeek and Zai inherit the same
18
18
  // limit via their OpenAI-compatible APIs. Clamp with unicode-aware slicing.
19
- export const clampPromptCacheKey = (key) => {
20
- if (key === undefined)
19
+ export const promptCacheKey = (request) => {
20
+ if (request.cache === "none" || request.promptCacheKey === undefined)
21
21
  return undefined;
22
- const chars = Array.from(key);
22
+ const chars = Array.from(request.promptCacheKey);
23
23
  if (chars.length <= OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH)
24
- return key;
24
+ return request.promptCacheKey;
25
25
  return chars.slice(0, OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH).join("");
26
26
  };
27
27
  /**
@@ -43,8 +43,58 @@ export function parseJSON(jsonString, allowPartial = Allow.ALL) {
43
43
  return decodeJson(input);
44
44
  }
45
45
  catch { }
46
- return _parseJSON(input, allowPartial);
46
+ const repaired = repairJSON(input);
47
+ if (repaired !== input) {
48
+ try {
49
+ return decodeJson(repaired);
50
+ }
51
+ catch { }
52
+ }
53
+ try {
54
+ return _parseJSON(input, allowPartial);
55
+ }
56
+ catch (error) {
57
+ if (repaired !== input)
58
+ return _parseJSON(repaired, allowPartial);
59
+ throw error;
60
+ }
47
61
  }
62
+ const repairJSON = (input) => {
63
+ let repaired = "";
64
+ let quoted = false;
65
+ for (let index = 0; index < input.length; index++) {
66
+ const character = input[index];
67
+ if (!quoted) {
68
+ repaired += character;
69
+ if (character === '"')
70
+ quoted = true;
71
+ continue;
72
+ }
73
+ if (character === '"') {
74
+ repaired += character;
75
+ quoted = false;
76
+ continue;
77
+ }
78
+ if (character === "\\") {
79
+ const next = input[index + 1];
80
+ if (next === "u" && /^[0-9a-fA-F]{4}$/.test(input.slice(index + 2, index + 6))) {
81
+ repaired += input.slice(index, index + 6);
82
+ index += 5;
83
+ continue;
84
+ }
85
+ if (next !== undefined && '"\\/bfnrtu'.includes(next)) {
86
+ repaired += `\\${next}`;
87
+ index++;
88
+ continue;
89
+ }
90
+ repaired += "\\\\";
91
+ continue;
92
+ }
93
+ const code = character.charCodeAt(0);
94
+ repaired += code <= 0x1f ? `\\u${code.toString(16).padStart(4, "0")}` : character;
95
+ }
96
+ return repaired;
97
+ };
48
98
  const _parseJSON = (jsonString, allow) => {
49
99
  const length = jsonString.length;
50
100
  let index = 0;
@@ -138,7 +188,12 @@ const _parseJSON = (jsonString, allow) => {
138
188
  skipBlank();
139
189
  index++;
140
190
  try {
141
- object[key] = parseAny();
191
+ Object.defineProperty(object, key, {
192
+ value: parseAny(),
193
+ enumerable: true,
194
+ configurable: true,
195
+ writable: true,
196
+ });
142
197
  }
143
198
  catch (error) {
144
199
  if (Allow.OBJ & allow)
@@ -61,7 +61,7 @@ export declare const appendOrStart: <K extends StreamKey>(route: string, tools:
61
61
  export declare const appendExisting: <K extends StreamKey>(route: string, tools: State<K>, key: K, text: string, missingToolMessage: string) => AppendOutcome<K> | AIError;
62
62
  /**
63
63
  * Finalize one pending tool call: parse the accumulated raw JSON, remove it
64
- * from state, and return either a call or a non-executable local input error.
64
+ * from state, and recover incomplete local arguments when needed.
65
65
  * Missing keys are a no-op because some providers emit stop events for
66
66
  * non-tool content blocks.
67
67
  */
@@ -19,34 +19,28 @@ const inputStart = (tool) => LLMEvent.toolInputStart({
19
19
  providerExecuted: tool.providerExecuted ? true : undefined,
20
20
  providerMetadata: tool.providerMetadata,
21
21
  });
22
- const inputDelta = (tool, text) => {
23
- const input = parsePartialInput(tool.input);
24
- return LLMEvent.toolInputDelta({
25
- id: tool.id,
26
- name: tool.name,
27
- text,
28
- ...(Option.isSome(input) ? { input: input.value } : {}),
29
- });
30
- };
22
+ const inputDelta = (tool, text) => LLMEvent.toolInputDelta({
23
+ id: tool.id,
24
+ name: tool.name,
25
+ text,
26
+ input: Option.getOrElse(parsePartialInput(tool.input), () => ({})),
27
+ });
31
28
  const toolCall = (route, tool, inputOverride) => {
32
29
  const raw = inputOverride ?? tool.input;
33
- return parseToolInput(route, tool.name, raw).pipe(Effect.map((input) => LLMEvent.toolCall({
30
+ return parseToolInput(route, tool.name, raw).pipe(Effect.catch((error) => tool.providerExecuted
31
+ ? Effect.fail(error)
32
+ : Effect.succeed(Option.getOrElse(Option.map(parsePartialInput(raw), (input) => input ?? {}), () => ({})))), Effect.map((input) => LLMEvent.toolCall({
34
33
  id: tool.id,
35
34
  name: tool.name,
36
35
  input,
37
36
  providerExecuted: tool.providerExecuted ? true : undefined,
38
37
  providerMetadata: tool.providerMetadata,
39
- })), Effect.catch((error) => tool.providerExecuted
40
- ? Effect.fail(error)
41
- : Effect.succeed(LLMEvent.toolInputError({
42
- id: tool.id,
43
- name: tool.name,
44
- raw,
45
- }))));
38
+ })));
46
39
  };
47
- const finishEvents = (tool, event) => event.type === "tool-input-error"
48
- ? [event]
49
- : [LLMEvent.toolInputEnd({ id: tool.id, name: tool.name, providerMetadata: tool.providerMetadata }), event];
40
+ const finishEvents = (tool, event) => [
41
+ LLMEvent.toolInputEnd({ id: tool.id, name: tool.name, providerMetadata: tool.providerMetadata }),
42
+ event,
43
+ ];
50
44
  /** Store the updated tool and produce the optional public delta event. */
51
45
  const appendTool = (tools, key, tool, text) => {
52
46
  const events = [];
@@ -105,7 +99,7 @@ export const appendExisting = (route, tools, key, text, missingToolMessage) => {
105
99
  };
106
100
  /**
107
101
  * Finalize one pending tool call: parse the accumulated raw JSON, remove it
108
- * from state, and return either a call or a non-executable local input error.
102
+ * from state, and recover incomplete local arguments when needed.
109
103
  * Missing keys are a no-op because some providers emit stop events for
110
104
  * non-tool content blocks.
111
105
  */
@@ -143,6 +143,15 @@ export declare const protocol: Protocol<{
143
143
  readonly response?: {
144
144
  readonly [x: string]: unknown;
145
145
  readonly id?: string | undefined;
146
+ readonly output?: readonly {
147
+ readonly [x: string]: unknown;
148
+ readonly type: string;
149
+ readonly id?: string | undefined;
150
+ readonly name?: string | undefined;
151
+ readonly arguments?: string | undefined;
152
+ readonly encrypted_content?: string | null | undefined;
153
+ readonly call_id?: string | undefined;
154
+ }[] | undefined;
146
155
  readonly error?: {
147
156
  readonly type?: string | null | undefined;
148
157
  readonly code?: string | null | undefined;
@@ -166,9 +175,11 @@ export declare const protocol: Protocol<{
166
175
  readonly reason?: string | undefined;
167
176
  } | null | undefined;
168
177
  } | undefined;
178
+ readonly arguments?: string | undefined;
169
179
  readonly param?: string | null | undefined;
170
180
  readonly status_code?: unknown;
171
181
  readonly item_id?: string | undefined;
182
+ readonly output_index?: number | undefined;
172
183
  readonly summary_index?: number | undefined;
173
184
  }, OpenResponses.ParserState>;
174
185
  export * as XAIResponses from "./xai-responses.js";
@@ -283,7 +283,9 @@ export declare const protocol: Protocol<{
283
283
  readonly frequency_penalty?: number | undefined;
284
284
  readonly presence_penalty?: number | undefined;
285
285
  }, string, {
286
+ readonly [x: string]: unknown;
286
287
  readonly error?: {
288
+ readonly [x: string]: unknown;
287
289
  readonly message: string;
288
290
  readonly code?: string | number | null | undefined;
289
291
  } | null | undefined;
@@ -8,7 +8,7 @@ import { ProviderID } from "../schema/index.js";
8
8
  import * as OpenAICompatibleProfiles from "./openai-compatible-profile.js";
9
9
  import * as OpenAIChat from "../protocols/openai-chat.js";
10
10
  import { newBreakpoints, ttlBucket } from "../protocols/utils/cache.js";
11
- import { isRecord, ProviderShared } from "../protocols/shared.js";
11
+ import { isRecord } from "../protocols/shared.js";
12
12
  export const profile = OpenAICompatibleProfiles.profiles.openrouter;
13
13
  export const id = ProviderID.make(profile.provider);
14
14
  const ADAPTER = "openrouter";
@@ -39,12 +39,10 @@ export const protocol = Protocol.make({
39
39
  reasoning_details: reasoningDetails,
40
40
  };
41
41
  });
42
- const cacheKey = ProviderShared.clampPromptCacheKey(request.promptCacheKey);
43
42
  return {
44
43
  ...body,
45
44
  messages,
46
45
  ...bodyOptions(request.providerOptions),
47
- ...(cacheKey ? { prompt_cache_key: cacheKey } : {}),
48
46
  };
49
47
  })),
50
48
  },
@@ -21,7 +21,7 @@ const responsesRoute = Route.make({
21
21
  name: "xAI Responses",
22
22
  rotateAfterMs: RESPONSES_WEBSOCKET_ROTATE_AFTER_MS,
23
23
  }),
24
- defaults: { providerOptions: { store: false } },
24
+ defaults: { providerOptions: { store: false, include: ["reasoning.encrypted_content"] } },
25
25
  });
26
26
  const chatRoute = Route.make({
27
27
  id: "openai-compatible-chat",
@@ -5,6 +5,7 @@ import { RequestExecutor } from "./executor.js";
5
5
  import { Framing } from "./framing.js";
6
6
  import { HttpTransport } from "./transport/index.js";
7
7
  import { applyCachePolicy } from "../cache-policy.js";
8
+ import { sanitizeSurrogates } from "../utils/sanitize.js";
8
9
  import * as ProviderShared from "../protocols/shared.js";
9
10
  import { AIError, GenerationOptions, HttpOptions, LLMRequest, LLMResponse, LanguageModel, LLMEvent, InvalidProviderOutputReason, ProviderID, mergeGenerationOptions, mergeHttpOptions, mergeProviderOptions, } from "../schema/index.js";
10
11
  const makeRouteLanguageModel = (route, mapped) => {
@@ -123,7 +124,17 @@ function makeFromTransport(input) {
123
124
  const route = `${request.model.provider}/${request.model.route.id}`;
124
125
  return Stream.unwrap(routeInput.transport.execute(prepared, request, runtime, options).pipe(Effect.map((execution) => {
125
126
  const events = execution.frames.pipe(Stream.mapEffect(decodeEvent(route)), protocol.stream.terminal ? Stream.takeUntil(protocol.stream.terminal) : (stream) => stream);
126
- const stream = events.pipe(Stream.mapAccumEffect(() => protocol.stream.initial(request), protocol.stream.step, protocol.stream.onHalt ? { onHalt: protocol.stream.onHalt } : undefined), Stream.catchCause((cause) => Stream.fail(streamError(route, `Failed to read ${route} stream`, cause))), requireTerminalEvent(route));
127
+ const stream = Stream.suspend(() => {
128
+ let state = protocol.stream.initial(request);
129
+ const parsed = events.pipe(Stream.mapEffect((event) => protocol.stream.step(state, event).pipe(Effect.map(([next, output]) => {
130
+ state = next;
131
+ return output;
132
+ }))), Stream.flatMap(Stream.fromIterable));
133
+ const onHalt = protocol.stream.onHalt;
134
+ return onHalt
135
+ ? parsed.pipe(Stream.concat(Stream.suspend(() => Stream.unwrap(onHalt(state).pipe(Effect.map(Stream.fromIterable))))))
136
+ : parsed;
137
+ }).pipe(Stream.catchCause((cause) => Stream.fail(streamError(route, `Failed to read ${route} stream`, cause))), requireTerminalEvent(route));
127
138
  return execution.complete ? stream.pipe(Stream.onEnd(execution.complete)) : stream;
128
139
  })));
129
140
  },
@@ -149,7 +160,8 @@ export function make(input) {
149
160
  });
150
161
  }
151
162
  const compile = Effect.fn("LLM.compile")(function* (request, options) {
152
- const resolved = applyCachePolicy(resolveRequestOptions(request));
163
+ const original = applyCachePolicy(resolveRequestOptions(request));
164
+ const resolved = LLMRequest.update(original, sanitizeSurrogates({ ...LLMRequest.input(original), model: undefined }));
153
165
  const route = resolved.model.route;
154
166
  const body = yield* route.body
155
167
  .from(resolved)
@@ -56,8 +56,8 @@ export interface ProtocolStream<Frame, Event, State> {
56
56
  readonly step: (state: State, event: Event) => Effect.Effect<readonly [State, ReadonlyArray<LLMEvent>], AIError>;
57
57
  /** Optional request-completion signal for transports that do not end naturally. */
58
58
  readonly terminal?: (event: Event) => boolean;
59
- /** Optional flush emitted when the framed stream ends. */
60
- readonly onHalt?: (state: State) => ReadonlyArray<LLMEvent>;
59
+ /** Optional effectful flush emitted when the framed stream ends. */
60
+ readonly onHalt?: (state: State) => Effect.Effect<ReadonlyArray<LLMEvent>, AIError>;
61
61
  }
62
62
  /**
63
63
  * Construct a `Protocol` from its body and stream pieces:
@@ -74,6 +74,7 @@ declare const LanguageModelCompatibility_base: Schema.Class<LanguageModelCompati
74
74
  readonly reasoningField: Schema.optional<Schema.String>;
75
75
  readonly maxTokensField: Schema.optional<Schema.Literals<readonly ["max_completion_tokens", "max_tokens"]>>;
76
76
  readonly requireFinishReason: Schema.optional<Schema.Boolean>;
77
+ readonly requireAssistantAfterTool: Schema.optional<Schema.Boolean>;
77
78
  readonly supportsStore: Schema.optional<Schema.Boolean>;
78
79
  readonly supportsUsageInStreaming: Schema.optional<Schema.Boolean>;
79
80
  readonly supportsStrictMode: Schema.optional<Schema.Boolean>;
@@ -101,6 +101,7 @@ export class LanguageModelCompatibility extends Schema.Class("LLM.LanguageModelC
101
101
  reasoningField: Schema.optional(Schema.String),
102
102
  maxTokensField: Schema.optional(LanguageModelMaxTokensFieldCompatibility),
103
103
  requireFinishReason: Schema.optional(Schema.Boolean),
104
+ requireAssistantAfterTool: Schema.optional(Schema.Boolean),
104
105
  supportsStore: Schema.optional(Schema.Boolean),
105
106
  supportsUsageInStreaming: Schema.optional(Schema.Boolean),
106
107
  supportsStrictMode: Schema.optional(Schema.Boolean),
@@ -0,0 +1 @@
1
+ export declare const sanitizeSurrogates: <T>(value: T) => T;
@@ -0,0 +1,12 @@
1
+ import { isRecord } from "./record.js";
2
+ export const sanitizeSurrogates = (value) => {
3
+ if (typeof value === "string")
4
+ return value.toWellFormed();
5
+ if (Array.isArray(value))
6
+ return value.map(sanitizeSurrogates);
7
+ if (value instanceof Uint8Array || value instanceof Error)
8
+ return value;
9
+ if (isRecord(value))
10
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key.toWellFormed(), sanitizeSurrogates(entry)]));
11
+ return value;
12
+ };
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-18148",
3
+ "version": "0.0.0-beta-18219",
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-18148",
33
+ "@opencode-ai/http-recorder": "0.0.0-beta-18219",
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-18148",
42
+ "@opencode-ai/schema": "0.0.0-beta-18219",
43
43
  "aws4fetch": "1.0.20",
44
44
  "effect": "4.0.0-rc.111",
45
45
  "google-auth-library": "10.5.0"