@opencode/ai 0.0.0-dev-20138 → 0.0.0-dev-20140

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.
@@ -14,7 +14,6 @@ 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
16
  import { Lifecycle } from "./utils/lifecycle.js";
17
- import { ToolSchemaProjection } from "./utils/tool-schema.js";
18
17
  import { ToolStream } from "./utils/tool-stream.js";
19
18
  const ADAPTER = "anthropic-messages";
20
19
  export const DEFAULT_BASE_URL = "https://api.anthropic.com/v1";
@@ -408,10 +407,10 @@ const redactedDataFromMetadata = (metadata, key) => {
408
407
  return undefined;
409
408
  return typeof provider.redactedData === "string" ? provider.redactedData : undefined;
410
409
  };
411
- const lowerTool = (breakpoints, tool, inputSchema) => ({
410
+ const lowerTool = (breakpoints, tool) => ({
412
411
  name: tool.name,
413
412
  description: tool.description,
414
- input_schema: inputSchema,
413
+ input_schema: tool.inputSchema,
415
414
  cache_control: cacheControl(breakpoints, tool.cache),
416
415
  });
417
416
  const lowerToolChoice = (toolChoice) => ProviderShared.matchToolChoice("Anthropic Messages", toolChoice, {
@@ -910,9 +909,7 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques
910
909
  // over-mark we keep their tool hints and shed the message-tail ones first.
911
910
  const breakpoints = Cache.newBreakpoints(ANTHROPIC_BREAKPOINT_CAP);
912
911
  const flattened = ProviderShared.flattenToolRequest(updates.request);
913
- const tools = flattened.tools.length === 0
914
- ? undefined
915
- : flattened.tools.map((tool) => lowerTool(breakpoints, tool, ToolSchemaProjection.modelCompatibility(tool.inputSchema, request.model)));
912
+ const tools = flattened.tools.length === 0 ? undefined : flattened.tools.map((tool) => lowerTool(breakpoints, tool));
916
913
  // Anthropic rejects tool_choice when tools are absent; "none" is only meaningful with tools present.
917
914
  const toolChoice = tools === undefined || !request.toolChoice ? undefined : yield* lowerToolChoice(request.toolChoice);
918
915
  const systemParts = request.system.filter((part) => part.text.length > 0);
@@ -11,7 +11,6 @@ import { BedrockCache } from "./utils/bedrock-cache.js";
11
11
  import { BedrockMedia } from "./utils/bedrock-media.js";
12
12
  import { Lifecycle } from "./utils/lifecycle.js";
13
13
  import { MistralToolID } from "./utils/mistral-tool-id.js";
14
- import { ToolSchemaProjection } from "./utils/tool-schema.js";
15
14
  import { ToolStream } from "./utils/tool-stream.js";
16
15
  import { concatBytes } from "../utils/bytes.js";
17
16
  const ADAPTER = "bedrock-converse";
@@ -156,17 +155,17 @@ const BedrockEvent = Schema.Struct({
156
155
  // =============================================================================
157
156
  // Request Lowering
158
157
  // =============================================================================
159
- const lowerToolSpec = (tool, inputSchema) => ({
158
+ const lowerToolSpec = (tool) => ({
160
159
  toolSpec: {
161
160
  name: tool.name,
162
161
  ...(tool.description.trim().length > 0 ? { description: tool.description } : {}),
163
- inputSchema: { json: inputSchema },
162
+ inputSchema: { json: tool.inputSchema },
164
163
  },
165
164
  });
166
- const lowerTools = (model, breakpoints, tools) => {
165
+ const lowerTools = (breakpoints, tools) => {
167
166
  const result = [];
168
167
  for (const tool of tools) {
169
- result.push(lowerToolSpec(tool, ToolSchemaProjection.modelCompatibility(tool.inputSchema, model)));
168
+ result.push(lowerToolSpec(tool));
170
169
  const cachePoint = BedrockCache.block(breakpoints, tool.cache);
171
170
  if (cachePoint)
172
171
  result.push(cachePoint);
@@ -371,7 +370,7 @@ const fromRequest = Effect.fn("BedrockConverse.fromRequest")(function* (request)
371
370
  if (flattened.tools.length === 0)
372
371
  return undefined;
373
372
  return {
374
- tools: lowerTools(request.model, breakpoints, flattened.tools),
373
+ tools: lowerTools(breakpoints, flattened.tools),
375
374
  // Converse has no native "none". Keep definitions stable for prompt
376
375
  // caching and omit only the unsupported choice.
377
376
  toolChoice,
@@ -11,7 +11,6 @@ import { Media } from "../media.js";
11
11
  import { JsonObject, knownString, lenient, optionalArray, optionalNull, ProviderShared } from "./shared.js";
12
12
  import { GeminiGenerateContent } from "./utils/gemini-generate-content.js";
13
13
  import { Lifecycle } from "./utils/lifecycle.js";
14
- import { ToolSchemaProjection } from "./utils/tool-schema.js";
15
14
  const ADAPTER = "gemini";
16
15
  // Google documents this sentinel for replaying Gemini 3 function calls after their original signature was lost.
17
16
  const SKIP_THOUGHT_SIGNATURE_VALIDATOR = "skip_thought_signature_validator";
@@ -190,12 +189,11 @@ const GeminiEvent = Schema.Struct({
190
189
  // =============================================================================
191
190
  // Request Lowering
192
191
  // =============================================================================
193
- // Tool schemas go in `parametersJsonSchema`, which accepts standard JSON Schema. Gemini's schema
194
- // rules are this API's default, including for tuned endpoints whose IDs do not name Gemini.
195
- const lowerTool = (tool, model) => ({
192
+ // Tool schemas go in `parametersJsonSchema`, which accepts standard JSON Schema.
193
+ const lowerTool = (tool) => ({
196
194
  name: tool.name,
197
195
  description: tool.description,
198
- parametersJsonSchema: ToolSchemaProjection.modelCompatibility(tool.inputSchema, model, "gemini"),
196
+ parametersJsonSchema: tool.inputSchema,
199
197
  });
200
198
  const lowerToolConfig = (toolChoice) => ProviderShared.matchToolChoice("Gemini", toolChoice, {
201
199
  auto: () => ({ functionCallingConfig: { mode: "AUTO" } }),
@@ -388,7 +386,7 @@ const fromRequest = Effect.fn("Gemini.fromRequest")(function* (request) {
388
386
  tools: hasTools
389
387
  ? [
390
388
  {
391
- functionDeclarations: flattened.tools.map((tool) => lowerTool(tool, request.model)),
389
+ functionDeclarations: flattened.tools.map(lowerTool),
392
390
  },
393
391
  ]
394
392
  : undefined,
@@ -654,6 +652,8 @@ export const protocol = Protocol.make({
654
652
  schema: GeminiBody,
655
653
  from: fromRequest,
656
654
  },
655
+ // Gemini's schema rules are this API's default, including for tuned endpoints whose IDs do not name Gemini.
656
+ sanitizer: "gemini",
657
657
  stream: {
658
658
  event: Protocol.jsonEvent(GeminiEvent),
659
659
  initial: (request) => ({
@@ -5,7 +5,6 @@ import { LLMEvent, LLMRequest, Message, ToolResultPart } from "../schema/index.j
5
5
  import { OpenResponses } from "./open-responses.js";
6
6
  import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js";
7
7
  import { ResponsesHostedTools } from "./utils/responses-hosted-tools.js";
8
- import { ToolSchemaProjection } from "./utils/tool-schema.js";
9
8
  import { detectMediaType } from "../utils/media-type.js";
10
9
  const ADAPTER = "meta-responses";
11
10
  const NAME = "Meta Responses";
@@ -82,7 +81,7 @@ const fromRequest = Effect.fn("MetaResponses.fromRequest")(function* (request) {
82
81
  ? undefined
83
82
  : yield* Effect.forEach(projected.tools, (tool) => Effect.gen(function* () {
84
83
  if (tool.native === undefined)
85
- return yield* OpenResponses.lowerTool(NAME, tool, ToolSchemaProjection.modelCompatibility(tool.inputSchema, request.model));
84
+ return yield* OpenResponses.lowerTool(NAME, tool);
86
85
  return yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(NativeTool))(tool.native.meta);
87
86
  })),
88
87
  tool_choice: OpenResponses.allowedToolChoice(request) ??
@@ -10,7 +10,6 @@ import { classifyProviderFailure } from "../provider-error.js";
10
10
  import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js";
11
11
  import { Lifecycle } from "./utils/lifecycle.js";
12
12
  import { MistralToolID } from "./utils/mistral-tool-id.js";
13
- import { ToolSchemaProjection } from "./utils/tool-schema.js";
14
13
  import { ToolStream } from "./utils/tool-stream.js";
15
14
  const ADAPTER = "mistral-chat";
16
15
  const DONE = "[DONE]";
@@ -273,9 +272,9 @@ const lowerMessages = Effect.fn("MistralChat.lowerMessages")(function* (request)
273
272
  }
274
273
  return messages;
275
274
  });
276
- const lowerTool = (tool, inputSchema) => ({
275
+ const lowerTool = (tool) => ({
277
276
  type: "function",
278
- function: { name: tool.name, description: tool.description, parameters: inputSchema, strict: false },
277
+ function: { name: tool.name, description: tool.description, parameters: tool.inputSchema, strict: false },
279
278
  });
280
279
  export const fromRequest = Effect.fn("MistralChat.fromRequest")(function* (request) {
281
280
  const options = yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(MistralOptions))(request.providerOptions ?? {});
@@ -296,9 +295,7 @@ export const fromRequest = Effect.fn("MistralChat.fromRequest")(function* (reque
296
295
  return {
297
296
  model: request.model.id,
298
297
  messages: yield* lowerMessages(flattened.request),
299
- tools: flattened.tools.length > 0
300
- ? flattened.tools.map((tool) => lowerTool(tool, ToolSchemaProjection.modelCompatibility(tool.inputSchema, request.model)))
301
- : undefined,
298
+ tools: flattened.tools.length > 0 ? flattened.tools.map(lowerTool) : undefined,
302
299
  tool_choice: toolChoice,
303
300
  stream: true,
304
301
  max_tokens: request.generation?.maxTokens,
@@ -716,9 +716,7 @@ interface ReasoningStreamItem {
716
716
  readonly summaryParts: Readonly<Record<number, ReasoningSummaryStatus>>;
717
717
  readonly deltaIndexes: ReadonlySet<number>;
718
718
  }
719
- export declare const lowerTool: (protocolName: string, tool: ToolDefinition, inputSchema: {
720
- readonly [x: string]: unknown;
721
- }) => Effect.Effect<{
719
+ export declare const lowerTool: (protocolName: string, tool: ToolDefinition) => Effect.Effect<{
722
720
  type: "function";
723
721
  name: string;
724
722
  description: string;
@@ -7,7 +7,6 @@ import { classifyProviderFailure } from "../provider-error.js";
7
7
  import { effortUpdate } from "../effort-updates.js";
8
8
  import { OpenResponsesOptions } from "./utils/open-responses-options.js";
9
9
  import { Lifecycle } from "./utils/lifecycle.js";
10
- import { ToolSchemaProjection } from "./utils/tool-schema.js";
11
10
  import { ToolStream } from "./utils/tool-stream.js";
12
11
  const ADAPTER = "open-responses";
13
12
  const NAME = "Open Responses";
@@ -281,14 +280,14 @@ const BASE_ADAPTER = { id: ADAPTER, name: NAME };
281
280
  // =============================================================================
282
281
  // Request Lowering
283
282
  // =============================================================================
284
- export const lowerTool = Effect.fn("OpenResponses.lowerTool")(function* (protocolName, tool, inputSchema) {
283
+ export const lowerTool = Effect.fn("OpenResponses.lowerTool")(function* (protocolName, tool) {
285
284
  if (tool.native !== undefined)
286
285
  return yield* ProviderShared.invalidRequest(`${protocolName} does not support provider-native tool ${tool.name}`);
287
286
  return {
288
287
  type: "function",
289
288
  name: tool.name,
290
289
  description: tool.description,
291
- parameters: inputSchema,
290
+ parameters: tool.inputSchema,
292
291
  // The common tool definition does not currently express Responses strict-schema policy.
293
292
  strict: false,
294
293
  };
@@ -610,7 +609,7 @@ export const fromRequestWithAdapter = Effect.fn("OpenResponses.fromRequestWithAd
610
609
  ? undefined
611
610
  : yield* Effect.forEach(projected.tools, (tool) => tool.native !== undefined && adapter.nativeTool
612
611
  ? adapter.nativeTool(tool.native)
613
- : lowerTool(adapter.name, tool, ToolSchemaProjection.modelCompatibility(tool.inputSchema, request.model))),
612
+ : lowerTool(adapter.name, tool)),
614
613
  tool_choice: allowedToolChoice(request) ??
615
614
  (request.toolChoice ? yield* lowerToolChoice(adapter.name, request.toolChoice) : undefined),
616
615
  };
@@ -11,7 +11,6 @@ import { classifyProviderFailure } from "../provider-error.js";
11
11
  import { isRecord, JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js";
12
12
  import { OpenAIOptions } from "./utils/openai-options.js";
13
13
  import { Lifecycle } from "./utils/lifecycle.js";
14
- import { ToolSchemaProjection } from "./utils/tool-schema.js";
15
14
  import { ToolStream } from "./utils/tool-stream.js";
16
15
  const ADAPTER = "openai-chat";
17
16
  const RESERVED_REASONING_FIELDS = new Set(["role", "content", "refusal", "tool_calls"]);
@@ -209,12 +208,12 @@ export const OpenAIChatEvent = Schema.StructWithRest(Schema.Struct({
209
208
  }), [Schema.Record(Schema.String, Schema.Unknown)]);
210
209
  const DONE = "[DONE]";
211
210
  const OpenAIChatStreamEvent = Schema.Union([Schema.Literal(DONE), Protocol.jsonEvent(OpenAIChatEvent)]);
212
- const lowerTool = (tool, inputSchema, options, supportsStrictMode) => ({
211
+ const lowerTool = (tool, options, supportsStrictMode) => ({
213
212
  type: "function",
214
213
  function: {
215
214
  name: tool.name,
216
215
  description: tool.description,
217
- parameters: inputSchema,
216
+ parameters: tool.inputSchema,
218
217
  ...(supportsStrictMode ? { strict: false } : {}),
219
218
  },
220
219
  cache_control: options.cacheControl?.(tool.cache),
@@ -649,7 +648,7 @@ export const fromRequest = Effect.fn("OpenAIChat.fromRequest")(function* (reques
649
648
  ? hasHistory
650
649
  ? []
651
650
  : undefined
652
- : flattened.tools.map((tool) => lowerTool(tool, ToolSchemaProjection.modelCompatibility(tool.inputSchema, request.model), options, supportsStrictMode)),
651
+ : flattened.tools.map((tool) => lowerTool(tool, options, supportsStrictMode)),
653
652
  tool_choice: hasActiveTools && request.toolChoice ? yield* lowerToolChoice(request.toolChoice) : undefined,
654
653
  stream: true,
655
654
  ...(supportsUsageInStreaming ? { stream_options: { include_usage: true } } : {}),
@@ -5,13 +5,12 @@ import { Auth } from "../route/auth.js";
5
5
  import { Endpoint } from "../route/endpoint.js";
6
6
  import { Protocol } from "../route/protocol.js";
7
7
  import { HttpTransport } from "../route/transport/index.js";
8
- import { LLMRequest, mergeJsonRecords, } from "../schema/index.js";
8
+ import { LLMRequest, mergeJsonRecords } from "../schema/index.js";
9
9
  import { resolveEffortUpdates } from "../effort-updates.js";
10
10
  import { OpenResponses } from "./open-responses.js";
11
11
  import { OpenResponsesOptions } from "./utils/open-responses-options.js";
12
12
  import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js";
13
13
  import { ResponsesHostedTools } from "./utils/responses-hosted-tools.js";
14
- import { ToolSchemaProjection } from "./utils/tool-schema.js";
15
14
  import { OpenResponsesChannel } from "./open-responses-channel.js";
16
15
  import { ResponsesCompaction } from "./utils/responses-compaction.js";
17
16
  import { ResponsesCheckpoint } from "./utils/responses-checkpoint.js";
@@ -132,27 +131,27 @@ const nativeImageTool = (tool) => {
132
131
  const native = nativeImageToolInput(tool);
133
132
  return Schema.is(OpenAIResponsesImageGenerationTool)(native) ? native : undefined;
134
133
  };
135
- const lowerTool = Effect.fn("OpenAIResponses.lowerTool")(function* (tool, inputSchema) {
134
+ const lowerTool = Effect.fn("OpenAIResponses.lowerTool")(function* (tool) {
136
135
  const native = nativeImageToolInput(tool);
137
136
  if (native !== undefined) {
138
137
  if (Schema.is(OpenAIResponsesImageGenerationTool)(native))
139
138
  return native;
140
139
  return yield* ProviderShared.invalidRequest("OpenAI Responses image generation tool options are invalid");
141
140
  }
142
- return yield* OpenResponses.lowerTool(NAME, tool, inputSchema);
141
+ return yield* OpenResponses.lowerTool(NAME, tool);
143
142
  });
144
143
  // Native namespaces hold only function tools, so deeper levels flatten into
145
144
  // the leaf names the same way non-native protocols flatten the whole tree.
146
- const lowerToolEntry = Effect.fn("OpenAIResponses.lowerToolEntry")(function* (tool, model) {
145
+ const lowerToolEntry = Effect.fn("OpenAIResponses.lowerToolEntry")(function* (tool) {
147
146
  if (tool.type === "tool")
148
- return yield* lowerTool(tool, ToolSchemaProjection.modelCompatibility(tool.inputSchema, model));
147
+ return yield* lowerTool(tool);
149
148
  // OpenAI requires a namespace description; fall back to a generic one so a
150
149
  // missing description never blocks the request.
151
150
  return {
152
151
  type: "namespace",
153
152
  name: tool.name,
154
153
  description: tool.description ?? `Tools in the ${tool.name} namespace.`,
155
- tools: yield* Effect.forEach(ProviderShared.flattenTools(tool.tools), (leaf) => OpenResponses.lowerTool(NAME, leaf, ToolSchemaProjection.modelCompatibility(leaf.inputSchema, model))),
154
+ tools: yield* Effect.forEach(ProviderShared.flattenTools(tool.tools), (leaf) => OpenResponses.lowerTool(NAME, leaf)),
156
155
  };
157
156
  });
158
157
  const lowerToolChoice = (toolChoice, tools) => ProviderShared.matchToolChoice(NAME, toolChoice, {
@@ -172,9 +171,7 @@ const fromRequest = Effect.fn("OpenAIResponses.fromRequest")(function* (request)
172
171
  ...(yield* OpenResponses.lowerConversation(updates.request, adapter)),
173
172
  ...OpenResponses.lowerGeneration(request, { ...options, reasoningEffort: updates.effort }),
174
173
  context_management: management?.map((edit) => ({ type: edit.type, compact_threshold: edit.compactThreshold })),
175
- tools: request.tools.length === 0
176
- ? undefined
177
- : yield* Effect.forEach(request.tools, (tool) => lowerToolEntry(tool, request.model)),
174
+ tools: request.tools.length === 0 ? undefined : yield* Effect.forEach(request.tools, lowerToolEntry),
178
175
  tool_choice: request.tools.length === 0
179
176
  ? undefined
180
177
  : (OpenResponses.allowedToolChoice(request) ??
@@ -1,8 +1,8 @@
1
- import type { JsonSchema, LanguageModel, LanguageModelSanitizerCompatibility } from "../../schema/index.js";
1
+ import { type JsonSchema, type LanguageModel, type LLMRequest } from "../../schema/index.js";
2
2
  export declare const ToolSchemaProjection: {
3
3
  readonly gemini: (schema: JsonSchema) => JsonSchema;
4
- readonly modelCompatibility: (schema: JsonSchema, model: LanguageModel, protocolDefault?: LanguageModelSanitizerCompatibility) => JsonSchema;
5
4
  readonly moonshot: (schema: JsonSchema) => JsonSchema;
6
5
  readonly openAI: (schema: JsonSchema) => JsonSchema;
7
6
  readonly responses: (schema: JsonSchema) => JsonSchema;
7
+ readonly tools: (entries: LLMRequest["tools"], model: LanguageModel) => LLMRequest["tools"];
8
8
  };
@@ -1,3 +1,4 @@
1
+ import { ToolDefinition } from "../../schema/index.js";
1
2
  import { isRecord } from "../../utils/record.js";
2
3
  import { GeminiJsonSchema } from "./gemini-json-schema.js";
3
4
  const tupleItemsSchema = (items) => {
@@ -69,9 +70,11 @@ const objectRoot = (schema) => {
69
70
  // Otherwise the protocol's own default applies (the Gemini API always uses Gemini's rules), then the
70
71
  // model name selects the family's rules so models reached through gateways and OpenAI-compatible
71
72
  // endpoints get the same handling.
72
- const modelCompatibility = (schema, model, protocolDefault) => {
73
+ const modelCompatibility = (schema, model) => {
73
74
  const root = objectRoot(schema);
74
- switch (model.compatibility?.sanitizer ?? protocolDefault ?? MODEL_NAMES.find(([name]) => name.test(model.id))?.[1]) {
75
+ switch (model.compatibility?.sanitizer ??
76
+ model.route.sanitizer ??
77
+ MODEL_NAMES.find(([name]) => name.test(model.id))?.[1]) {
75
78
  case "gemini":
76
79
  return gemini(root);
77
80
  case "moonshot":
@@ -81,10 +84,14 @@ const modelCompatibility = (schema, model, protocolDefault) => {
81
84
  return root;
82
85
  }
83
86
  };
87
+ // Applied once to every request before any protocol builds its body, including tools in namespaces.
88
+ const tools = (entries, model) => entries.map((tool) => tool.type === "tool"
89
+ ? new ToolDefinition({ ...tool, inputSchema: modelCompatibility(tool.inputSchema, model) })
90
+ : { ...tool, tools: tools(tool.tools, model) });
84
91
  export const ToolSchemaProjection = {
85
92
  gemini,
86
- modelCompatibility,
87
93
  moonshot,
88
94
  openAI,
89
95
  responses,
96
+ tools,
90
97
  };
@@ -6,7 +6,7 @@ import { Framing } from "./framing.js";
6
6
  import { HttpTransport } from "./transport/index.js";
7
7
  import type { HttpMiddleware, Transport, TransportRuntime, WebSocketChannelExecutor } from "./transport/index.js";
8
8
  import type { Protocol } from "./protocol.js";
9
- import type { ProtocolID, ProviderOptions } from "../schema/index.js";
9
+ import type { LanguageModelSanitizerCompatibility, ProtocolID, ProviderOptions } from "../schema/index.js";
10
10
  import { AIError, CompactionResponse, CompactionCheckpointResponse, GenerationOptions, HttpOptions, LLMRequest, LLMResponse, LanguageModel, LLMEvent, ProviderID } from "../schema/index.js";
11
11
  export interface RouteBody<Body> {
12
12
  /** Schema for the validated provider-native body sent as the JSON request. */
@@ -31,6 +31,7 @@ export interface Route<Body, Prepared = unknown, Compact extends CompactionOpera
31
31
  readonly defaults: RouteDefaults;
32
32
  readonly body: RouteBody<Body>;
33
33
  readonly supportsEffortUpdates?: (request: LLMRequest) => boolean;
34
+ readonly sanitizer?: LanguageModelSanitizerCompatibility;
34
35
  readonly with: {
35
36
  <Next extends CompactionOperations | undefined>(patch: RoutePatch<Body, Prepared> & {
36
37
  readonly compact: Next;
@@ -9,6 +9,7 @@ import { applyEffortUpdates } from "../effort-updates.js";
9
9
  import { normalizeToolHistory } from "../tool-history.js";
10
10
  import { sanitizeSurrogates } from "../utils/sanitize.js";
11
11
  import * as ProviderShared from "../protocols/shared.js";
12
+ import { ToolSchemaProjection } from "../protocols/utils/tool-schema.js";
12
13
  import { AIError, CompactionResponse, CompactionCheckpointResponse, AIErrorReason, GenerationOptions, HttpOptions, LLMRequest, LLMResponse, LanguageModel, LLMEvent, InvalidProviderOutputError, ProviderConfigurationError, ProviderID, mergeGenerationOptions, mergeHttpOptions, mergeProviderOptions, } from "../schema/index.js";
13
14
  const makeRouteLanguageModel = (route, mapped) => {
14
15
  const provider = route.provider ?? ("provider" in mapped ? mapped.provider : undefined);
@@ -118,6 +119,7 @@ function makeFromTransport(input) {
118
119
  defaults: routeInput.defaults ?? {},
119
120
  body: protocol.body,
120
121
  supportsEffortUpdates: protocol.supportsEffortUpdates,
122
+ sanitizer: protocol.sanitizer,
121
123
  with: (patch) => {
122
124
  const { compact, id, provider, providerMetadataKey, auth, transport, endpoint, ...defaults } = patch;
123
125
  return build({
@@ -210,7 +212,7 @@ const prepareRequest = (request) => {
210
212
  const sanitized = LLMRequest.update(original, sanitizeSurrogates({ ...LLMRequest.input(original), model: undefined }));
211
213
  // Deduplicate per sibling level; a tool and a namespace may share a name.
212
214
  const dedupe = (tools) => [...new Map(tools.map((tool) => [`${tool.type}:${tool.name}`, tool])).values()].map((tool) => tool.type === "tool" ? tool : { ...tool, tools: dedupe(tool.tools) });
213
- const resolved = applyCachePolicy(applyEffortUpdates(LLMRequest.update(sanitized, { tools: dedupe(sanitized.tools) })));
215
+ const resolved = applyCachePolicy(applyEffortUpdates(LLMRequest.update(sanitized, { tools: ToolSchemaProjection.tools(dedupe(sanitized.tools), sanitized.model) })));
214
216
  const headers = resolved.model.route.headers?.({ request: resolved });
215
217
  return headers === undefined
216
218
  ? resolved
@@ -1,5 +1,5 @@
1
1
  import { Schema, type Effect } from "effect";
2
- import type { AIError, LLMEvent, LLMRequest, ProtocolID } from "../schema/index.js";
2
+ import type { AIError, LanguageModelSanitizerCompatibility, LLMEvent, LLMRequest, ProtocolID } from "../schema/index.js";
3
3
  /**
4
4
  * The semantic API contract of one model server family.
5
5
  *
@@ -42,6 +42,8 @@ export interface Protocol<Body, Frame, Event, State> {
42
42
  readonly stream: ProtocolStream<Frame, Event, State>;
43
43
  /** Whether `body.from` lowers `Message.effort(...)` markers; wrappers around another `body.from` must forward it. */
44
44
  readonly supportsEffortUpdates?: (request: LLMRequest) => boolean;
45
+ /** Tool schema sanitizer for every model on this protocol unless the model's compatibility sets one; wrappers around another `body.from` must forward it. */
46
+ readonly sanitizer?: LanguageModelSanitizerCompatibility;
45
47
  }
46
48
  export interface ProtocolBody<Body> {
47
49
  /** Schema for the validated provider-native body sent as the JSON request. */
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-20138",
3
+ "version": "0.0.0-dev-20140",
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-20138",
37
+ "@opencode/http-recorder": "0.0.0-dev-20140",
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-20138",
47
+ "@opencode/schema": "0.0.0-dev-20140",
48
48
  "aws4fetch": "1.0.20",
49
49
  "effect": "4.0.0-rc.112",
50
50
  "google-auth-library": "10.5.0"