Skip to content
Closed
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions packages/opencode/src/provider/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -880,8 +880,67 @@ export namespace ProviderTransform {
return obj.map(sanitizeGemini)
}

// Convert anyOf/oneOf with const values to enum before processing
if (obj.anyOf || obj.oneOf) {
const variants = obj.anyOf || obj.oneOf
if (Array.isArray(variants)) {
const constValues = variants
.filter((v: any) => v && typeof v === "object" && "const" in v)
.map((v: any) => String(v.const))
if (constValues.length === variants.length && constValues.length > 0) {
const merged: any = { ...obj, type: "string", enum: constValues }
delete merged.anyOf
delete merged.oneOf
delete merged.const
return sanitizeGemini(merged)

Copilot AI Feb 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When converting anyOf/oneOf with const values to enum, the description field from individual const objects is lost. For example, {"anyOf": [{"const": "append", "description": "Add to end"}, {"const": "prepend", "description": "Add to start"}]} becomes {"type": "string", "enum": ["append", "prepend"]} without preserving the descriptions for each enum value. Consider preserving the parent description if it exists, or documenting that per-value descriptions cannot be represented in Gemini's enum format.

Copilot uses AI. Check for mistakes.
}
// If anyOf/oneOf contains type variants, pick the first valid one
const typeVariants = variants.filter((v: any) => v && typeof v === "object" && v.type)
if (typeVariants.length > 0) {
const merged: any = { ...obj, ...typeVariants[0] }
delete merged.anyOf
delete merged.oneOf
return sanitizeGemini(merged)
}

Copilot AI Feb 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback logic for handling anyOf/oneOf with type variants may produce unexpected results. When multiple type variants exist, selecting only the first variant discards the other variants without considering whether they represent compatible types or important constraints. For example, if anyOf contains [{"type": "string", "minLength": 5}, {"type": "string", "maxLength": 10}], only the minLength constraint would be preserved. Consider whether this behavior is acceptable for Gemini API compatibility, or if a more sophisticated merge strategy is needed.

Copilot uses AI. Check for mistakes.
}
}
Comment on lines +1019 to +1093

Copilot AI Feb 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When anyOf/oneOf contains a mix of const values and other schema types (e.g., [{"const": "foo"}, {"type": "string"}]), the const values are extracted but the logic checks if constValues.length === variants.length. This means mixed arrays are not converted to enum, and the variants are later stripped entirely on line 939-940. This could result in loss of schema information. Consider logging a warning when schema information is being discarded, or implementing a more graceful degradation strategy.

Copilot uses AI. Check for mistakes.

const result: any = {}
for (const [key, value] of Object.entries(obj)) {
// Skip keywords unsupported by Gemini
if (
key === "additionalProperties" ||
key === "$ref" ||

Copilot AI Feb 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code strips $ref without resolving it to the referenced schema. Issue #12295 mentions that Gemini API rejects schemas containing $ref references and expects them to be expanded. Simply removing $ref will result in an incomplete schema (e.g., {"$ref": "#/$defs/MyType", "description": "..."} becomes just {"description": "..."}). To properly fix this issue, $ref should be resolved by looking up the reference in $defs/definitions and merging the referenced schema into the current location before stripping occurs.

Copilot uses AI. Check for mistakes.
key === "$schema" ||
key === "$id" ||
key === "$defs" ||
key === "definitions" ||
key === "default" ||

Copilot AI Feb 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code strips the "default" keyword which may result in loss of default values that could be useful for API consumers. According to issue #12295, Gemini allows "default" alongside $ref, suggesting it may be supported in some contexts. Consider verifying through testing whether "default" needs to be stripped in all contexts or only in specific ones, and document the reasoning for stripping it if it's necessary for API compatibility.

Copilot uses AI. Check for mistakes.
key === "const" ||
key === "minItems" ||
key === "maxItems" ||
key === "minLength" ||
key === "maxLength" ||
key === "pattern" ||
key === "patternProperties" ||
key === "propertyNames" ||
key === "uniqueItems" ||
key === "minimum" ||
key === "maximum" ||
key === "exclusiveMinimum" ||
key === "exclusiveMaximum" ||
key === "multipleOf" ||
key === "if" ||
key === "then" ||
key === "else" ||
key === "not" ||
key === "title" ||
key === "allOf" ||

Copilot AI Feb 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code strips allOf, anyOf, and oneOf keywords from the schema but does not handle allOf which could be used to merge schemas. Unlike anyOf/oneOf which represent alternatives, allOf represents a combination of all schemas and could potentially be merged into a single schema object. Consider adding logic to handle allOf by merging its array elements into the parent schema, similar to how anyOf/oneOf with const values are handled.

Copilot uses AI. Check for mistakes.
key === "anyOf" ||
key === "oneOf"
) {
continue
}
if (key === "enum" && Array.isArray(value)) {
// Convert all enum values to strings
result[key] = value.map((v) => String(v))
Expand All @@ -896,6 +955,11 @@ export namespace ProviderTransform {
}
}

// Infer type="object" when properties exist but type is missing
if (!result.type && (result.properties || result.required)) {
result.type = "object"
}
Comment on lines 1018 to +1147

Copilot AI Feb 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new functionality for handling anyOf/oneOf with const values, stripping unsupported keywords, and inferring type: "object" lacks test coverage. While the test file packages/opencode/test/provider/transform.test.ts contains tests for other Gemini schema transformations, there are no tests verifying:

  1. Conversion of anyOf/oneOf with const values to enum
  2. Stripping of the 28 newly-added unsupported keywords (additionalProperties, $ref, $schema, etc.)
  3. Type inference when properties/required exist without type

Consider adding comprehensive tests for these new behaviors to prevent regressions.

Copilot uses AI. Check for mistakes.

// Filter required array to only include fields that exist in properties
if (result.type === "object" && result.properties && Array.isArray(result.required)) {
result.required = result.required.filter((field: any) => field in result.properties)
Expand Down
Loading