From 3adeed8f97d612171dfdecf8cc33eee193811172 Mon Sep 17 00:00:00 2001 From: Kiyoung Chang <81131079+ChickenBreast-ky@users.noreply.github.com> Date: Tue, 3 Feb 2026 14:13:24 +0900 Subject: [PATCH 1/3] fix(provider): strip properties/required from non-object types in Gemini schema (#11888) --- packages/opencode/src/provider/transform.ts | 6 + .../opencode/test/provider/transform.test.ts | 110 ++++++++++++++++++ 2 files changed, 116 insertions(+) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index c05bf75c46..b4f1aaca4d 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -772,6 +772,12 @@ export namespace ProviderTransform { result.items = {} } + // Remove properties/required from non-object types (Gemini rejects these) + if (result.type && result.type !== "object") { + delete result.properties + delete result.required + } + return result } diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 8e28f1209e..0743049fe0 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -293,6 +293,116 @@ describe("ProviderTransform.schema - gemini array items", () => { }) }) +describe("ProviderTransform.schema - gemini non-object properties removal", () => { + const geminiModel = { + providerID: "google", + api: { + id: "gemini-3-pro", + }, + } as any + + test("removes properties from non-object types", () => { + const schema = { + type: "object", + properties: { + data: { + type: "string", + properties: { invalid: { type: "string" } }, + }, + }, + } as any + + const result = ProviderTransform.schema(geminiModel, schema) as any + + expect(result.properties.data.type).toBe("string") + expect(result.properties.data.properties).toBeUndefined() + }) + + test("removes required from non-object types", () => { + const schema = { + type: "object", + properties: { + data: { + type: "array", + items: { type: "string" }, + required: ["invalid"], + }, + }, + } as any + + const result = ProviderTransform.schema(geminiModel, schema) as any + + expect(result.properties.data.type).toBe("array") + expect(result.properties.data.required).toBeUndefined() + }) + + test("removes properties and required from nested non-object types", () => { + const schema = { + type: "object", + properties: { + outer: { + type: "object", + properties: { + inner: { + type: "number", + properties: { bad: { type: "string" } }, + required: ["bad"], + }, + }, + }, + }, + } as any + + const result = ProviderTransform.schema(geminiModel, schema) as any + + expect(result.properties.outer.properties.inner.type).toBe("number") + expect(result.properties.outer.properties.inner.properties).toBeUndefined() + expect(result.properties.outer.properties.inner.required).toBeUndefined() + }) + + test("keeps properties and required on object types", () => { + const schema = { + type: "object", + properties: { + data: { + type: "object", + properties: { name: { type: "string" } }, + required: ["name"], + }, + }, + } as any + + const result = ProviderTransform.schema(geminiModel, schema) as any + + expect(result.properties.data.type).toBe("object") + expect(result.properties.data.properties).toBeDefined() + expect(result.properties.data.required).toEqual(["name"]) + }) + + test("does not affect non-gemini providers", () => { + const openaiModel = { + providerID: "openai", + api: { + id: "gpt-4", + }, + } as any + + const schema = { + type: "object", + properties: { + data: { + type: "string", + properties: { invalid: { type: "string" } }, + }, + }, + } as any + + const result = ProviderTransform.schema(openaiModel, schema) as any + + expect(result.properties.data.properties).toBeDefined() + }) +}) + describe("ProviderTransform.message - DeepSeek reasoning content", () => { test("DeepSeek with tool calls includes reasoning_content in providerOptions", () => { const msgs = [ From 801e4a8a9dbf2e11859e1e4126b7f1d058722eee Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 3 Feb 2026 00:17:03 -0500 Subject: [PATCH 2/3] wip: zen --- packages/console/app/src/routes/zen/util/handler.ts | 2 +- packages/console/app/src/routes/zen/util/rateLimiter.ts | 8 ++++++-- packages/console/core/src/model.ts | 2 ++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/console/app/src/routes/zen/util/handler.ts b/packages/console/app/src/routes/zen/util/handler.ts index b97af851e2..91fa306af4 100644 --- a/packages/console/app/src/routes/zen/util/handler.ts +++ b/packages/console/app/src/routes/zen/util/handler.ts @@ -79,7 +79,7 @@ export async function handler( const dataDumper = createDataDumper(sessionId, requestId, projectId) const trialLimiter = createTrialLimiter(modelInfo.trial, ip, ocClient) const isTrial = await trialLimiter?.isTrial() - const rateLimiter = createRateLimiter(modelInfo.rateLimit, ip) + const rateLimiter = createRateLimiter(modelInfo.rateLimit, ip, input.request.headers) await rateLimiter?.check() const stickyTracker = createStickyTracker(modelInfo.stickyProvider, sessionId) const stickyProvider = await stickyTracker?.get() diff --git a/packages/console/app/src/routes/zen/util/rateLimiter.ts b/packages/console/app/src/routes/zen/util/rateLimiter.ts index d54bd0306d..918c33a403 100644 --- a/packages/console/app/src/routes/zen/util/rateLimiter.ts +++ b/packages/console/app/src/routes/zen/util/rateLimiter.ts @@ -4,9 +4,13 @@ import { RateLimitError } from "./error" import { logger } from "./logger" import { ZenData } from "@opencode-ai/console-core/model.js" -export function createRateLimiter(limit: ZenData.RateLimit | undefined, rawIp: string) { +export function createRateLimiter(limit: ZenData.RateLimit | undefined, rawIp: string, headers: Headers) { if (!limit) return + const limitValue = (limit.checkHeader && !headers.get(limit.checkHeader)) + ? limit.fallbackValue! + : limit.value + const ip = !rawIp.length ? "unknown" : rawIp const now = Date.now() const intervals = @@ -32,7 +36,7 @@ export function createRateLimiter(limit: ZenData.RateLimit | undefined, rawIp: s ) const total = rows.reduce((sum, r) => sum + r.count, 0) logger.debug(`rate limit total: ${total}`) - if (total >= limit.value) throw new RateLimitError(`Rate limit exceeded. Please try again later.`) + if (total >= limitValue) throw new RateLimitError(`Rate limit exceeded. Please try again later.`) }, } } diff --git a/packages/console/core/src/model.ts b/packages/console/core/src/model.ts index fc9674cedb..6b06f275d4 100644 --- a/packages/console/core/src/model.ts +++ b/packages/console/core/src/model.ts @@ -21,6 +21,8 @@ export namespace ZenData { const RateLimitSchema = z.object({ period: z.enum(["day", "rolling"]), value: z.number().int(), + checkHeader: z.string().optional(), + fallbackValue: z.number().int().optional(), }) export type Format = z.infer export type Trial = z.infer From 3f07dffbb0bbf591755321498037f003fc483444 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Tue, 3 Feb 2026 05:19:12 +0000 Subject: [PATCH 3/3] chore: generate --- packages/console/app/src/routes/zen/util/rateLimiter.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/console/app/src/routes/zen/util/rateLimiter.ts b/packages/console/app/src/routes/zen/util/rateLimiter.ts index 918c33a403..90e10479c4 100644 --- a/packages/console/app/src/routes/zen/util/rateLimiter.ts +++ b/packages/console/app/src/routes/zen/util/rateLimiter.ts @@ -7,9 +7,7 @@ import { ZenData } from "@opencode-ai/console-core/model.js" export function createRateLimiter(limit: ZenData.RateLimit | undefined, rawIp: string, headers: Headers) { if (!limit) return - const limitValue = (limit.checkHeader && !headers.get(limit.checkHeader)) - ? limit.fallbackValue! - : limit.value + const limitValue = limit.checkHeader && !headers.get(limit.checkHeader) ? limit.fallbackValue! : limit.value const ip = !rawIp.length ? "unknown" : rawIp const now = Date.now()