refactor(kilo-gateway): clean up proxy auth naming, error handling, and OpenAPI schema alignment

Rename `proxy` helper to `proxyAuth` for clarity, switch modes endpoint
to gracefully handle missing auth instead of returning BadRequest, remove
redundant Content-Type header from audio transcriptions response, and
fix OpenAPI parameter types:

- Change `worktrees` query param from string enum to boolean
- Add `^que.*` pattern to network endpoint requestID path params
- Register experimental session worktrees in query parameter schemas
- Re-enable and update Effect HttpApi route parity tests
This commit is contained in:
Imanol Maiztegui
2026-05-18 15:51:10 +02:00
parent a1a1936416
commit d7fcb894ed
6 changed files with 29 additions and 23 deletions
@@ -55,7 +55,7 @@ export const kiloGatewayHandlers = HttpApiBuilder.group(InstanceHttpApi, "kilo",
return { profile, balance, currentOrgId }
})
const proxy = Effect.fn("KiloGatewayHttpApi.proxyAuth")(function* () {
const proxyAuth = Effect.fn("KiloGatewayHttpApi.proxyAuth")(function* () {
const info = yield* auth.get("kilo").pipe(Effect.mapError(() => new HttpApiError.Unauthorized({})))
return {
auth: info,
@@ -65,7 +65,7 @@ export const kiloGatewayHandlers = HttpApiBuilder.group(InstanceHttpApi, "kilo",
})
const modes = Effect.fn("KiloGatewayHttpApi.modes")(function* () {
const info = yield* auth.get("kilo").pipe(Effect.mapError(() => new HttpApiError.BadRequest({})))
const info = yield* auth.get("kilo").pipe(Effect.catch(() => Effect.succeed(undefined)))
if (!info || info.type !== "oauth" || !info.access || !info.accountId) return { modes: [] }
return yield* Effect.promise(() => fetchOrganizationModes(info.access, info.accountId)).pipe(
@@ -75,7 +75,7 @@ export const kiloGatewayHandlers = HttpApiBuilder.group(InstanceHttpApi, "kilo",
})
const fim = Effect.fn("KiloGatewayHttpApi.fim")(function* (ctx: { payload: typeof FimBody.Type }) {
const info = yield* proxy()
const info = yield* proxyAuth()
if (!info.auth) return yield* Effect.fail(new HttpApiError.Unauthorized({}))
if (!info.token) return yield* Effect.fail(new HttpApiError.Unauthorized({}))
@@ -136,7 +136,7 @@ export const kiloGatewayHandlers = HttpApiBuilder.group(InstanceHttpApi, "kilo",
const audioTranscriptions = Effect.fn("KiloGatewayHttpApi.audioTranscriptions")(function* (ctx: {
payload: typeof AudioTranscriptionsBody.Type
}) {
const info = yield* proxy()
const info = yield* proxyAuth()
if (!info.auth) return yield* Effect.fail(new HttpApiError.Unauthorized({}))
if (!info.token) return yield* Effect.fail(new HttpApiError.Unauthorized({}))
@@ -160,9 +160,6 @@ export const kiloGatewayHandlers = HttpApiBuilder.group(InstanceHttpApi, "kilo",
return HttpServerResponse.raw(text, {
status: response.status,
contentType: response.headers.get("Content-Type") ?? "application/json",
headers: {
"Content-Type": response.headers.get("Content-Type") ?? "application/json",
},
})
})
@@ -75,6 +75,7 @@ const QueryNumberParameters = new Set(["start", "cursor", "limit", "method"])
const QueryBooleanParameters = new Set(["roots", "archived"])
const QueryParameterSchemas = {
"GET /find/file limit": { type: "integer", minimum: 1, maximum: 200 },
"GET /experimental/session worktrees": { type: "boolean" }, // kilocode_change
"GET /kilo/cloud-sessions cursor": { type: "string" }, // kilocode_change
"GET /session/{sessionID}/diff messageID": { type: "string", pattern: "^msg.*" },
"GET /session/{sessionID}/message limit": { type: "integer", minimum: 0, maximum: Number.MAX_SAFE_INTEGER },
@@ -523,6 +524,8 @@ function pathParameterSchema(route: string, name: string) {
if (name === "id" && route.startsWith("POST /experimental/workspace/")) return { type: "string", pattern: "^wrk.*" }
if (name === "requestID" && route.startsWith("POST /permission/")) return { type: "string", pattern: "^per.*" }
if (name === "requestID" && route.startsWith("POST /question/")) return { type: "string", pattern: "^que.*" }
// /network/* reuses QuestionID (prefix "que"), not a separate brand. // kilocode_change
if (name === "requestID" && route.startsWith("POST /network/")) return { type: "string", pattern: "^que.*" } // kilocode_change
return undefined
}
@@ -2,10 +2,12 @@ import { afterEach, describe, expect, test } from "bun:test"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Instance } from "../../src/project/instance"
import { ControlPaths } from "../../src/server/routes/instance/httpapi/groups/control"
import { ExperimentalPaths } from "../../src/server/routes/instance/httpapi/groups/experimental"
import { FilePaths } from "../../src/server/routes/instance/httpapi/groups/file"
import { GlobalPaths } from "../../src/server/routes/instance/httpapi/groups/global"
import { KiloGatewayPaths } from "../../src/kilocode/server/httpapi/groups/kilo-gateway"
import { PublicApi } from "../../src/server/routes/instance/httpapi/public"
import { SessionPaths } from "../../src/server/routes/instance/httpapi/groups/session"
import { ExperimentalHttpApiServer } from "../../src/server/routes/instance/httpapi/server"
import { Server } from "../../src/server/server"
import * as Log from "@opencode-ai/core/util/log"
@@ -222,13 +224,8 @@ describe("HttpApi server", () => {
expect(Server.backend()).toEqual({ backend: "effect-httpapi", reason: "env" })
})
// kilocode_change start - skip Effect HttpApi parity tests until Kilo overlay routes are migrated.
// These tests verify every Hono route has an Effect HttpApi contract. Kilo-specific routes
// (/config/warnings, /indexing/status, /kilo/claw/*, /kilo/cloud-sessions, /experimental/worktree/diff*)
// aren't yet wired into PublicApi. The Effect HttpApi bridge is gated behind KILO_EXPERIMENTAL_HTTPAPI
// and is not enabled in any production client (VS Code extension, JetBrains, TUI, desktop all use Hono).
// Follow-up: migrate Kilo overlay routes onto the Effect HttpApi bridge.
test.skip("covers every generated OpenAPI route with Effect HttpApi contracts", async () => {
// kilocode_change start - Effect HttpApi route parity.
test("covers every generated OpenAPI route with Effect HttpApi contracts", async () => {
const honoRoutes = openApiRouteKeys(await Server.openapiHono())
const effectRoutes = openApiRouteKeys(effectOpenApi())
@@ -237,13 +234,14 @@ describe("HttpApi server", () => {
"GET /api/session",
"GET /api/session/{sessionID}/context",
"GET /api/session/{sessionID}/message",
"GET /indexing/status",
"POST /api/session/{sessionID}/compact",
"POST /api/session/{sessionID}/prompt",
"POST /api/session/{sessionID}/wait",
])
})
test.skip("matches generated OpenAPI route parameters", async () => {
test("matches generated OpenAPI route parameters", async () => {
const hono = openApiParameters(await Server.openapiHono())
const effect = openApiParameters(effectOpenApi())
@@ -254,7 +252,7 @@ describe("HttpApi server", () => {
).toEqual([])
})
test.skip("matches generated OpenAPI request body shape", async () => {
test("matches generated OpenAPI request body shape", async () => {
const hono = openApiRequestBodies(await Server.openapiHono())
const effect = openApiRequestBodies(effectOpenApi())
@@ -270,17 +268,24 @@ describe("HttpApi server", () => {
test("Kilo overlay routes are mirrored in Hono and Effect specs", async () => {
const hono = new Set(openApiRouteKeys(await Server.openapiHono()))
const effect = new Set(openApiRouteKeys(effectOpenApi()))
// The 22 Kilo overlay paths ported to Effect HttpApi. Both backends must serve each.
// Kilo overlay paths ported to Effect HttpApi. Both backends must serve each.
const kilo = [
"POST /permission/allow-everything",
"POST /enhance-prompt",
"POST /commit-message",
`GET ${ExperimentalPaths.worktreeDiff}`,
`GET ${ExperimentalPaths.worktreeDiffFile}`,
`GET ${ExperimentalPaths.worktreeDiffSummary}`,
"GET /network",
"POST /network/{requestID}/reply",
"POST /network/{requestID}/reject",
`GET ${KiloGatewayPaths.modes}`,
`POST ${KiloGatewayPaths.fim}`,
`POST ${KiloGatewayPaths.audioTranscriptions}`,
"POST /remote/enable",
"POST /remote/disable",
"GET /remote/status",
`POST ${SessionPaths.viewed}`,
"POST /telemetry/capture",
"POST /telemetry/setEnabled",
"GET /suggestion",
+1 -1
View File
@@ -866,7 +866,7 @@ export class Session extends HeyApiClient {
directory?: string
workspace?: string
projectID?: string
worktrees?: "true" | "false"
worktrees?: boolean
roots?: boolean | "true" | "false"
start?: number
cursor?: number
+1 -1
View File
@@ -4186,7 +4186,7 @@ export type ExperimentalSessionListData = {
directory?: string
workspace?: string
projectID?: string
worktrees?: "true" | "false"
worktrees?: boolean
roots?: boolean | "true" | "false"
start?: number
cursor?: number
+5 -4
View File
@@ -1488,8 +1488,7 @@
"name": "worktrees",
"in": "query",
"schema": {
"type": "string",
"enum": ["true", "false"]
"type": "boolean"
},
"required": false
},
@@ -10564,7 +10563,8 @@
"name": "requestID",
"in": "path",
"schema": {
"type": "string"
"type": "string",
"pattern": "^que.*"
},
"required": true
}
@@ -10637,7 +10637,8 @@
"name": "requestID",
"in": "path",
"schema": {
"type": "string"
"type": "string",
"pattern": "^que.*"
},
"required": true
}