mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-28 03:27:37 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59aede8f57 | ||
|
|
5a08f39f22 |
@@ -1034,7 +1034,6 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques
|
||||
const format = outputConfig?.format ?? undefined
|
||||
const updates = resolveEffortUpdates(request, options.effort ?? outputConfig?.effort ?? undefined)
|
||||
const generation = request.generation
|
||||
const toolSchemaCompatibility = request.model.compatibility?.toolSchema
|
||||
// Allocate the 4-breakpoint budget in invalidation order: tools → system →
|
||||
// messages. Tools live highest in the cache hierarchy, so when callers
|
||||
// over-mark we keep their tool hints and shed the message-tail ones first.
|
||||
@@ -1044,11 +1043,7 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques
|
||||
flattened.tools.length === 0
|
||||
? undefined
|
||||
: flattened.tools.map((tool) =>
|
||||
lowerTool(
|
||||
breakpoints,
|
||||
tool,
|
||||
ToolSchemaProjection.modelCompatibility(tool.inputSchema, toolSchemaCompatibility),
|
||||
),
|
||||
lowerTool(breakpoints, tool, ToolSchemaProjection.modelCompatibility(tool.inputSchema, request.model)),
|
||||
)
|
||||
// Anthropic rejects tool_choice when tools are absent; "none" is only meaningful with tools present.
|
||||
const toolChoice = tools === undefined || !request.toolChoice ? undefined : yield* lowerToolChoice(request.toolChoice)
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
type FinishReasonDetails,
|
||||
type JsonSchema,
|
||||
type LLMRequest,
|
||||
type LanguageModelToolSchemaCompatibility,
|
||||
type LanguageModel,
|
||||
type ProviderMetadata,
|
||||
type ReasoningPart,
|
||||
type ToolCallPart,
|
||||
@@ -230,13 +230,13 @@ const lowerToolSpec = (tool: ToolDefinition, inputSchema: JsonSchema): BedrockTo
|
||||
})
|
||||
|
||||
const lowerTools = (
|
||||
compatibility: LanguageModelToolSchemaCompatibility | undefined,
|
||||
model: LanguageModel,
|
||||
breakpoints: BedrockCache.Breakpoints,
|
||||
tools: ReadonlyArray<ToolDefinition>,
|
||||
): BedrockTool[] => {
|
||||
const result: BedrockTool[] = []
|
||||
for (const tool of tools) {
|
||||
result.push(lowerToolSpec(tool, ToolSchemaProjection.modelCompatibility(tool.inputSchema, compatibility)))
|
||||
result.push(lowerToolSpec(tool, ToolSchemaProjection.modelCompatibility(tool.inputSchema, model)))
|
||||
const cachePoint = BedrockCache.block(breakpoints, tool.cache)
|
||||
if (cachePoint) result.push(cachePoint)
|
||||
}
|
||||
@@ -440,7 +440,7 @@ const fromRequest = Effect.fn("BedrockConverse.fromRequest")(function* (request:
|
||||
const toolConfig = (() => {
|
||||
if (flattened.tools.length === 0) return undefined
|
||||
return {
|
||||
tools: lowerTools(request.model.compatibility?.toolSchema, breakpoints, flattened.tools),
|
||||
tools: lowerTools(request.model, breakpoints, flattened.tools),
|
||||
// Converse has no native "none". Keep definitions stable for prompt
|
||||
// caching and omit only the unsupported choice.
|
||||
toolChoice,
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
LLMEvent,
|
||||
Usage,
|
||||
type FinishReason,
|
||||
type JsonSchema,
|
||||
type LLMRequest,
|
||||
type MediaPart,
|
||||
type ProviderMetadata,
|
||||
@@ -23,9 +22,8 @@ import { classifyProviderFailure } from "../provider-error.js"
|
||||
import { Media } from "../media.js"
|
||||
import { JsonObject, knownString, lenient, optionalArray, optionalNull, ProviderShared } from "./shared.js"
|
||||
import { GeminiGenerateContent } from "./utils/gemini-generate-content.js"
|
||||
import { GeminiToolSchema } from "./utils/gemini-tool-schema.js"
|
||||
import { GeminiJsonSchema } from "./utils/gemini-json-schema.js"
|
||||
import { Lifecycle } from "./utils/lifecycle.js"
|
||||
import { ToolSchemaProjection } from "./utils/tool-schema.js"
|
||||
|
||||
const ADAPTER = "gemini"
|
||||
// Google documents this sentinel for replaying Gemini 3 function calls after their original signature was lost.
|
||||
@@ -133,7 +131,7 @@ const GeminiSystemInstruction = Schema.Struct({
|
||||
const GeminiFunctionDeclaration = Schema.Struct({
|
||||
name: Schema.String,
|
||||
description: Schema.String,
|
||||
parameters: Schema.optional(JsonObject),
|
||||
parametersJsonSchema: JsonObject,
|
||||
})
|
||||
|
||||
const GeminiTool = Schema.Struct({
|
||||
@@ -266,36 +264,16 @@ interface ParserState {
|
||||
readonly seenCallIds?: ReadonlySet<string>
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Tool Schema Conversion
|
||||
// =============================================================================
|
||||
// Tool-schema conversion has two distinct concerns:
|
||||
//
|
||||
// 1. Sanitize — fix common authoring mistakes Gemini rejects: integer/number
|
||||
// enums (must be strings), `required` entries that don't match a property,
|
||||
// untyped arrays (`items` must be present), and `properties`/`required`
|
||||
// keys on non-object scalars. Mirrors OpenCode's historical Gemini rules.
|
||||
//
|
||||
// 2. Project — lossy mapping from JSON Schema to Gemini's schema dialect:
|
||||
// drop empty root parameter schemas while preserving nested empty objects,
|
||||
// expand type arrays into `anyOf`, derive `nullable: true` from null members,
|
||||
// coerce `const` to `[const]` enum, recurse properties/items, and propagate
|
||||
// only an allowlisted set of keys (description, required, format, type,
|
||||
// nullable, enum, properties, items, allOf, anyOf, oneOf, minLength).
|
||||
// Anything outside the allowlist (e.g. `additionalProperties`, `$ref`) is
|
||||
// silently dropped.
|
||||
//
|
||||
// Sanitize runs first, then project. The implementation lives in
|
||||
// `utils/gemini-tool-schema` so this protocol keeps the same shape as the other
|
||||
// provider protocols.
|
||||
|
||||
// =============================================================================
|
||||
// Request Lowering
|
||||
// =============================================================================
|
||||
const lowerTool = (tool: ToolDefinition, inputSchema: JsonSchema) => ({
|
||||
// Tool schemas go in `parametersJsonSchema`, which accepts standard JSON Schema.
|
||||
// `utils/gemini-json-schema` rewrites only the few shapes Gemini still rejects. This
|
||||
// protocol always applies it, including for tuned endpoints whose IDs do not name Gemini.
|
||||
const lowerTool = (tool: ToolDefinition) => ({
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
parameters: GeminiToolSchema.convert(inputSchema),
|
||||
parametersJsonSchema: GeminiJsonSchema.normalize(tool.inputSchema),
|
||||
})
|
||||
|
||||
const lowerToolConfig = (toolChoice: NonNullable<LLMRequest["toolChoice"]>) =>
|
||||
@@ -465,7 +443,6 @@ const fromRequest = Effect.fn("Gemini.fromRequest")(function* (request: LLMReque
|
||||
const hasTools = flattened.tools.length > 0
|
||||
const generation = request.generation
|
||||
const options = yield* decodeOptions(request.providerOptions ?? {})
|
||||
const toolSchemaCompatibility = request.model.compatibility?.toolSchema
|
||||
const generationConfig = {
|
||||
maxOutputTokens: generation?.maxTokens,
|
||||
temperature: generation?.temperature,
|
||||
@@ -491,9 +468,7 @@ const fromRequest = Effect.fn("Gemini.fromRequest")(function* (request: LLMReque
|
||||
tools: hasTools
|
||||
? [
|
||||
{
|
||||
functionDeclarations: flattened.tools.map((tool) =>
|
||||
lowerTool(tool, ToolSchemaProjection.modelCompatibility(tool.inputSchema, toolSchemaCompatibility)),
|
||||
),
|
||||
functionDeclarations: flattened.tools.map(lowerTool),
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
|
||||
@@ -107,7 +107,7 @@ const fromRequest = Effect.fn("MetaResponses.fromRequest")(function* (request: L
|
||||
return yield* OpenResponses.lowerTool(
|
||||
NAME,
|
||||
tool,
|
||||
ToolSchemaProjection.modelCompatibility(tool.inputSchema, request.model.compatibility?.toolSchema),
|
||||
ToolSchemaProjection.modelCompatibility(tool.inputSchema, request.model),
|
||||
)
|
||||
return yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(NativeTool))(tool.native.meta)
|
||||
}),
|
||||
|
||||
@@ -818,7 +818,6 @@ export const fromRequestWithAdapter = Effect.fn("OpenResponses.fromRequestWithAd
|
||||
adapter: ProviderAdapter,
|
||||
) {
|
||||
const projected = ProviderShared.flattenToolRequest(request)
|
||||
const toolSchemaCompatibility = request.model.compatibility?.toolSchema
|
||||
return {
|
||||
...(yield* lowerConversation(projected.request, adapter)),
|
||||
...lowerGeneration(request),
|
||||
@@ -828,11 +827,7 @@ export const fromRequestWithAdapter = Effect.fn("OpenResponses.fromRequestWithAd
|
||||
: yield* Effect.forEach(projected.tools, (tool) =>
|
||||
tool.native !== undefined && adapter.nativeTool
|
||||
? adapter.nativeTool(tool.native)
|
||||
: lowerTool(
|
||||
adapter.name,
|
||||
tool,
|
||||
ToolSchemaProjection.modelCompatibility(tool.inputSchema, toolSchemaCompatibility),
|
||||
),
|
||||
: lowerTool(adapter.name, tool, ToolSchemaProjection.modelCompatibility(tool.inputSchema, request.model)),
|
||||
),
|
||||
tool_choice:
|
||||
allowedToolChoice(request) ??
|
||||
|
||||
@@ -803,7 +803,6 @@ export const fromRequest = Effect.fn("OpenAIChat.fromRequest")(function* (
|
||||
`OpenAI Chat reasoning field conflicts with reserved field ${reasoningField}`,
|
||||
)
|
||||
const generation = request.generation
|
||||
const toolSchemaCompatibility = request.model.compatibility?.toolSchema
|
||||
const flattened = ProviderShared.flattenToolRequest(request)
|
||||
const provider = String(request.model.provider)
|
||||
const baseURL = request.model.route.endpoint.baseURL
|
||||
@@ -829,7 +828,7 @@ export const fromRequest = Effect.fn("OpenAIChat.fromRequest")(function* (
|
||||
: flattened.tools.map((tool) =>
|
||||
lowerTool(
|
||||
tool,
|
||||
ToolSchemaProjection.modelCompatibility(tool.inputSchema, toolSchemaCompatibility),
|
||||
ToolSchemaProjection.modelCompatibility(tool.inputSchema, request.model),
|
||||
options,
|
||||
supportsStrictMode,
|
||||
),
|
||||
|
||||
@@ -5,7 +5,14 @@ import { Auth } from "../route/auth.js"
|
||||
import { Endpoint } from "../route/endpoint.js"
|
||||
import { Protocol } from "../route/protocol.js"
|
||||
import { HttpTransport } from "../route/transport/index.js"
|
||||
import { LLMRequest, mergeJsonRecords, type JsonSchema, type ToolDefinition, type ToolEntry } from "../schema/index.js"
|
||||
import {
|
||||
LLMRequest,
|
||||
mergeJsonRecords,
|
||||
type JsonSchema,
|
||||
type LanguageModel,
|
||||
type ToolDefinition,
|
||||
type ToolEntry,
|
||||
} from "../schema/index.js"
|
||||
import { resolveEffortUpdates } from "../effort-updates.js"
|
||||
import { OpenResponses } from "./open-responses.js"
|
||||
import { OpenResponsesOptions } from "./utils/open-responses-options.js"
|
||||
@@ -170,12 +177,9 @@ const lowerTool = Effect.fn("OpenAIResponses.lowerTool")(function* (tool: ToolDe
|
||||
|
||||
// Native namespaces hold only function tools, so deeper levels flatten into
|
||||
// the leaf names the same way non-native protocols flatten the whole tree.
|
||||
const lowerToolEntry = Effect.fn("OpenAIResponses.lowerToolEntry")(function* (
|
||||
tool: ToolEntry,
|
||||
compatibility: Parameters<typeof ToolSchemaProjection.modelCompatibility>[1],
|
||||
) {
|
||||
const lowerToolEntry = Effect.fn("OpenAIResponses.lowerToolEntry")(function* (tool: ToolEntry, model: LanguageModel) {
|
||||
if (tool.type === "tool")
|
||||
return yield* lowerTool(tool, ToolSchemaProjection.modelCompatibility(tool.inputSchema, compatibility))
|
||||
return yield* lowerTool(tool, ToolSchemaProjection.modelCompatibility(tool.inputSchema, model))
|
||||
// OpenAI requires a namespace description; fall back to a generic one so a
|
||||
// missing description never blocks the request.
|
||||
return {
|
||||
@@ -183,7 +187,7 @@ const lowerToolEntry = Effect.fn("OpenAIResponses.lowerToolEntry")(function* (
|
||||
name: tool.name,
|
||||
description: tool.description ?? `Tools in the ${tool.name} namespace.`,
|
||||
tools: yield* Effect.forEach(ProviderShared.flattenTools(tool.tools), (leaf) =>
|
||||
OpenResponses.lowerTool(NAME, leaf, ToolSchemaProjection.modelCompatibility(leaf.inputSchema, compatibility)),
|
||||
OpenResponses.lowerTool(NAME, leaf, ToolSchemaProjection.modelCompatibility(leaf.inputSchema, model)),
|
||||
),
|
||||
}
|
||||
})
|
||||
@@ -207,7 +211,6 @@ const fromRequest = Effect.fn("OpenAIResponses.fromRequest")(function* (request:
|
||||
)(request.providerOptions?.contextManagement)
|
||||
const options = OpenResponsesOptions.resolve(request)
|
||||
const updates = resolveEffortUpdates(request, options.reasoningEffort)
|
||||
const toolSchemaCompatibility = request.model.compatibility?.toolSchema
|
||||
return yield* decodeBody({
|
||||
...(yield* OpenResponses.lowerConversation(updates.request, adapter)),
|
||||
...OpenResponses.lowerGeneration(request, { ...options, reasoningEffort: updates.effort }),
|
||||
@@ -215,7 +218,7 @@ const fromRequest = Effect.fn("OpenAIResponses.fromRequest")(function* (request:
|
||||
tools:
|
||||
request.tools.length === 0
|
||||
? undefined
|
||||
: yield* Effect.forEach(request.tools, (tool) => lowerToolEntry(tool, toolSchemaCompatibility)),
|
||||
: yield* Effect.forEach(request.tools, (tool) => lowerToolEntry(tool, request.model)),
|
||||
tool_choice:
|
||||
request.tools.length === 0
|
||||
? undefined
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { JsonSchema } from "../../schema/index.js"
|
||||
import { isRecord } from "../../utils/record.js"
|
||||
|
||||
// Gemini's `parametersJsonSchema` accepts standard JSON Schema, but rejects a few shapes that
|
||||
// published tool schemas commonly contain. Rewrite only those and send everything else unchanged.
|
||||
const SCHEMA_MAPS = new Set([
|
||||
"properties",
|
||||
"patternProperties",
|
||||
"$defs",
|
||||
"definitions",
|
||||
"dependentSchemas",
|
||||
"dependencies",
|
||||
])
|
||||
const VALUES = new Set(["const", "default", "enum", "examples", "dependentRequired"])
|
||||
|
||||
const mapValues = (record: Record<string, unknown>, map: (value: unknown, key: string) => unknown) =>
|
||||
Object.fromEntries(Object.entries(record).map(([key, value]) => [key, map(value, key)]))
|
||||
|
||||
const normalizeNode = (schema: unknown): unknown => {
|
||||
if (Array.isArray(schema)) return schema.map(normalizeNode)
|
||||
if (!isRecord(schema)) return schema
|
||||
const properties = isRecord(schema.properties) ? schema.properties : undefined
|
||||
return Object.fromEntries(
|
||||
Object.entries(schema).flatMap(([key, value]) => {
|
||||
if (VALUES.has(key)) return [[key, value]]
|
||||
if (SCHEMA_MAPS.has(key) && isRecord(value)) return [[key, mapValues(value, normalizeNode)]]
|
||||
// `required` may only name declared properties.
|
||||
if (key === "required" && properties && Array.isArray(value))
|
||||
return [[key, value.filter((name) => typeof name === "string" && Object.hasOwn(properties, name))]]
|
||||
// Draft-04 boolean exclusive bounds become the numeric form.
|
||||
if (key === "exclusiveMinimum" && typeof value === "boolean")
|
||||
return value && typeof schema.minimum === "number" ? [[key, schema.minimum]] : []
|
||||
if (key === "exclusiveMaximum" && typeof value === "boolean")
|
||||
return value && typeof schema.maximum === "number" ? [[key, schema.maximum]] : []
|
||||
if (key === "minimum" && schema.exclusiveMinimum === true) return []
|
||||
if (key === "maximum" && schema.exclusiveMaximum === true) return []
|
||||
// Draft-07 tuples (`items` array plus `additionalItems`) are `prefixItems` plus `items` in 2020-12.
|
||||
if (key === "items" && Array.isArray(value)) return [["prefixItems", value.map(normalizeNode)]]
|
||||
if (key === "additionalItems" && Array.isArray(schema.items)) return [["items", normalizeNode(value)]]
|
||||
return [[key, normalizeNode(value)]]
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// Gemini accepts a recursive `$ref` only when the loop passes through an optional property or
|
||||
// potentially empty array `items`. Replace other self-references with an unconstrained schema.
|
||||
const cutLoops = (schema: unknown, target: string, safe: boolean): unknown => {
|
||||
if (Array.isArray(schema)) return schema.map((item) => cutLoops(item, target, safe))
|
||||
if (!isRecord(schema)) return schema
|
||||
if (schema.$ref === target && !safe) return {}
|
||||
const required = Array.isArray(schema.required) ? schema.required : []
|
||||
return mapValues(schema, (value, key) => {
|
||||
if (VALUES.has(key)) return value
|
||||
if (key === "items") return cutLoops(value, target, safe || !(Number(schema.minItems) > 0))
|
||||
if (key === "properties" && isRecord(value))
|
||||
return mapValues(value, (child, name) => cutLoops(child, target, safe || !required.includes(name)))
|
||||
if (SCHEMA_MAPS.has(key) && isRecord(value)) return mapValues(value, (child) => cutLoops(child, target, safe))
|
||||
return cutLoops(value, target, safe)
|
||||
})
|
||||
}
|
||||
|
||||
export const normalize = (schema: JsonSchema): JsonSchema => {
|
||||
const normalized = normalizeNode(schema)
|
||||
if (!isRecord(normalized)) return {}
|
||||
const result = cutLoops(
|
||||
mapValues(normalized, (value, key) =>
|
||||
(key === "$defs" || key === "definitions") && isRecord(value)
|
||||
? mapValues(value, (def, name) => cutLoops(def, `#/${key}/${name}`, false))
|
||||
: value,
|
||||
),
|
||||
"#",
|
||||
false,
|
||||
)
|
||||
return isRecord(result) ? result : {}
|
||||
}
|
||||
|
||||
export * as GeminiJsonSchema from "./gemini-json-schema.js"
|
||||
@@ -1,119 +0,0 @@
|
||||
import { isRecord } from "../../utils/record.js"
|
||||
|
||||
// Gemini accepts a JSON Schema-like dialect for tool parameters, but rejects a
|
||||
// handful of common JSON Schema shapes. Keep this projection isolated so the
|
||||
// Gemini protocol file still reads like the other protocol modules.
|
||||
const SCHEMA_INTENT_KEYS = [
|
||||
"type",
|
||||
"properties",
|
||||
"items",
|
||||
"prefixItems",
|
||||
"enum",
|
||||
"const",
|
||||
"$ref",
|
||||
"additionalProperties",
|
||||
"patternProperties",
|
||||
"required",
|
||||
"not",
|
||||
"if",
|
||||
"then",
|
||||
"else",
|
||||
]
|
||||
|
||||
const hasCombiner = (schema: unknown) =>
|
||||
isRecord(schema) && (Array.isArray(schema.anyOf) || Array.isArray(schema.oneOf) || Array.isArray(schema.allOf))
|
||||
|
||||
const hasSchemaIntent = (schema: unknown) =>
|
||||
isRecord(schema) && (hasCombiner(schema) || SCHEMA_INTENT_KEYS.some((key) => key in schema))
|
||||
|
||||
const sanitizeNode = (schema: unknown): unknown => {
|
||||
if (!isRecord(schema)) return Array.isArray(schema) ? schema.map(sanitizeNode) : schema
|
||||
|
||||
const result: Record<string, unknown> = Object.fromEntries(
|
||||
Object.entries(schema).map(([key, value]) => [
|
||||
key,
|
||||
key === "enum" && Array.isArray(value) ? value.map(String) : sanitizeNode(value),
|
||||
]),
|
||||
)
|
||||
|
||||
if (Array.isArray(result.enum) && (result.type === "integer" || result.type === "number")) result.type = "string"
|
||||
|
||||
const properties = result.properties
|
||||
if (result.type === "object" && isRecord(properties) && Array.isArray(result.required)) {
|
||||
result.required = result.required.filter((field) => typeof field === "string" && field in properties)
|
||||
}
|
||||
|
||||
if (result.type === "array" && !hasCombiner(result)) {
|
||||
result.items = result.items ?? {}
|
||||
if (isRecord(result.items) && !hasSchemaIntent(result.items)) result.items = { ...result.items, type: "string" }
|
||||
}
|
||||
|
||||
if (typeof result.type === "string" && result.type !== "object" && !hasCombiner(result)) {
|
||||
delete result.properties
|
||||
delete result.required
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const emptyObjectSchema = (schema: Record<string, unknown>) =>
|
||||
schema.type === "object" &&
|
||||
(!isRecord(schema.properties) || Object.keys(schema.properties).length === 0) &&
|
||||
!schema.additionalProperties
|
||||
|
||||
const projectNode = (schema: unknown, nested = false): Record<string, unknown> | undefined => {
|
||||
if (!isRecord(schema)) return undefined
|
||||
if (!nested && emptyObjectSchema(schema)) return undefined
|
||||
const types = Array.isArray(schema.type) ? schema.type.filter((type) => type !== "null") : undefined
|
||||
const anyOf = Array.isArray(schema.anyOf) ? schema.anyOf : undefined
|
||||
const hasNullAnyOf = anyOf?.some((item) => isRecord(item) && item.type === "null") ?? false
|
||||
const anyOfTypes = hasNullAnyOf ? anyOf?.filter((item) => !isRecord(item) || item.type !== "null") : anyOf
|
||||
const flattenedAnyOf = hasNullAnyOf && anyOfTypes?.length === 1 ? projectNode(anyOfTypes[0], true) : undefined
|
||||
const result = Object.fromEntries(
|
||||
[
|
||||
["description", schema.description],
|
||||
["required", schema.required],
|
||||
["format", schema.format],
|
||||
["type", types ? (types.length === 0 ? "null" : undefined) : schema.type],
|
||||
[
|
||||
"nullable",
|
||||
(Array.isArray(schema.type) && schema.type.includes("null") && types && types.length > 0) || hasNullAnyOf
|
||||
? true
|
||||
: undefined,
|
||||
],
|
||||
["enum", schema.const !== undefined ? [schema.const] : schema.enum],
|
||||
[
|
||||
"properties",
|
||||
isRecord(schema.properties)
|
||||
? Object.fromEntries(Object.entries(schema.properties).map(([key, value]) => [key, projectNode(value, true)]))
|
||||
: undefined,
|
||||
],
|
||||
[
|
||||
"items",
|
||||
Array.isArray(schema.items)
|
||||
? schema.items.map((item) => projectNode(item, true))
|
||||
: schema.items === undefined
|
||||
? undefined
|
||||
: projectNode(schema.items, true),
|
||||
],
|
||||
["allOf", Array.isArray(schema.allOf) ? schema.allOf.map((item) => projectNode(item, true)) : undefined],
|
||||
[
|
||||
"anyOf",
|
||||
anyOfTypes
|
||||
? hasNullAnyOf && anyOfTypes.length === 1
|
||||
? undefined
|
||||
: anyOfTypes.map((item) => projectNode(item, true))
|
||||
: types && types.length > 0
|
||||
? types.map((type) => ({ type }))
|
||||
: undefined,
|
||||
],
|
||||
["oneOf", Array.isArray(schema.oneOf) ? schema.oneOf.map((item) => projectNode(item, true)) : undefined],
|
||||
["minLength", schema.minLength],
|
||||
].filter((entry) => entry[1] !== undefined),
|
||||
)
|
||||
return flattenedAnyOf ? { ...result, ...flattenedAnyOf } : result
|
||||
}
|
||||
|
||||
export const convert = (schema: unknown) => projectNode(sanitizeNode(schema))
|
||||
|
||||
export * as GeminiToolSchema from "./gemini-tool-schema.js"
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { JsonSchema, LanguageModelToolSchemaCompatibility } from "../../schema/index.js"
|
||||
import type { JsonSchema, LanguageModel } from "../../schema/index.js"
|
||||
import { isRecord } from "../../utils/record.js"
|
||||
import { GeminiToolSchema } from "./gemini-tool-schema.js"
|
||||
import { GeminiJsonSchema } from "./gemini-json-schema.js"
|
||||
|
||||
const tupleItemsSchema = (items: ReadonlyArray<unknown>) => {
|
||||
const projected = items.map(moonshotNode)
|
||||
@@ -46,18 +46,18 @@ const moonshot = (schema: JsonSchema): JsonSchema => {
|
||||
const openAI = (schema: JsonSchema): JsonSchema => schema
|
||||
const responses = openAI
|
||||
|
||||
const gemini = (schema: JsonSchema): JsonSchema => GeminiToolSchema.convert(schema) ?? {}
|
||||
const gemini = GeminiJsonSchema.normalize
|
||||
|
||||
const modelCompatibility = (
|
||||
schema: JsonSchema,
|
||||
compatibility: LanguageModelToolSchemaCompatibility | undefined,
|
||||
): JsonSchema => {
|
||||
if (compatibility === undefined) return schema
|
||||
switch (compatibility) {
|
||||
// An explicit compatibility setting wins. Otherwise a Gemini model name selects Gemini's rules,
|
||||
// so Gemini reached through gateways and OpenAI-compatible endpoints gets the same schema handling.
|
||||
const modelCompatibility = (schema: JsonSchema, model: LanguageModel): JsonSchema => {
|
||||
switch (model.compatibility?.toolSchema ?? (/gemini/i.test(model.id) ? "gemini" : undefined)) {
|
||||
case "gemini":
|
||||
return gemini(schema)
|
||||
case "moonshot":
|
||||
return moonshot(schema)
|
||||
case undefined:
|
||||
return schema
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"protocol:gemini"
|
||||
],
|
||||
"name": "google-vertex/calls-a-tool",
|
||||
"recordedAt": "2026-09-22T03:46:29.725Z"
|
||||
"recordedAt": "2026-09-24T01:06:19.246Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
@@ -18,14 +18,14 @@
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"contents\":[{\"role\":\"user\",\"parts\":[{\"text\":\"What is the weather in Paris? Use the lookup_weather tool.\"}]}],\"tools\":[{\"functionDeclarations\":[{\"name\":\"lookup_weather\",\"description\":\"Look up the current weather for a city\",\"parameters\":{\"required\":[\"city\"],\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}}}}]}]}"
|
||||
"body": "{\"contents\":[{\"role\":\"user\",\"parts\":[{\"text\":\"What is the weather in Paris? Use the lookup_weather tool.\"}]}],\"tools\":[{\"functionDeclarations\":[{\"name\":\"lookup_weather\",\"description\":\"Look up the current weather for a city\",\"parametersJsonSchema\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]}}]}]}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": "data: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"functionCall\": {\"name\": \"lookup_weather\",\"args\": {\"city\": \"Paris\"},\"id\": \"call_28936\"},\"thoughtSignature\": \"AY89a1/MVeBxtwu9l/96clFobOrrd7Q5MTf5o8/A22fc++1EsFYVeCFx9WCGmJ+D3yW3FWlRXKtJECYfprSoxixCSOYBNiV9g8/IkejbAxF26k1vxTsHkoqts0O2s8GqqBSRAavJfKQ/taTRbPuIq+b+RKZ8SDJFoWDnFjXpdS5S164uJTjmgs4ALd8oB1oQRycMsTbAY51TmkcNUnhARpjKQfqiizz4KfSRFjx7xDnza0v4jj73m2GV86eLDhVT7trIT+Dl5DsJt6RBLOE2Amse54mcpIbsYaANlY1YDU6FKuUBqnuwpl0OfhMXwWuYMogv1tH/0rTXsqNwmHyt/b9ZZXqYCdFXpMGt8udmoZ0d\"}]}}],\"usageMetadata\": {\"trafficType\": \"ON_DEMAND\"},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-09-22T03:46:28.953809Z\",\"responseId\": \"FPqxatGbOsSorb8PtY2z8Q4\"}\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \"\"}]},\"finishReason\": \"STOP\"}],\"usageMetadata\": {\"promptTokenCount\": 39,\"candidatesTokenCount\": 16,\"totalTokenCount\": 93,\"trafficType\": \"ON_DEMAND\",\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 39}],\"candidatesTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 16}],\"thoughtsTokenCount\": 38},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-09-22T03:46:28.953809Z\",\"responseId\": \"FPqxatGbOsSorb8PtY2z8Q4\"}\r\n\r\n"
|
||||
"body": "data: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"functionCall\": {\"name\": \"lookup_weather\",\"args\": {\"city\": \"Paris\"},\"id\": \"call_554608\"},\"thoughtSignature\": \"AY89a19hRAmVFgUw7vo3hvQF7VpSnCGYSRXo34+yjCySmgHtvL/4LEf6V2F12glreaWUixVINDFWR+DLprZZxayR8gb+wZMWssV0Cbv1fDPYV6qxggRiLEKnG6A7UY+vmhXUO5IAtL06Ud24IYd+3QI0T05gzSnwqy7Uotw9J5B8LSr76MvbDncgyeWcBhZSpxxi2XPJxZqR6N/7gwxRSW3KMBp0CKHnU4MF+t0fN1oQ2uSWCxQAYW0nq8TQL6nYYgShN35VeGJ5+UvJ8OIYC0COQ1u/VNdqmJs2uRs3dHGzXzDSWdb/lIcqtzyKRLsIa7RWYmF1xZhEQZkXGGf9yco8e1OOovYhR/cEwE2+aw1scl3Deg2+D2R1PiFwwoZuMV+lIj3IwNexjLd56ZO/7QpzaX2K97b11duH4mBjgfG0x7c3+r9raD17quokhgW1ffoPiXiaemvD6mA84kkDIc4jxSnwNwuVxTgUkEwswub1C1lrlnzzeLHjnMvdopdHIViDsdcfRnXkhIwGyvr+QBSuz7yF1V/Vw+9MxYVVNM/7eQPpMxjJHpHTBMWqtzkC9zE6wK2iWr49AJm8ZpMS8a9uhW0OAksUslv5gvWKDEecaufO/66w+xltgnvDh2NVJuthULuG+nWnxGRq4ZbX00s3p0/J1vqQZcLsi37RpAs75g==\"}]}}],\"usageMetadata\": {\"trafficType\": \"ON_DEMAND\"},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-09-24T01:06:18.093130Z\",\"responseId\": \"ine0asrXBbui3dAPpNDS8QE\"}\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \"\"}]},\"finishReason\": \"STOP\"}],\"usageMetadata\": {\"promptTokenCount\": 39,\"candidatesTokenCount\": 16,\"totalTokenCount\": 156,\"trafficType\": \"ON_DEMAND\",\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 39}],\"candidatesTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 16}],\"thoughtsTokenCount\": 101},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-09-24T01:06:18.093130Z\",\"responseId\": \"ine0asrXBbui3dAPpNDS8QE\"}\r\n\r\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
+3
-3
@@ -7,7 +7,7 @@
|
||||
"protocol:gemini"
|
||||
],
|
||||
"name": "google-vertex/continues-after-a-tool-result",
|
||||
"recordedAt": "2026-09-22T03:46:30.691Z"
|
||||
"recordedAt": "2026-09-24T01:06:20.321Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
@@ -18,14 +18,14 @@
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"contents\":[{\"role\":\"user\",\"parts\":[{\"text\":\"What is the weather in Paris?\"}]},{\"role\":\"model\",\"parts\":[{\"functionCall\":{\"id\":\"call_paris_1\",\"name\":\"lookup_weather\",\"args\":{\"city\":\"Paris\"}},\"thoughtSignature\":\"skip_thought_signature_validator\"}]},{\"role\":\"user\",\"parts\":[{\"functionResponse\":{\"id\":\"call_paris_1\",\"name\":\"lookup_weather\",\"response\":{\"name\":\"lookup_weather\",\"content\":\"18C, light rain\"}}}]}],\"tools\":[{\"functionDeclarations\":[{\"name\":\"lookup_weather\",\"description\":\"Look up the current weather for a city\",\"parameters\":{\"required\":[\"city\"],\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}}}}]}]}"
|
||||
"body": "{\"contents\":[{\"role\":\"user\",\"parts\":[{\"text\":\"What is the weather in Paris?\"}]},{\"role\":\"model\",\"parts\":[{\"functionCall\":{\"id\":\"call_paris_1\",\"name\":\"lookup_weather\",\"args\":{\"city\":\"Paris\"}},\"thoughtSignature\":\"skip_thought_signature_validator\"}]},{\"role\":\"user\",\"parts\":[{\"functionResponse\":{\"id\":\"call_paris_1\",\"name\":\"lookup_weather\",\"response\":{\"name\":\"lookup_weather\",\"content\":\"18C, light rain\"}}}]}],\"tools\":[{\"functionDeclarations\":[{\"name\":\"lookup_weather\",\"description\":\"Look up the current weather for a city\",\"parametersJsonSchema\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]}}]}]}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": "data: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \"The weather\"}]}}],\"usageMetadata\": {\"trafficType\": \"ON_DEMAND\"},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-09-22T03:46:29.830958Z\",\"responseId\": \"Ffqxau7bMoCOrb8P_Iu5sA0\"}\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \" in Paris is currently 18°C with light rain.\"}]}}],\"usageMetadata\": {\"trafficType\": \"ON_DEMAND\"},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-09-22T03:46:29.830958Z\",\"responseId\": \"Ffqxau7bMoCOrb8P_Iu5sA0\"}\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \"\",\"thoughtSignature\": \"AY89a18aVI8Uxr2VaOI822u0DyuQdy4B00uhLnjqYb5Qb6Mkscccm018knLtYThB5UX8dRv1VFsORSQ0Qo6Gx9RCng2AK9EPce7p\"}]},\"finishReason\": \"STOP\"}],\"usageMetadata\": {\"promptTokenCount\": 59,\"candidatesTokenCount\": 15,\"totalTokenCount\": 74,\"trafficType\": \"ON_DEMAND\",\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 59}],\"candidatesTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 15}]},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-09-22T03:46:29.830958Z\",\"responseId\": \"Ffqxau7bMoCOrb8P_Iu5sA0\"}\r\n\r\n"
|
||||
"body": "data: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \"The weather\"}]}}],\"usageMetadata\": {\"trafficType\": \"ON_DEMAND\"},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-09-24T01:06:19.664329Z\",\"responseId\": \"i3e0aonGKPCnrb8Pm97yiQs\"}\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \" in Paris is currently 18°C with light rain.\"}]}}],\"usageMetadata\": {\"trafficType\": \"ON_DEMAND\"},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-09-24T01:06:19.664329Z\",\"responseId\": \"i3e0aonGKPCnrb8Pm97yiQs\"}\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \"\",\"thoughtSignature\": \"AY89a19mRwbMV0dQccZShfDptLQ20vI7Rg/LSyPDwW1Wt62fQvRT/2nUWW13cMmgYbd2VGGtrZIbnj+hxHMZt1NK2RyJIkg/FB13\"}]},\"finishReason\": \"STOP\"}],\"usageMetadata\": {\"promptTokenCount\": 59,\"candidatesTokenCount\": 15,\"totalTokenCount\": 74,\"trafficType\": \"ON_DEMAND\",\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 59}],\"candidatesTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 15}]},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-09-24T01:06:19.664329Z\",\"responseId\": \"i3e0aonGKPCnrb8Pm97yiQs\"}\r\n\r\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"tags": ["prefix:pdf", "pdf", "provider:google", "protocol:gemini", "tool", "tool-result"],
|
||||
"tags": [
|
||||
"prefix:pdf",
|
||||
"pdf",
|
||||
"provider:google",
|
||||
"protocol:gemini",
|
||||
"tool",
|
||||
"tool-result"
|
||||
],
|
||||
"name": "pdf/gemini-tool-result",
|
||||
"recordedAt": "2026-07-22T18:21:59.606Z"
|
||||
"recordedAt": "2026-09-24T01:06:23.498Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
@@ -14,14 +21,14 @@
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"contents\":[{\"role\":\"user\",\"parts\":[{\"text\":\"Use read_pdf with path verification.pdf and return the verification code.\"}]}],\"systemInstruction\":{\"parts\":[{\"text\":\"Call read_pdf exactly once with path verification.pdf, then reply only with the verification code from its PDF.\"}]},\"tools\":[{\"functionDeclarations\":[{\"name\":\"read_pdf\",\"description\":\"Read the attached PDF.\",\"parameters\":{\"required\":[\"path\"],\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\"}}}}]}],\"generationConfig\":{\"maxOutputTokens\":256,\"temperature\":0}}"
|
||||
"body": "{\"contents\":[{\"role\":\"user\",\"parts\":[{\"text\":\"Use read_pdf with path verification.pdf and return the verification code.\"}]}],\"systemInstruction\":{\"parts\":[{\"text\":\"Call read_pdf exactly once with path verification.pdf, then reply only with the verification code from its PDF.\"}]},\"tools\":[{\"functionDeclarations\":[{\"name\":\"read_pdf\",\"description\":\"Read the attached PDF.\",\"parametersJsonSchema\":{\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\"}},\"required\":[\"path\"],\"additionalProperties\":false}}]}],\"generationConfig\":{\"maxOutputTokens\":256,\"temperature\":0}}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": "data: {\"candidates\": [{\"content\": {\"parts\": [{\"functionCall\": {\"name\": \"read_pdf\",\"args\": {\"path\": \"verification.pdf\"},\"id\": \"58shgmez\"},\"thoughtSignature\": \"EqkCCqYCARFNMg/JrCTv5i3zYENFBVpZNFL3pbzJmi5Eu387ncF703xFMB4pwyaP7a1gi49EqBhCI2hWOpesU5nZQOLAhGgExKGa2GM+HzpEB5g62r0NFblm/BGkVZaImTuHR7bytfRC5jHQlHKo4OS27OLUVjvkMkBIYsvjhDErY7niERbXJVpyxTVqUf1GgZMSu8kC9/5WDlMs9xVKNT/6KMW4PhhSR9nXg4KZUa+bC03/ydhsWWgBa5aLCgvTq7WPj217xIsmUkSiRedIffPsUSNjYdMHUvWi8bOlvM1veEEP6GIfv5h9gXXzjnHbEHfQxV8PZuBAyY7iM6nqyfkJNdkZ1HdB7DXMBsMsRN6SgrIrFoXX2WaGrkoEI5tdZx1t/gdwF1jEVT6k\"}],\"role\": \"model\"},\"index\": 0}],\"usageMetadata\": {\"promptTokenCount\": 81,\"candidatesTokenCount\": 18,\"totalTokenCount\": 151,\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 81}],\"thoughtsTokenCount\": 52,\"serviceTier\": \"standard\"},\"modelVersion\": \"gemini-3.5-flash\",\"responseId\": \"RAphaui3OaSHz7IPy8Kb4Ak\"}\r\n\r\ndata: {\"candidates\": [{\"content\": {\"parts\": [{\"text\": \"\"}],\"role\": \"model\"},\"finishReason\": \"STOP\",\"index\": 0}],\"usageMetadata\": {\"promptTokenCount\": 81,\"candidatesTokenCount\": 18,\"totalTokenCount\": 151,\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 81}],\"thoughtsTokenCount\": 52,\"serviceTier\": \"standard\"},\"modelVersion\": \"gemini-3.5-flash\",\"responseId\": \"RAphaui3OaSHz7IPy8Kb4Ak\"}\r\n\r\n"
|
||||
"body": "data: {\"candidates\": [{\"content\": {\"parts\": [{\"functionCall\": {\"name\": \"read_pdf\",\"args\": {\"path\": \"verification.pdf\"},\"id\": \"call_475949\"},\"thoughtSignature\": \"Et4CCtsCAWkUfRPfQWeGIEQ/AhexY97IZoK8aNLr2EbW3nufnAXpa1y74z7qH1Tdk20j5EC57BSBbdy5u6xFHNomzZhGFVmdVlhgZFXGSO8F8K4t8rqQWUJZSDH4dFaC95KpUTfp5CAMepbOuc/dGLgXfVsSV0e/HvUcdWKoEz8Jlsgk59nexqSsdl/kGzuuIeW6zS5r8JHus+PfRzvSgua4B9OpDU9WWPCJZu93NLbXJELbZ330h7CP+uxXoo1Puu0KSrvLman8Geoc0nagMx895L0gO3CsbB+PpijQ6Pt6YAaiv1V2MeOCUKCYUpk+77MjC6RnsTFTRaJYJ5PPkniuRKedp/DF1aMj7nGxQ6BpHOMq9mjw2zlH3I2kJi1wqm+amVz30vTHa31xTH88w+IUc1yNdLAzdVJV+/+0mqw8bLweSjSHFxZN0NIv6aczAIS5stoGobNDO4bRGDETH3Q=\"}],\"role\": \"model\"},\"index\": 0}],\"usageMetadata\": {\"promptTokenCount\": 81,\"candidatesTokenCount\": 18,\"totalTokenCount\": 152,\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 81}],\"thoughtsTokenCount\": 53,\"serviceTier\": \"standard\"},\"modelVersion\": \"gemini-3.5-flash\",\"responseId\": \"jHe0apWcM92aqtsP8t2LyAg\"}\r\n\r\ndata: {\"candidates\": [{\"content\": {\"parts\": [{\"text\": \"\"}],\"role\": \"model\"},\"finishReason\": \"STOP\",\"index\": 0}],\"usageMetadata\": {\"promptTokenCount\": 81,\"candidatesTokenCount\": 18,\"totalTokenCount\": 152,\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 81}],\"thoughtsTokenCount\": 53,\"serviceTier\": \"standard\"},\"modelVersion\": \"gemini-3.5-flash\",\"responseId\": \"jHe0apWcM92aqtsP8t2LyAg\"}\r\n\r\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -32,14 +39,14 @@
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"contents\":[{\"role\":\"user\",\"parts\":[{\"text\":\"Use read_pdf with path verification.pdf and return the verification code.\"}]},{\"role\":\"model\",\"parts\":[{\"functionCall\":{\"id\":\"58shgmez\",\"name\":\"read_pdf\",\"args\":{\"path\":\"verification.pdf\"}},\"thoughtSignature\":\"EqkCCqYCARFNMg/JrCTv5i3zYENFBVpZNFL3pbzJmi5Eu387ncF703xFMB4pwyaP7a1gi49EqBhCI2hWOpesU5nZQOLAhGgExKGa2GM+HzpEB5g62r0NFblm/BGkVZaImTuHR7bytfRC5jHQlHKo4OS27OLUVjvkMkBIYsvjhDErY7niERbXJVpyxTVqUf1GgZMSu8kC9/5WDlMs9xVKNT/6KMW4PhhSR9nXg4KZUa+bC03/ydhsWWgBa5aLCgvTq7WPj217xIsmUkSiRedIffPsUSNjYdMHUvWi8bOlvM1veEEP6GIfv5h9gXXzjnHbEHfQxV8PZuBAyY7iM6nqyfkJNdkZ1HdB7DXMBsMsRN6SgrIrFoXX2WaGrkoEI5tdZx1t/gdwF1jEVT6k\"}]},{\"role\":\"user\",\"parts\":[{\"functionResponse\":{\"id\":\"58shgmez\",\"name\":\"read_pdf\",\"response\":{\"name\":\"read_pdf\",\"content\":\"PDF read successfully\"},\"parts\":[{\"inlineData\":{\"mimeType\":\"application/pdf\",\"data\":\"JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4KZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3ggWzAgMCA2MTIgNzkyXSAvUmVzb3VyY2VzIDw8IC9Gb250IDw8IC9GMSA1IDAgUiA+PiA+PiAvQ29udGVudHMgNCAwIFIgPj4KZW5kb2JqCjQgMCBvYmoKPDwgL0xlbmd0aCA3NSA+PgpzdHJlYW0KQlQKL0YxIDE4IFRmCjcyIDcyMCBUZAooUERGIGNhc3NldHRlIHZlcmlmaWNhdGlvbiBjb2RlOiBPUkNISUQtNzM5MSkgVGoKRVQKZW5kc3RyZWFtCmVuZG9iago1IDAgb2JqCjw8IC9UeXBlIC9Gb250IC9TdWJ0eXBlIC9UeXBlMSAvQmFzZUZvbnQgL0hlbHZldGljYSA+PgplbmRvYmoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTggMDAwMDAgbiAKMDAwMDAwMDExNSAwMDAwMCBuIAowMDAwMDAwMjQxIDAwMDAwIG4gCjAwMDAwMDAzNjUgMDAwMDAgbiAKdHJhaWxlcgo8PCAvU2l6ZSA2IC9Sb290IDEgMCBSID4+CnN0YXJ0eHJlZgo0MzUKJSVFT0YK\"}}]}}]}],\"systemInstruction\":{\"parts\":[{\"text\":\"Call read_pdf exactly once with path verification.pdf, then reply only with the verification code from its PDF.\"}]},\"tools\":[{\"functionDeclarations\":[{\"name\":\"read_pdf\",\"description\":\"Read the attached PDF.\",\"parameters\":{\"required\":[\"path\"],\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\"}}}}]}],\"generationConfig\":{\"maxOutputTokens\":256,\"temperature\":0}}"
|
||||
"body": "{\"contents\":[{\"role\":\"user\",\"parts\":[{\"text\":\"Use read_pdf with path verification.pdf and return the verification code.\"}]},{\"role\":\"model\",\"parts\":[{\"functionCall\":{\"id\":\"call_475949\",\"name\":\"read_pdf\",\"args\":{\"path\":\"verification.pdf\"}},\"thoughtSignature\":\"Et4CCtsCAWkUfRPfQWeGIEQ/AhexY97IZoK8aNLr2EbW3nufnAXpa1y74z7qH1Tdk20j5EC57BSBbdy5u6xFHNomzZhGFVmdVlhgZFXGSO8F8K4t8rqQWUJZSDH4dFaC95KpUTfp5CAMepbOuc/dGLgXfVsSV0e/HvUcdWKoEz8Jlsgk59nexqSsdl/kGzuuIeW6zS5r8JHus+PfRzvSgua4B9OpDU9WWPCJZu93NLbXJELbZ330h7CP+uxXoo1Puu0KSrvLman8Geoc0nagMx895L0gO3CsbB+PpijQ6Pt6YAaiv1V2MeOCUKCYUpk+77MjC6RnsTFTRaJYJ5PPkniuRKedp/DF1aMj7nGxQ6BpHOMq9mjw2zlH3I2kJi1wqm+amVz30vTHa31xTH88w+IUc1yNdLAzdVJV+/+0mqw8bLweSjSHFxZN0NIv6aczAIS5stoGobNDO4bRGDETH3Q=\"}]},{\"role\":\"user\",\"parts\":[{\"functionResponse\":{\"id\":\"call_475949\",\"name\":\"read_pdf\",\"response\":{\"name\":\"read_pdf\",\"content\":\"PDF read successfully\"},\"parts\":[{\"inlineData\":{\"mimeType\":\"application/pdf\",\"data\":\"JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4KZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3ggWzAgMCA2MTIgNzkyXSAvUmVzb3VyY2VzIDw8IC9Gb250IDw8IC9GMSA1IDAgUiA+PiA+PiAvQ29udGVudHMgNCAwIFIgPj4KZW5kb2JqCjQgMCBvYmoKPDwgL0xlbmd0aCA3NSA+PgpzdHJlYW0KQlQKL0YxIDE4IFRmCjcyIDcyMCBUZAooUERGIGNhc3NldHRlIHZlcmlmaWNhdGlvbiBjb2RlOiBPUkNISUQtNzM5MSkgVGoKRVQKZW5kc3RyZWFtCmVuZG9iago1IDAgb2JqCjw8IC9UeXBlIC9Gb250IC9TdWJ0eXBlIC9UeXBlMSAvQmFzZUZvbnQgL0hlbHZldGljYSA+PgplbmRvYmoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTggMDAwMDAgbiAKMDAwMDAwMDExNSAwMDAwMCBuIAowMDAwMDAwMjQxIDAwMDAwIG4gCjAwMDAwMDAzNjUgMDAwMDAgbiAKdHJhaWxlcgo8PCAvU2l6ZSA2IC9Sb290IDEgMCBSID4+CnN0YXJ0eHJlZgo0MzUKJSVFT0YK\"}}]}}]}],\"systemInstruction\":{\"parts\":[{\"text\":\"Call read_pdf exactly once with path verification.pdf, then reply only with the verification code from its PDF.\"}]},\"tools\":[{\"functionDeclarations\":[{\"name\":\"read_pdf\",\"description\":\"Read the attached PDF.\",\"parametersJsonSchema\":{\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\"}},\"required\":[\"path\"],\"additionalProperties\":false}}]}],\"generationConfig\":{\"maxOutputTokens\":256,\"temperature\":0}}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": "data: {\"candidates\": [{\"content\": {\"parts\": [{\"text\": \"ORCHID-7391\"}],\"role\": \"model\"},\"index\": 0}],\"usageMetadata\": {\"promptTokenCount\": 123,\"candidatesTokenCount\": 8,\"totalTokenCount\": 184,\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 123}],\"thoughtsTokenCount\": 53,\"serviceTier\": \"standard\"},\"modelVersion\": \"gemini-3.5-flash\",\"responseId\": \"RgphaoL6CMjQz7IPjOnEmQI\"}\r\n\r\ndata: {\"candidates\": [{\"content\": {\"parts\": [{\"text\": \"\",\"thoughtSignature\": \"EqECCp4CARFNMg9obBl8O6iU9lawUIWiE+1vztZm9NtaT9FuyJz343hd9ruz+xPco4Q1DY1GF81ZiSI2ElBkt8Wfwsqtix9LNGSMvbZhhk/ZnB54t05M/Dft1kujcMvEdZUWUI/jWaJ349tO1bKVH9MacG5+gl0n4y8DwyQZSV3xIcet547drSkcA/TM03RB+yj1/dcLHsvUjmv9EnO897vZgO2Dk4tbZ2NyCtOeQ3JKVhUTLg2pjkGk+POCNiOdESWiUzxdQKw9LiV6nnzi071tXNiMeVimq6d7xAzRVNapI2uXynvn9Uk3eyn85purOFa8cKriK9oD6vcyGMqgd9+gu2m3to0IHqd7o+2YSr1m5qV1xT1R2/WRQEtb1b1AuOAU6w==\"}],\"role\": \"model\"},\"finishReason\": \"STOP\",\"index\": 0}],\"usageMetadata\": {\"promptTokenCount\": 1277,\"candidatesTokenCount\": 8,\"totalTokenCount\": 1338,\"promptTokensDetails\": [{\"modality\": \"IMAGE\",\"tokenCount\": 1102},{\"modality\": \"TEXT\",\"tokenCount\": 175}],\"thoughtsTokenCount\": 53,\"serviceTier\": \"standard\"},\"modelVersion\": \"gemini-3.5-flash\",\"responseId\": \"RgphaoL6CMjQz7IPjOnEmQI\"}\r\n\r\n"
|
||||
"body": "data: {\"candidates\": [{\"content\": {\"parts\": [{\"text\": \"ORCHID-7391\"}],\"role\": \"model\"},\"index\": 0}],\"usageMetadata\": {\"promptTokenCount\": 123,\"candidatesTokenCount\": 8,\"totalTokenCount\": 184,\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 123}],\"thoughtsTokenCount\": 53,\"serviceTier\": \"standard\"},\"modelVersion\": \"gemini-3.5-flash\",\"responseId\": \"jXe0ar7CNdjBqtsPqMm2oAs\"}\r\n\r\ndata: {\"candidates\": [{\"content\": {\"parts\": [{\"text\": \"\",\"thoughtSignature\": \"EtcCCtQCAWkUfRM3dQjvavztF1qActLo1mpXoGlWdQ+qUpKf1DdrmgWIT1nm70cm+yGf4pj6UU9fhKU+/y+ScUv1IJrb3c3yU5X2/CGzcN/DeRhxuvuXRaGxtWi4GIu2nFr3oJrAOnJnh/kosgiqixwQ3EK6/0jdE2HOqxu7jImvozaVcR9OiVGgAHRwmDYtB6K+6/jMVjORBKB4nZp8MTb3IWaiN6soO+FiWRgl3etnnJhkJb6eBHhHmLW8qxyOmVwRII3Gtm5jJbxGND/aZSpxlBvfBpGfbJQJIfNQDs+sXeQpuZh8SZUVRgVAZyOrqbVS1vRq8kODaBvDHKuTr0Qo9RpGGGo0npaYooP663omHDhm7Uu5plFwAVYR5G4z11my6efbHE1WvbcCgg6deqMFf6BX0SzzgPovB3p+giuHVwjD7gyYS3sdw/txxYPkY3+H/gWchf0FGw==\"}],\"role\": \"model\"},\"finishReason\": \"STOP\",\"index\": 0}],\"usageMetadata\": {\"promptTokenCount\": 696,\"candidatesTokenCount\": 8,\"totalTokenCount\": 757,\"promptTokensDetails\": [{\"modality\": \"IMAGE\",\"tokenCount\": 520},{\"modality\": \"TEXT\",\"tokenCount\": 176}],\"thoughtsTokenCount\": 53,\"serviceTier\": \"standard\"},\"modelVersion\": \"gemini-3.5-flash\",\"responseId\": \"jXe0ar7CNdjBqtsPqMm2oAs\"}\r\n\r\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -381,7 +381,7 @@ describe("Gemini route", () => {
|
||||
{
|
||||
name: "lookup",
|
||||
description: "Lookup data",
|
||||
parameters: { type: "object", properties: { query: { type: "string" } } },
|
||||
parametersJsonSchema: { type: "object", properties: { query: { type: "string" } } },
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -676,7 +676,7 @@ describe("Gemini route", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("sanitizes integer enums, dangling required, untyped arrays, and scalar object keys", () =>
|
||||
it.effect("normalizes the JSON Schema shapes Gemini rejects", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
LLM.request({
|
||||
@@ -689,11 +689,32 @@ describe("Gemini route", () => {
|
||||
description: "Lookup data",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
required: ["status", "missing"],
|
||||
required: ["ttl", "items", "parent", "missing"],
|
||||
properties: {
|
||||
status: { type: "integer", enum: [1, 2] },
|
||||
tags: { type: "array" },
|
||||
name: { type: "string", properties: { ignored: { type: "string" } }, required: ["ignored"] },
|
||||
ttl: { type: "number", minimum: 0, exclusiveMinimum: true, maximum: 60, exclusiveMaximum: false },
|
||||
pair: { type: "array", items: [{ type: "string" }, { type: "integer" }], additionalItems: false },
|
||||
items: { type: "array", items: { type: "string" } },
|
||||
required: { type: "boolean" },
|
||||
params: { $ref: "#/$defs/Json" },
|
||||
tree: { $ref: "#/$defs/Node" },
|
||||
chain: { $ref: "#/$defs/Link" },
|
||||
batch: { $ref: "#/$defs/Batch" },
|
||||
self: { $ref: "#" },
|
||||
parent: { $ref: "#" },
|
||||
cart: { type: "object", default: { items: ["a"] } },
|
||||
},
|
||||
dependentRequired: { items: ["ttl"] },
|
||||
$defs: {
|
||||
Json: {
|
||||
anyOf: [
|
||||
{ type: "string" },
|
||||
{ type: "array", items: { $ref: "#/$defs/Json" } },
|
||||
{ type: "object", additionalProperties: { $ref: "#/$defs/Json" } },
|
||||
],
|
||||
},
|
||||
Node: { type: "object", properties: { child: { $ref: "#/$defs/Node" } } },
|
||||
Link: { type: "object", required: ["next"], properties: { next: { $ref: "#/$defs/Link" } } },
|
||||
Batch: { type: "array", minItems: 1, items: { $ref: "#/$defs/Batch" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -701,24 +722,68 @@ describe("Gemini route", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
expect(prepared.body).toMatchObject({
|
||||
tools: [
|
||||
{
|
||||
functionDeclarations: [
|
||||
{
|
||||
parameters: {
|
||||
type: "object",
|
||||
required: ["status"],
|
||||
properties: {
|
||||
status: { type: "string", enum: ["1", "2"] },
|
||||
tags: { type: "array", items: { type: "string" } },
|
||||
name: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
expect(prepared.body.tools?.[0]?.functionDeclarations[0]?.parametersJsonSchema).toEqual({
|
||||
type: "object",
|
||||
required: ["ttl", "items", "parent"],
|
||||
properties: {
|
||||
ttl: { type: "number", exclusiveMinimum: 0, maximum: 60 },
|
||||
pair: { type: "array", prefixItems: [{ type: "string" }, { type: "integer" }], items: false },
|
||||
items: { type: "array", items: { type: "string" } },
|
||||
required: { type: "boolean" },
|
||||
params: { $ref: "#/$defs/Json" },
|
||||
tree: { $ref: "#/$defs/Node" },
|
||||
chain: { $ref: "#/$defs/Link" },
|
||||
batch: { $ref: "#/$defs/Batch" },
|
||||
self: { $ref: "#" },
|
||||
parent: {},
|
||||
cart: { type: "object", default: { items: ["a"] } },
|
||||
},
|
||||
dependentRequired: { items: ["ttl"] },
|
||||
$defs: {
|
||||
Json: {
|
||||
anyOf: [
|
||||
{ type: "string" },
|
||||
{ type: "array", items: { $ref: "#/$defs/Json" } },
|
||||
{ type: "object", additionalProperties: {} },
|
||||
],
|
||||
},
|
||||
],
|
||||
Node: { type: "object", properties: { child: { $ref: "#/$defs/Node" } } },
|
||||
Link: { type: "object", required: ["next"], properties: { next: {} } },
|
||||
Batch: { type: "array", minItems: 1, items: {} },
|
||||
},
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("normalizes tool schemas for tuned endpoints whose IDs do not name Gemini", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
LLM.request({
|
||||
model: Gemini.route
|
||||
.with({
|
||||
endpoint: { baseURL: "https://generativelanguage.test/v1beta/" },
|
||||
auth: Auth.header("x-goog-api-key", "test"),
|
||||
})
|
||||
.model({ id: "projects/p/locations/global/endpoints/123" }),
|
||||
prompt: "Use the tool.",
|
||||
tools: [
|
||||
{
|
||||
name: "lookup",
|
||||
description: "Lookup data",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
required: ["query", "missing"],
|
||||
properties: { query: { type: "string" } },
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
expect(prepared.body.tools?.[0]?.functionDeclarations[0]?.parametersJsonSchema).toEqual({
|
||||
type: "object",
|
||||
required: ["query"],
|
||||
properties: { query: { type: "string" } },
|
||||
})
|
||||
}),
|
||||
)
|
||||
@@ -751,7 +816,7 @@ describe("Gemini route", () => {
|
||||
{
|
||||
name: "configure",
|
||||
description: "Configure the operation",
|
||||
parameters: {
|
||||
parametersJsonSchema: {
|
||||
type: "object",
|
||||
required: ["options"],
|
||||
properties: {
|
||||
@@ -765,55 +830,38 @@ describe("Gemini route", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("projects Gemini type arrays without narrowing their allowed values", () =>
|
||||
it.effect("sends standard JSON Schema tool parameters unchanged", () =>
|
||||
Effect.gen(function* () {
|
||||
const inputSchema = {
|
||||
type: "object",
|
||||
required: ["level"],
|
||||
properties: {
|
||||
level: { type: "integer", enum: [1, 2, 3] },
|
||||
status: { type: ["number", "string"], description: "Status filter" },
|
||||
maybe: { anyOf: [{ type: "string" }, { type: "null" }] },
|
||||
kind: { const: "fixed" },
|
||||
mode: { enum: ["fast", "safe"] },
|
||||
tags: { type: "array" },
|
||||
env: { type: "object", additionalProperties: { type: "string" } },
|
||||
action: {
|
||||
oneOf: [
|
||||
{ type: "object", properties: { type: { const: "move" }, to: { type: "string" } } },
|
||||
{ type: "object", properties: { type: { const: "delete" } } },
|
||||
],
|
||||
},
|
||||
item: { $ref: "#/$defs/Item" },
|
||||
},
|
||||
$defs: { Item: { type: "object", properties: { id: { type: "string" } }, required: ["id"] } },
|
||||
}
|
||||
const prepared = yield* compileRequest(
|
||||
LLM.request({
|
||||
model,
|
||||
prompt: "Use the tool.",
|
||||
tools: [
|
||||
{
|
||||
name: "filter",
|
||||
description: "Filter values",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
status: { type: ["number", "string"], description: "Status filter" },
|
||||
maybe: { type: ["string", "null"] },
|
||||
nothing: { type: ["null"] },
|
||||
explicit: { anyOf: [{ type: "string" }, { type: "null" }] },
|
||||
choice: { anyOf: [{ type: "string" }, { type: "number" }, { type: "null" }] },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
tools: [{ name: "filter", description: "Filter values", inputSchema }],
|
||||
}),
|
||||
)
|
||||
|
||||
expect(prepared.body.tools?.[0]?.functionDeclarations[0]?.parameters).toEqual({
|
||||
type: "object",
|
||||
properties: {
|
||||
status: {
|
||||
description: "Status filter",
|
||||
anyOf: [{ type: "number" }, { type: "string" }],
|
||||
},
|
||||
maybe: {
|
||||
nullable: true,
|
||||
anyOf: [{ type: "string" }],
|
||||
},
|
||||
nothing: {
|
||||
type: "null",
|
||||
},
|
||||
explicit: {
|
||||
type: "string",
|
||||
nullable: true,
|
||||
},
|
||||
choice: {
|
||||
anyOf: [{ type: "string" }, { type: "number" }],
|
||||
nullable: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(prepared.body.tools?.[0]?.functionDeclarations[0]?.parametersJsonSchema).toEqual(inputSchema)
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -60,27 +60,37 @@ describe("tool schema projections", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("gemini handles numeric enums, dangling required fields, untyped arrays, and scalar object keys", () => {
|
||||
expect(
|
||||
ToolSchemaProjection.gemini({
|
||||
it.effect("selects Gemini schema handling from the model name unless compatibility is explicit", () =>
|
||||
Effect.gen(function* () {
|
||||
const route = OpenAIChat.route.with({
|
||||
endpoint: { baseURL: "https://api.openai.test/v1/" },
|
||||
auth: Auth.bearer("test"),
|
||||
})
|
||||
const original = {
|
||||
type: "object",
|
||||
required: ["status", "missing"],
|
||||
properties: {
|
||||
status: { type: "integer", enum: [1, 2] },
|
||||
tags: { type: "array" },
|
||||
name: { type: "string", properties: { ignored: { type: "string" } }, required: ["ignored"] },
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
type: "object",
|
||||
required: ["status"],
|
||||
properties: {
|
||||
status: { type: "string", enum: ["1", "2"] },
|
||||
tags: { type: "array", items: { type: "string" } },
|
||||
name: { type: "string" },
|
||||
},
|
||||
})
|
||||
})
|
||||
required: ["mode", "missing"],
|
||||
properties: { mode: { enum: ["fast", "safe"] } },
|
||||
}
|
||||
const parameters = (model: ReturnType<typeof route.model>) =>
|
||||
compileRequest(
|
||||
LLM.request({
|
||||
model,
|
||||
prompt: "Use the tool.",
|
||||
tools: [{ name: "lookup", description: "Lookup data.", inputSchema: original }],
|
||||
}),
|
||||
).pipe(Effect.map((prepared) => prepared.body.tools?.[0]?.function.parameters))
|
||||
const gemini = { ...original, required: ["mode"] }
|
||||
|
||||
expect(yield* parameters(route.model({ id: "google/Gemini-3.8-Flash" }))).toEqual(gemini)
|
||||
expect(
|
||||
yield* parameters(route.model({ id: "my-tuned-endpoint", compatibility: { toolSchema: "gemini" } })),
|
||||
).toEqual(gemini)
|
||||
expect(
|
||||
yield* parameters(route.model({ id: "google/gemini-3.8-flash", compatibility: { toolSchema: "moonshot" } })),
|
||||
).toEqual({ ...original, properties: { mode: { type: "string", enum: ["fast", "safe"] } } })
|
||||
expect(yield* parameters(route.model({ id: "gpt-6-luna" }))).toEqual(original)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("applies model compatibility without changing schema semantics", () =>
|
||||
Effect.gen(function* () {
|
||||
|
||||
Reference in New Issue
Block a user