diff --git a/.gitignore b/.gitignore index fc175568df6..341eca2b082 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ tmp dist ts-dist .turbo +packages/*/generated **/.serena .serena/ /result diff --git a/bun.lock b/bun.lock index 08021803a09..dc5bbf09f48 100644 --- a/bun.lock +++ b/bun.lock @@ -14,7 +14,10 @@ "devDependencies": { "@actions/artifact": "5.0.1", "@tsconfig/bun": "catalog:", + "ajv": "8.17.1", + "ajv-formats": "3.0.1", "husky": "9.1.7", + "json-schema-to-zod": "2.7.0", "prettier": "3.6.2", "sst": "3.17.23", "turbo": "2.5.6", @@ -1901,7 +1904,9 @@ "ai": ["ai@5.0.97", "", { "dependencies": { "@ai-sdk/gateway": "2.0.12", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8zBx0b/owis4eJI2tAlV8a1Rv0BANmLxontcAelkLNwEHhgfgXeKpDkhNB6OgV+BJSwboIUDkgd9312DdJnCOQ=="], - "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], "ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="], @@ -2407,6 +2412,8 @@ "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + "fast-xml-parser": ["fast-xml-parser@4.4.1", "", { "dependencies": { "strnum": "^1.0.5" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw=="], "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], @@ -2781,7 +2788,9 @@ "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], - "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + "json-schema-to-zod": ["json-schema-to-zod@2.7.0", "", { "bin": { "json-schema-to-zod": "dist/cjs/cli.js" } }, "sha512-eW59l3NQ6sa3HcB+Ahf7pP6iGU7MY4we5JsPqXQ2ZcIPF8QxSg/lkY8lN0Js/AG0NjMbk+nZGUfHlceiHF+bwQ=="], + + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], @@ -3383,6 +3392,8 @@ "remeda": ["remeda@2.26.0", "", { "dependencies": { "type-fest": "^4.41.0" } }, "sha512-lmNNwtaC6Co4m0WTTNoZ/JlpjEqAjPZO0+czC9YVRQUpkbS4x8Hmh+Mn9HPfJfiXqUQ5IXXgSXSOB2pBKAytdA=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "reselect": ["reselect@4.1.8", "", {}, "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ=="], "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], @@ -4055,6 +4066,8 @@ "@jsx-email/doiuse-email/htmlparser2": ["htmlparser2@9.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.1.0", "entities": "^4.5.0" } }, "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ=="], + "@modelcontextprotocol/sdk/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + "@modelcontextprotocol/sdk/express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="], "@modelcontextprotocol/sdk/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], @@ -4599,6 +4612,8 @@ "@jsx-email/cli/vite/rollup": ["rollup@3.29.5", "", { "optionalDependencies": { "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w=="], + "@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + "@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], "@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="], diff --git a/package.json b/package.json index 66d2523652d..2e59b95d07f 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,8 @@ "scripts": { "dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts", "typecheck": "bun turbo typecheck", + "generate": "bun ./script/generate.ts", + "generate:schemas": "bun ./script/generate-from-schemas.ts", "prepare": "husky", "random": "echo 'Random script'", "hello": "echo 'Hello World!'", @@ -62,7 +64,10 @@ "devDependencies": { "@actions/artifact": "5.0.1", "@tsconfig/bun": "catalog:", + "ajv": "8.17.1", + "ajv-formats": "3.0.1", "husky": "9.1.7", + "json-schema-to-zod": "2.7.0", "prettier": "3.6.2", "sst": "3.17.23", "turbo": "2.5.6" diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index c683727dfa3..4243be21e09 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -13,32 +13,13 @@ import PROMPT_TITLE from "./prompt/title.txt" import { PermissionNext } from "@/permission/next" import { mergeDeep, pipe, sortBy, values } from "remeda" +// Generated from JSON Schema - see schema/agentInfo.schema.json +import { agentInfoSchema, type AgentInfo } from "@generated/validators/agentInfo" + export namespace Agent { - export const Info = z - .object({ - name: z.string(), - description: z.string().optional(), - mode: z.enum(["subagent", "primary", "all"]), - native: z.boolean().optional(), - hidden: z.boolean().optional(), - topP: z.number().optional(), - temperature: z.number().optional(), - color: z.string().optional(), - permission: PermissionNext.Ruleset, - model: z - .object({ - modelID: z.string(), - providerID: z.string(), - }) - .optional(), - prompt: z.string().optional(), - options: z.record(z.string(), z.any()), - steps: z.number().int().positive().optional(), - }) - .meta({ - ref: "Agent", - }) - export type Info = z.infer + // Generated from JSON Schema - see schema/agentInfo.schema.json + export const Info = agentInfoSchema + export type Info = AgentInfo const state = Instance.state(async () => { const cfg = await Config.get() diff --git a/packages/opencode/src/auth/index.ts b/packages/opencode/src/auth/index.ts index b9c8a78caf9..f4d4ab9d8bf 100644 --- a/packages/opencode/src/auth/index.ts +++ b/packages/opencode/src/auth/index.ts @@ -1,36 +1,27 @@ import path from "path" import { Global } from "../global" import fs from "fs/promises" -import z from "zod" +import { authSchema, type Auth as AuthType } from "@generated/validators/auth" +import { oauthSchema, type Oauth as OauthType } from "@generated/validators/oauth" +import { apiAuthSchema, type ApiAuth as ApiAuthType } from "@generated/validators/apiAuth" +import { wellKnownAuthSchema, type WellKnownAuth as WellKnownAuthType } from "@generated/validators/wellKnownAuth" export namespace Auth { - export const Oauth = z - .object({ - type: z.literal("oauth"), - refresh: z.string(), - access: z.string(), - expires: z.number(), - enterpriseUrl: z.string().optional(), - }) - .meta({ ref: "OAuth" }) + // Generated from JSON Schema - see schema/oauth.schema.json + export const Oauth = oauthSchema + export type Oauth = OauthType - export const Api = z - .object({ - type: z.literal("api"), - key: z.string(), - }) - .meta({ ref: "ApiAuth" }) + // Generated from JSON Schema - see schema/apiAuth.schema.json + export const Api = apiAuthSchema + export type Api = ApiAuthType - export const WellKnown = z - .object({ - type: z.literal("wellknown"), - key: z.string(), - token: z.string(), - }) - .meta({ ref: "WellKnownAuth" }) + // Generated from JSON Schema - see schema/wellKnownAuth.schema.json + export const WellKnown = wellKnownAuthSchema + export type WellKnown = WellKnownAuthType - export const Info = z.discriminatedUnion("type", [Oauth, Api, WellKnown]).meta({ ref: "Auth" }) - export type Info = z.infer + // Generated from JSON Schema - see schema/auth.schema.json + export const Info = authSchema + export type Info = AuthType const filepath = path.join(Global.Path.data, "auth.json") diff --git a/packages/opencode/src/permission/next.ts b/packages/opencode/src/permission/next.ts index 9a0395fa1ec..8f5946d1fe7 100644 --- a/packages/opencode/src/permission/next.ts +++ b/packages/opencode/src/permission/next.ts @@ -9,29 +9,27 @@ import { Log } from "@/util/log" import { Wildcard } from "@/util/wildcard" import z from "zod" +// Generated from JSON Schema - see schema/*.schema.json +import { permissionActionSchema, type PermissionAction } from "@generated/validators/permissionAction" +import { permissionReplySchema, type PermissionReply } from "@generated/validators/permissionReply" +import { permissionRequestSchema, type PermissionRequest } from "@generated/validators/permissionRequest" +import { permissionRuleSchema, type PermissionRule } from "@generated/validators/permissionRule" +import { permissionRulesetSchema, type PermissionRuleset } from "@generated/validators/permissionRuleset" + export namespace PermissionNext { const log = Log.create({ service: "permission" }) - export const Action = z.enum(["allow", "deny", "ask"]).meta({ - ref: "PermissionAction", - }) - export type Action = z.infer + // Generated from JSON Schema - see schema/permissionAction.schema.json + export const Action = permissionActionSchema + export type Action = PermissionAction - export const Rule = z - .object({ - permission: z.string(), - pattern: z.string(), - action: Action, - }) - .meta({ - ref: "PermissionRule", - }) - export type Rule = z.infer + // Generated from JSON Schema - see schema/permissionRule.schema.json + export const Rule = permissionRuleSchema + export type Rule = PermissionRule - export const Ruleset = Rule.array().meta({ - ref: "PermissionRuleset", - }) - export type Ruleset = z.infer + // Generated from JSON Schema - see schema/permissionRuleset.schema.json + export const Ruleset = permissionRulesetSchema + export type Ruleset = PermissionRuleset export function fromConfig(permission: Config.Permission) { const ruleset: Ruleset = [] @@ -53,29 +51,13 @@ export namespace PermissionNext { return rulesets.flat() } - export const Request = z - .object({ - id: Identifier.schema("permission"), - sessionID: Identifier.schema("session"), - permission: z.string(), - patterns: z.string().array(), - metadata: z.record(z.string(), z.any()), - always: z.string().array(), - tool: z - .object({ - messageID: z.string(), - callID: z.string(), - }) - .optional(), - }) - .meta({ - ref: "PermissionRequest", - }) - - export type Request = z.infer + // Generated from JSON Schema - see schema/permissionRequest.schema.json + export const Request = permissionRequestSchema + export type Request = PermissionRequest - export const Reply = z.enum(["once", "always", "reject"]) - export type Reply = z.infer + // Generated from JSON Schema - see schema/permissionReply.schema.json + export const Reply = permissionReplySchema + export type Reply = PermissionReply export const Approval = z.object({ projectID: z.string(), diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index cd0a80c2c48..f879615c58e 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -13,6 +13,8 @@ import { Env } from "../env" import { Instance } from "../project/instance" import { Flag } from "../flag/flag" import { iife } from "@/util/iife" +import { modelInfoSchema, type ModelInfo } from "@generated/validators/modelInfo" +import { providerInfoSchema, type ProviderInfo } from "@generated/validators/providerInfo" // Direct imports for bundled providers import { createAmazonBedrock, type AmazonBedrockProviderSettings } from "@ai-sdk/amazon-bedrock" @@ -429,90 +431,13 @@ export namespace Provider { }, } - export const Model = z - .object({ - id: z.string(), - providerID: z.string(), - api: z.object({ - id: z.string(), - url: z.string(), - npm: z.string(), - }), - name: z.string(), - family: z.string().optional(), - capabilities: z.object({ - temperature: z.boolean(), - reasoning: z.boolean(), - attachment: z.boolean(), - toolcall: z.boolean(), - input: z.object({ - text: z.boolean(), - audio: z.boolean(), - image: z.boolean(), - video: z.boolean(), - pdf: z.boolean(), - }), - output: z.object({ - text: z.boolean(), - audio: z.boolean(), - image: z.boolean(), - video: z.boolean(), - pdf: z.boolean(), - }), - interleaved: z.union([ - z.boolean(), - z.object({ - field: z.enum(["reasoning_content", "reasoning_details"]), - }), - ]), - }), - cost: z.object({ - input: z.number(), - output: z.number(), - cache: z.object({ - read: z.number(), - write: z.number(), - }), - experimentalOver200K: z - .object({ - input: z.number(), - output: z.number(), - cache: z.object({ - read: z.number(), - write: z.number(), - }), - }) - .optional(), - }), - limit: z.object({ - context: z.number(), - output: z.number(), - }), - status: z.enum(["alpha", "beta", "deprecated", "active"]), - options: z.record(z.string(), z.any()), - headers: z.record(z.string(), z.string()), - release_date: z.string(), - variants: z.record(z.string(), z.record(z.string(), z.any())).optional(), - }) - .meta({ - ref: "Model", - }) - export type Model = z.infer - - export const Info = z - .object({ - id: z.string(), - name: z.string(), - source: z.enum(["env", "config", "custom", "api"]), - env: z.string().array(), - key: z.string().optional(), - options: z.record(z.string(), z.any()), - models: z.record(z.string(), Model), - }) - .meta({ - ref: "Provider", - }) - export type Info = z.infer + // Generated from JSON Schema - see schema/modelInfo.schema.json + export const Model = modelInfoSchema + export type Model = ModelInfo + + // Generated from JSON Schema - see schema/providerInfo.schema.json + export const Info = providerInfoSchema + export type Info = ProviderInfo function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model { const m: Model = { diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 04ec4673ec4..1373b0222e5 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -2,6 +2,10 @@ import { BusEvent } from "@/bus/bus-event" import { Bus } from "@/bus" import { GlobalBus } from "@/bus/global" import { Log } from "../util/log" + +// Generated from JSON Schema - see schema/event.schema.json and schema/globalEvent.schema.json +import { eventSchema } from "@generated/validators/event" +import { globalEventSchema } from "@generated/validators/globalEvent" import { describeRoute, generateSpecs, validator, resolver, openAPIRouteHandler } from "hono-openapi" import { Hono } from "hono" import { cors } from "hono/cors" @@ -160,16 +164,8 @@ export namespace Server { description: "Event stream", content: { "text/event-stream": { - schema: resolver( - z - .object({ - directory: z.string(), - payload: BusEvent.payloads(), - }) - .meta({ - ref: "GlobalEvent", - }), - ), + // Generated from JSON Schema - see schema/globalEvent.schema.json + schema: resolver(globalEventSchema), }, }, }, @@ -2757,7 +2753,8 @@ export namespace Server { description: "Event stream", content: { "text/event-stream": { - schema: resolver(BusEvent.payloads()), + // Generated from JSON Schema - see schema/event.schema.json + schema: resolver(eventSchema), }, }, }, diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 0776590d6a9..1f8da6c8bc5 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -20,6 +20,9 @@ import { Snapshot } from "@/snapshot" import type { Provider } from "@/provider/provider" import { PermissionNext } from "@/permission/next" +// Generated from JSON Schema - see schema/sessionInfo.schema.json +import { sessionInfoSchema, type SessionInfo } from "@generated/validators/sessionInfo" + export namespace Session { const log = Log.create({ service: "session" }) @@ -36,47 +39,9 @@ export namespace Session { ).test(title) } - export const Info = z - .object({ - id: Identifier.schema("session"), - projectID: z.string(), - directory: z.string(), - parentID: Identifier.schema("session").optional(), - summary: z - .object({ - additions: z.number(), - deletions: z.number(), - files: z.number(), - diffs: Snapshot.FileDiff.array().optional(), - }) - .optional(), - share: z - .object({ - url: z.string(), - }) - .optional(), - title: z.string(), - version: z.string(), - time: z.object({ - created: z.number(), - updated: z.number(), - compacting: z.number().optional(), - archived: z.number().optional(), - }), - permission: PermissionNext.Ruleset.optional(), - revert: z - .object({ - messageID: z.string(), - partID: z.string().optional(), - snapshot: z.string().optional(), - diff: z.string().optional(), - }) - .optional(), - }) - .meta({ - ref: "Session", - }) - export type Info = z.output + // Generated from JSON Schema - see schema/sessionInfo.schema.json + export const Info = sessionInfoSchema + export type Info = SessionInfo export const ShareInfo = z .object({ diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 2dff17a5efa..2bf7d65275a 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -12,6 +12,39 @@ import { STATUS_CODES } from "http" import { iife } from "@/util/iife" import { type SystemError } from "bun" +// Generated validators from JSON Schema - see schema/*.schema.json +import { + toolStatePendingSchema, + type ToolStatePending as GeneratedToolStatePending, +} from "@generated/validators/toolStatePending" +import { + toolStateRunningSchema, + type ToolStateRunning as GeneratedToolStateRunning, +} from "@generated/validators/toolStateRunning" +import { + toolStateCompletedSchema, + type ToolStateCompleted as GeneratedToolStateCompleted, +} from "@generated/validators/toolStateCompleted" +import { + toolStateErrorSchema, + type ToolStateError as GeneratedToolStateError, +} from "@generated/validators/toolStateError" +import { toolStateSchema, type ToolState as GeneratedToolState } from "@generated/validators/toolState" +import { toolPartSchema, type ToolPart as GeneratedToolPart } from "@generated/validators/toolPart" +import { textPartSchema, type TextPart as GeneratedTextPart } from "@generated/validators/textPart" +import { reasoningPartSchema, type ReasoningPart as GeneratedReasoningPart } from "@generated/validators/reasoningPart" +import { snapshotPartSchema, type SnapshotPart as GeneratedSnapshotPart } from "@generated/validators/snapshotPart" +import { patchPartSchema, type PatchPart as GeneratedPatchPart } from "@generated/validators/patchPart" +import { agentPartSchema, type AgentPart as GeneratedAgentPart } from "@generated/validators/agentPart" +import { compactionPartSchema, type CompactionPart as GeneratedCompactionPart } from "@generated/validators/compactionPart" +import { subtaskPartSchema, type SubtaskPart as GeneratedSubtaskPart } from "@generated/validators/subtaskPart" +import { stepStartPartSchema, type StepStartPart as GeneratedStepStartPart } from "@generated/validators/stepStartPart" +import { stepFinishPartSchema, type StepFinishPart as GeneratedStepFinishPart } from "@generated/validators/stepFinishPart" +import { partSchema, type Part as GeneratedPart } from "@generated/validators/part" +import { userMessageSchema, type UserMessage as GeneratedUserMessage } from "@generated/validators/userMessage" +import { assistantMessageSchema, type AssistantMessage as GeneratedAssistantMessage } from "@generated/validators/assistantMessage" +import { messageSchema, type Message as GeneratedMessage } from "@generated/validators/message" + export namespace MessageV2 { export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({})) export const AbortedError = NamedError.create("MessageAbortedError", z.object({ message: z.string() })) @@ -35,59 +68,32 @@ export namespace MessageV2 { ) export type APIError = z.infer + // Note: PartBase is kept for RetryPart which needs to reference APIError.Schema + // Other parts use generated validators from JSON Schema const PartBase = z.object({ id: z.string(), sessionID: z.string(), messageID: z.string(), }) - export const SnapshotPart = PartBase.extend({ - type: z.literal("snapshot"), - snapshot: z.string(), - }).meta({ - ref: "SnapshotPart", - }) - export type SnapshotPart = z.infer + // Generated from JSON Schema - see schema/snapshotPart.schema.json + export const SnapshotPart = snapshotPartSchema + export type SnapshotPart = GeneratedSnapshotPart - export const PatchPart = PartBase.extend({ - type: z.literal("patch"), - hash: z.string(), - files: z.string().array(), - }).meta({ - ref: "PatchPart", - }) - export type PatchPart = z.infer - - export const TextPart = PartBase.extend({ - type: z.literal("text"), - text: z.string(), - synthetic: z.boolean().optional(), - ignored: z.boolean().optional(), - time: z - .object({ - start: z.number(), - end: z.number().optional(), - }) - .optional(), - metadata: z.record(z.string(), z.any()).optional(), - }).meta({ - ref: "TextPart", - }) - export type TextPart = z.infer + // Generated from JSON Schema - see schema/patchPart.schema.json + export const PatchPart = patchPartSchema + export type PatchPart = GeneratedPatchPart - export const ReasoningPart = PartBase.extend({ - type: z.literal("reasoning"), - text: z.string(), - metadata: z.record(z.string(), z.any()).optional(), - time: z.object({ - start: z.number(), - end: z.number().optional(), - }), - }).meta({ - ref: "ReasoningPart", - }) - export type ReasoningPart = z.infer + // Generated from JSON Schema - see schema/textPart.schema.json + export const TextPart = textPartSchema + export type TextPart = GeneratedTextPart + // Generated from JSON Schema - see schema/reasoningPart.schema.json + export const ReasoningPart = reasoningPartSchema + export type ReasoningPart = GeneratedReasoningPart + + // FilePartSource types are kept inline because SymbolSource references LSP.Range + // which is an external dependency not captured in JSON Schema const FilePartSourceBase = z.object({ text: z .object({ @@ -129,6 +135,7 @@ export namespace MessageV2 { ref: "FilePartSource", }) + // FilePart is kept inline because it references FilePartSource which uses LSP.Range export const FilePart = PartBase.extend({ type: z.literal("file"), mime: z.string(), @@ -140,38 +147,22 @@ export namespace MessageV2 { }) export type FilePart = z.infer - export const AgentPart = PartBase.extend({ - type: z.literal("agent"), - name: z.string(), - source: z - .object({ - value: z.string(), - start: z.number().int(), - end: z.number().int(), - }) - .optional(), - }).meta({ - ref: "AgentPart", - }) - export type AgentPart = z.infer + // Generated from JSON Schema - see schema/agentPart.schema.json + export const AgentPart = agentPartSchema + export type AgentPart = GeneratedAgentPart - export const CompactionPart = PartBase.extend({ - type: z.literal("compaction"), - auto: z.boolean(), - }).meta({ - ref: "CompactionPart", - }) - export type CompactionPart = z.infer + // Generated from JSON Schema - see schema/compactionPart.schema.json + export const CompactionPart = compactionPartSchema + export type CompactionPart = GeneratedCompactionPart - export const SubtaskPart = PartBase.extend({ - type: z.literal("subtask"), - prompt: z.string(), - description: z.string(), - agent: z.string(), - command: z.string().optional(), - }) - export type SubtaskPart = z.infer + // Generated from JSON Schema - see schema/subtaskPart.schema.json + export const SubtaskPart = subtaskPartSchema + export type SubtaskPart = GeneratedSubtaskPart + // Note: RetryPart uses APIError.Schema which is a NamedError class. + // The retryPartSchema references apiErrorSchema which has the same shape. + // We keep the inline definition to maintain the APIError.Schema reference + // for runtime error handling compatibility. export const RetryPart = PartBase.extend({ type: z.literal("retry"), attempt: z.number(), @@ -184,111 +175,37 @@ export namespace MessageV2 { }) export type RetryPart = z.infer - export const StepStartPart = PartBase.extend({ - type: z.literal("step-start"), - snapshot: z.string().optional(), - }).meta({ - ref: "StepStartPart", - }) - export type StepStartPart = z.infer + // Generated from JSON Schema - see schema/stepStartPart.schema.json + export const StepStartPart = stepStartPartSchema + export type StepStartPart = GeneratedStepStartPart - export const StepFinishPart = PartBase.extend({ - type: z.literal("step-finish"), - reason: z.string(), - snapshot: z.string().optional(), - cost: z.number(), - tokens: z.object({ - input: z.number(), - output: z.number(), - reasoning: z.number(), - cache: z.object({ - read: z.number(), - write: z.number(), - }), - }), - }).meta({ - ref: "StepFinishPart", - }) - export type StepFinishPart = z.infer + // Generated from JSON Schema - see schema/stepFinishPart.schema.json + export const StepFinishPart = stepFinishPartSchema + export type StepFinishPart = GeneratedStepFinishPart - export const ToolStatePending = z - .object({ - status: z.literal("pending"), - input: z.record(z.string(), z.any()), - raw: z.string(), - }) - .meta({ - ref: "ToolStatePending", - }) + // Generated from JSON Schema - see schema/toolStatePending.schema.json + export const ToolStatePending = toolStatePendingSchema + export type ToolStatePending = GeneratedToolStatePending - export type ToolStatePending = z.infer + // Generated from JSON Schema - see schema/toolStateRunning.schema.json + export const ToolStateRunning = toolStateRunningSchema + export type ToolStateRunning = GeneratedToolStateRunning - export const ToolStateRunning = z - .object({ - status: z.literal("running"), - input: z.record(z.string(), z.any()), - title: z.string().optional(), - metadata: z.record(z.string(), z.any()).optional(), - time: z.object({ - start: z.number(), - }), - }) - .meta({ - ref: "ToolStateRunning", - }) - export type ToolStateRunning = z.infer - - export const ToolStateCompleted = z - .object({ - status: z.literal("completed"), - input: z.record(z.string(), z.any()), - output: z.string(), - title: z.string(), - metadata: z.record(z.string(), z.any()), - time: z.object({ - start: z.number(), - end: z.number(), - compacted: z.number().optional(), - }), - attachments: FilePart.array().optional(), - }) - .meta({ - ref: "ToolStateCompleted", - }) - export type ToolStateCompleted = z.infer - - export const ToolStateError = z - .object({ - status: z.literal("error"), - input: z.record(z.string(), z.any()), - error: z.string(), - metadata: z.record(z.string(), z.any()).optional(), - time: z.object({ - start: z.number(), - end: z.number(), - }), - }) - .meta({ - ref: "ToolStateError", - }) - export type ToolStateError = z.infer + // Generated from JSON Schema - see schema/toolStateCompleted.schema.json + export const ToolStateCompleted = toolStateCompletedSchema + export type ToolStateCompleted = GeneratedToolStateCompleted - export const ToolState = z - .discriminatedUnion("status", [ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError]) - .meta({ - ref: "ToolState", - }) + // Generated from JSON Schema - see schema/toolStateError.schema.json + export const ToolStateError = toolStateErrorSchema + export type ToolStateError = GeneratedToolStateError - export const ToolPart = PartBase.extend({ - type: z.literal("tool"), - callID: z.string(), - tool: z.string(), - state: ToolState, - metadata: z.record(z.string(), z.any()).optional(), - }).meta({ - ref: "ToolPart", - }) - export type ToolPart = z.infer + // Generated from JSON Schema - see schema/toolState.schema.json + export const ToolState = toolStateSchema + export type ToolState = GeneratedToolState + + // Generated from JSON Schema - see schema/toolPart.schema.json + export const ToolPart = toolPartSchema + export type ToolPart = GeneratedToolPart const Base = z.object({ id: z.string(), diff --git a/packages/opencode/src/session/status.ts b/packages/opencode/src/session/status.ts index 1db03b5db0d..07fd501ab9f 100644 --- a/packages/opencode/src/session/status.ts +++ b/packages/opencode/src/session/status.ts @@ -3,26 +3,13 @@ import { Bus } from "@/bus" import { Instance } from "@/project/instance" import z from "zod" +// Generated from JSON Schema - see schema/sessionStatus.schema.json +import { sessionStatusSchema, type SessionStatus as GeneratedSessionStatus } from "@generated/validators/sessionStatus" + export namespace SessionStatus { - export const Info = z - .union([ - z.object({ - type: z.literal("idle"), - }), - z.object({ - type: z.literal("retry"), - attempt: z.number(), - message: z.string(), - next: z.number(), - }), - z.object({ - type: z.literal("busy"), - }), - ]) - .meta({ - ref: "SessionStatus", - }) - export type Info = z.infer + // Generated from JSON Schema - see schema/sessionStatus.schema.json + export const Info = sessionStatusSchema + export type Info = GeneratedSessionStatus export const Event = { Status: BusEvent.define( diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 0bbb1115e61..e2a6134a256 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -7,6 +7,9 @@ import z from "zod" import { Config } from "../config/config" import { Instance } from "../project/instance" +// Generated from JSON Schema - see schema/fileDiff.schema.json +import { fileDiffSchema, type FileDiff as FileDiffType } from "@generated/validators/fileDiff" + export namespace Snapshot { const log = Log.create({ service: "snapshot" }) @@ -144,18 +147,9 @@ export namespace Snapshot { return result.text().trim() } - export const FileDiff = z - .object({ - file: z.string(), - before: z.string(), - after: z.string(), - additions: z.number(), - deletions: z.number(), - }) - .meta({ - ref: "FileDiff", - }) - export type FileDiff = z.infer + // Generated from JSON Schema - see schema/fileDiff.schema.json + export const FileDiff = fileDiffSchema + export type FileDiff = FileDiffType export async function diffFull(from: string, to: string): Promise { const git = gitdir() const result: FileDiff[] = [] diff --git a/packages/opencode/tsconfig.json b/packages/opencode/tsconfig.json index 9067d84fd6a..7f15a9e8f0b 100644 --- a/packages/opencode/tsconfig.json +++ b/packages/opencode/tsconfig.json @@ -10,7 +10,12 @@ "customConditions": ["browser"], "paths": { "@/*": ["./src/*"], - "@tui/*": ["./src/cli/cmd/tui/*"] + "@tui/*": ["./src/cli/cmd/tui/*"], + "@generated/*": ["./generated/*"] } - } + }, + "include": [ + "src/**/*", + "generated/**/*" + ] } diff --git a/schema/README.md b/schema/README.md new file mode 100644 index 00000000000..39e40cddc53 --- /dev/null +++ b/schema/README.md @@ -0,0 +1,320 @@ +# OpenCode JSON Schemas + +This directory contains JSON Schema definitions for OpenCode's Provider and Model APIs. These schemas serve as the **canonical source of truth** for API contracts, enabling type-safe client generation across multiple languages. + +## Overview + +OpenCode uses JSON Schema (draft-07) to define its API types, following the pattern used by specifications like Khronos glTF, OpenAPI, and Kubernetes. This approach provides: + +- **Language-agnostic contracts** - Generate type-safe bindings for TypeScript, Rust, Python, C#, etc. +- **Formal specification** - Machine-readable API documentation +- **Versioning support** - Track breaking changes and schema evolution +- **Validation** - Ensure data correctness at runtime + +## Schema Files + +The following 21 schemas define the complete Provider and Model API: + +### Core Schemas +- `providerInfo.schema.json` - Provider metadata and configuration +- `modelInfo.schema.json` - Complete model metadata with capabilities, pricing, and limits + +### Model Components +- `modelAPI.schema.json` - API endpoint configuration +- `modelCapabilities.schema.json` - Feature flags (temperature, reasoning, tool calls, etc.) +- `modelCost.schema.json` - Pricing per token with cache costs +- `modelLimits.schema.json` - Context window and output limits +- `modelStatus.schema.json` - Lifecycle status (alpha, beta, active, deprecated) +- `modelOptions.schema.json` - Runtime configuration options +- `modelSelection.schema.json` - Model selection preferences + +### Provider Components +- `providerSource.schema.json` - Provider origin (env, config, custom, API) +- `providerOptions.schema.json` - Provider-specific options +- `providerList.schema.json` - Collection of providers + +### Options & Configuration +- `anthropicOptions.schema.json` - Anthropic-specific options +- `googleOptions.schema.json` - Google-specific options +- `openaiOptions.schema.json` - OpenAI-specific options +- `universalOptions.schema.json` - Universal options (all providers) +- `thinkingConfig.schema.json` - Thinking/reasoning configuration +- `thinkingOptions.schema.json` - Thinking budget and effort settings + +### Supporting Schemas +- `ioCapabilities.schema.json` - Input/output modality capabilities +- `cacheCost.schema.json` - Cache read/write pricing +- `experimentalPricing.schema.json` - Over-200K token pricing + +## Workflow + +### Editing Schemas + +1. Edit JSON Schema files directly in this directory +2. Validate schemas: `bun run generate:schemas` +3. Review generated TypeScript validators in `packages/opencode/generated/validators/` +4. Run tests: `bun run typecheck` and `bun test` + +### Code Generation + +The build system automatically generates TypeScript types and Zod validators from these schemas: + +```bash +# Generate validators only +bun run generate:schemas + +# Full generation (schemas + SDK + OpenAPI) +bun run generate +``` + +Generated files are located at: +- `packages/opencode/generated/validators/*.ts` - Zod validators and TypeScript types + +**Note:** Generated files are gitignored - they're build artifacts, not source code. + +### Build Integration + +Schema generation is integrated with Turborepo: + +```bash +# Typecheck (runs schema generation first) +bun run typecheck + +# Build (includes schema generation) +bun turbo build + +# Test (includes schema generation) +bun turbo opencode#test +``` + +## Schema Conventions + +### Structure + +All schemas follow these conventions: + +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/.json", + "$comment": "Source: ", + "title": "", + "description": "", + "type": "object", + "required": ["field1", "field2"], + "properties": { + "field1": { + "type": "string", + "description": "Field description", + "examples": ["example1", "example2"] + } + } +} +``` + +### Cross-References + +Use relative file paths for `$ref`: + +```json +{ + "api": { + "$ref": "modelAPI.schema.json" + } +} +``` + +### Custom Extensions + +Use `x-` prefix for custom metadata: + +```json +{ + "x-design-notes": { + "key-concept": "Explanation of design decision" + } +} +``` + +## Validation + +Schemas are validated using AJV with these rules: + +- All schemas must be valid JSON Schema draft-07 +- All `$ref` references must resolve +- All `$id` URLs must follow the pattern: `https://opencode.ai/schemas/v1/.json` +- Custom `x-` extensions are allowed +- Examples must validate against their schemas + +## VSCode Support + +Add to `.vscode/settings.json` for auto-complete and validation: + +```json +{ + "json.schemas": [ + { + "fileMatch": ["schema/*.schema.json"], + "url": "http://json-schema.org/draft-07/schema#" + } + ] +} +``` + +## External Client Generation + +### Rust + +Using [typify](https://github.com/oxidecomputer/typify) (recommended for complex schemas): + +```bash +cargo add typify schemars serde +``` + +```rust +// build.rs +use std::fs; +use typify::{TypeSpace, TypeSpaceSettings}; + +fn main() { + let schema_dir = "path/to/opencode/schema"; + let mut type_space = TypeSpace::new(TypeSpaceSettings::default()); + + // Add core schemas + let model_info = fs::read_to_string(format!("{}/modelInfo.schema.json", schema_dir)).unwrap(); + let schema: schemars::schema::RootSchema = serde_json::from_str(&model_info).unwrap(); + type_space.add_root_schema(schema).unwrap(); + + // Generate Rust types + let contents = type_space.to_string(); + fs::write("src/generated/types.rs", contents).unwrap(); +} +``` + +Using [schemafy](https://github.com/Marwes/schemafy) (simpler, macro-based): + +```rust +use schemafy_lib::Expander; + +schemafy::schemafy!( + "schema/modelInfo.schema.json" +); + +// Now you have ModelInfo, ModelCapabilities, etc. as Rust structs +``` + +### Python + +Using [datamodel-code-generator](https://github.com/koxudaxi/datamodel-code-generator): + +```bash +pip install datamodel-code-generator + +# Generate Pydantic v2 models from a single schema +datamodel-codegen \ + --input schema/modelInfo.schema.json \ + --output opencode_types/model_info.py \ + --output-model-type pydantic_v2.BaseModel + +# Generate from all schemas +for schema in schema/*.schema.json; do + name=$(basename "$schema" .schema.json) + datamodel-codegen \ + --input "$schema" \ + --output "opencode_types/${name}.py" \ + --output-model-type pydantic_v2.BaseModel +done +``` + +Example generated code: + +```python +from pydantic import BaseModel +from typing import Optional + +class ModelInfo(BaseModel): + id: str + provider_id: str + name: str + family: Optional[str] = None + # ... etc +``` + +### C# (Blazor, .NET) + +Using [NJsonSchema](https://github.com/RicoSuter/NJsonSchema): + +```bash +dotnet add package NJsonSchema.CodeGeneration.CSharp +``` + +```csharp +using NJsonSchema; +using NJsonSchema.CodeGeneration.CSharp; + +// Generate C# classes from schema +var schema = await JsonSchema.FromFileAsync("schema/modelInfo.schema.json"); +var settings = new CSharpGeneratorSettings +{ + Namespace = "OpenCode.Models", + GenerateDataAnnotations = true +}; +var generator = new CSharpGenerator(schema, settings); +var code = generator.GenerateFile(); + +// Write to file +File.WriteAllText("Generated/ModelInfo.cs", code); +``` + +Or use the CLI tool: + +```bash +dotnet tool install -g NJsonSchema.CodeGeneration.CSharp +njs2cs schema/modelInfo.schema.json -o Generated/ModelInfo.cs -n OpenCode.Models +``` + +## Versioning + +Schemas use semantic versioning: + +- **Major version** (`v1`, `v2`) - Breaking changes (field removal, type change, required additions) +- **Minor version** - Non-breaking additions (new optional fields) +- **Patch version** - Documentation updates, examples, clarifications + +Current version: `v1` (all schemas) + +## Contributing + +When modifying schemas: + +1. **Non-breaking changes** are preferred (add optional fields, not required ones) +2. **Breaking changes** require discussion and migration plan +3. **Document changes** in schema `$comment` field +4. **Update examples** to reflect new fields +5. **Run validation** before committing + +## FAQ + +**Q: Why JSON Schema instead of TypeScript types?** +A: JSON Schema is language-agnostic. TypeScript types can't be consumed by Rust, Python, or C# clients. With JSON Schema, all languages generate from the same source of truth. + +**Q: Why are generated files gitignored?** +A: They're build artifacts, like `dist/`. This keeps git history clean and avoids merge conflicts. Turborepo caching ensures fast regeneration. + +**Q: Can I modify generated files?** +A: No. Edit the JSON Schemas, then regenerate. Generated files are overwritten on every build. + +**Q: How do I add a new schema?** +A: Create `.schema.json` in this directory, then run `bun run generate:schemas`. The generator will automatically include it. + +## References + +- [JSON Schema Specification](https://json-schema.org/draft-07/schema) +- [Understanding JSON Schema](https://json-schema.org/understanding-json-schema/) +- [JSON Schema Validator](https://www.jsonschemavalidator.net/) +- [Khronos glTF Specification](https://github.com/KhronosGroup/glTF/tree/main/specification) - Reference example + +--- + +**Last Updated:** 2026-01-04 +**Schema Version:** v1 diff --git a/schema/abortedError.schema.json b/schema/abortedError.schema.json new file mode 100644 index 00000000000..bb548725b60 --- /dev/null +++ b/schema/abortedError.schema.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/abortedError.json", + "$comment": "Source: packages/opencode/src/session/message-v2.ts @ current line 37 (via NamedError.create)", + "title": "MessageAbortedError", + "description": "Message aborted error (wrapped in NamedError format)", + "type": "object", + "required": ["name", "data"], + "properties": { + "name": { + "type": "string", + "const": "MessageAbortedError", + "description": "Error type name" + }, + "data": { + "type": "object", + "required": ["message"], + "properties": { + "message": { + "type": "string", + "description": "Abort reason message" + } + } + } + } +} diff --git a/schema/agentInfo.schema.json b/schema/agentInfo.schema.json new file mode 100644 index 00000000000..cab38d07bf2 --- /dev/null +++ b/schema/agentInfo.schema.json @@ -0,0 +1,66 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/agentInfo.json", + "$comment": "Source: packages/opencode/src/agent/agent.ts @ a14f9d216 lines 17-41", + "title": "AgentInfo", + "description": "Agent configuration and metadata", + "type": "object", + "required": ["name", "mode", "permission", "options"], + "properties": { + "name": { + "type": "string", + "description": "Agent name identifier" + }, + "description": { + "type": "string", + "description": "Human-readable description of the agent" + }, + "mode": { + "type": "string", + "enum": ["subagent", "primary", "all"], + "description": "Agent mode: subagent (spawned by other agents), primary (direct interaction), or all" + }, + "native": { + "type": "boolean", + "description": "Whether this is a built-in native agent" + }, + "hidden": { + "type": "boolean", + "description": "Whether to hide this agent from UI listings" + }, + "topP": { + "type": "number", + "description": "Top-p sampling parameter for model inference" + }, + "temperature": { + "type": "number", + "description": "Temperature parameter for model inference" + }, + "color": { + "type": "string", + "description": "Hex color for UI differentiation" + }, + "permission": { + "$ref": "permissionRuleset.schema.json", + "description": "Permission ruleset for the agent" + }, + "model": { + "$ref": "agentModel.schema.json", + "description": "Default model selection for the agent" + }, + "prompt": { + "type": "string", + "description": "Custom system prompt for the agent" + }, + "options": { + "type": "object", + "additionalProperties": true, + "description": "Additional agent-specific options" + }, + "steps": { + "type": "integer", + "minimum": 1, + "description": "Maximum number of inference steps" + } + } +} diff --git a/schema/agentModel.schema.json b/schema/agentModel.schema.json new file mode 100644 index 00000000000..d0af9385049 --- /dev/null +++ b/schema/agentModel.schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/agentModel.json", + "$comment": "Source: packages/opencode/src/agent/agent.ts @ a14f9d216 lines 28-31", + "title": "AgentModel", + "description": "Model selection for an agent", + "type": "object", + "required": ["modelID", "providerID"], + "properties": { + "modelID": { + "type": "string", + "description": "Model identifier" + }, + "providerID": { + "type": "string", + "description": "Provider identifier" + } + } +} diff --git a/schema/agentPart.schema.json b/schema/agentPart.schema.json new file mode 100644 index 00000000000..6b84b2f8736 --- /dev/null +++ b/schema/agentPart.schema.json @@ -0,0 +1,51 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/agentPart.json", + "$comment": "Source: packages/opencode/src/session/message-v2.ts @ current lines 163-176", + "title": "AgentPart", + "description": "Agent reference part with optional source location", + "type": "object", + "required": ["id", "sessionID", "messageID", "type", "name"], + "properties": { + "id": { + "type": "string", + "description": "Part identifier" + }, + "sessionID": { + "type": "string", + "description": "Session identifier" + }, + "messageID": { + "type": "string", + "description": "Message identifier" + }, + "type": { + "type": "string", + "const": "agent", + "description": "Part type discriminator" + }, + "name": { + "type": "string", + "description": "Agent name" + }, + "source": { + "type": "object", + "description": "Source location information", + "required": ["value", "start", "end"], + "properties": { + "value": { + "type": "string", + "description": "Source value/content" + }, + "start": { + "type": "integer", + "description": "Start offset" + }, + "end": { + "type": "integer", + "description": "End offset" + } + } + } + } +} diff --git a/schema/anthropicOptions.schema.json b/schema/anthropicOptions.schema.json new file mode 100644 index 00000000000..e2634b15ab2 --- /dev/null +++ b/schema/anthropicOptions.schema.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/anthropicOptions.json", + "$comment": "Source: packages/opencode/src/provider/transform.ts @ 2fd9737 (2026-01-02) lines 323-340", + "title": "AnthropicOptions", + "description": "Anthropic-specific options (Claude extended thinking)", + "type": "object", + "properties": { + "thinking": { + "$ref": "thinkingOptions.schema.json" + } + } +} diff --git a/schema/apiAuth.schema.json b/schema/apiAuth.schema.json new file mode 100644 index 00000000000..bb4cc092986 --- /dev/null +++ b/schema/apiAuth.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/apiAuth.json", + "$comment": "Source: packages/opencode/src/auth/index.ts @ c50f588 (2026-01-05) lines 17-22", + "title": "ApiAuth", + "description": "API key authentication credentials", + "type": "object", + "required": ["type", "key"], + "properties": { + "type": { + "type": "string", + "const": "api", + "description": "Authentication type discriminator" + }, + "key": { + "type": "string", + "description": "API key for authentication" + } + } +} diff --git a/schema/apiError.schema.json b/schema/apiError.schema.json new file mode 100644 index 00000000000..214074d3909 --- /dev/null +++ b/schema/apiError.schema.json @@ -0,0 +1,52 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/apiError.json", + "$comment": "Source: packages/opencode/src/session/message-v2.ts @ current lines 45-55 (via NamedError.create)", + "title": "APIError", + "description": "API error with retry information (wrapped in NamedError format)", + "type": "object", + "required": ["name", "data"], + "properties": { + "name": { + "type": "string", + "const": "APIError", + "description": "Error type name" + }, + "data": { + "type": "object", + "required": ["message", "isRetryable"], + "properties": { + "message": { + "type": "string", + "description": "Error message" + }, + "statusCode": { + "type": "number", + "description": "HTTP status code" + }, + "isRetryable": { + "type": "boolean", + "description": "Whether the request can be retried" + }, + "responseHeaders": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Response headers from the API" + }, + "responseBody": { + "type": "string", + "description": "Response body from the API" + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Additional error metadata" + } + } + } + } +} diff --git a/schema/assistantMessage.schema.json b/schema/assistantMessage.schema.json new file mode 100644 index 00000000000..3a176114654 --- /dev/null +++ b/schema/assistantMessage.schema.json @@ -0,0 +1,124 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/assistantMessage.json", + "$comment": "Source: packages/opencode/src/session/message-v2.ts @ current lines 308-350", + "title": "AssistantMessage", + "description": "Assistant message with model info, tokens, and optional error", + "type": "object", + "required": ["id", "sessionID", "role", "time", "parentID", "modelID", "providerID", "mode", "agent", "path", "cost", "tokens"], + "properties": { + "id": { + "type": "string", + "description": "Message identifier" + }, + "sessionID": { + "type": "string", + "description": "Session identifier" + }, + "role": { + "type": "string", + "const": "assistant", + "description": "Message role discriminator" + }, + "time": { + "type": "object", + "description": "Message timing", + "required": ["created"], + "properties": { + "created": { + "type": "number", + "description": "Creation timestamp (ms)" + }, + "completed": { + "type": "number", + "description": "Completion timestamp (ms)" + } + } + }, + "error": { + "$ref": "messageError.schema.json", + "description": "Optional error that occurred during processing" + }, + "parentID": { + "type": "string", + "description": "Parent message identifier (the user message this responds to)" + }, + "modelID": { + "type": "string", + "description": "Model identifier used for this response" + }, + "providerID": { + "type": "string", + "description": "Provider identifier used for this response" + }, + "mode": { + "type": "string", + "description": "Mode (deprecated)" + }, + "agent": { + "type": "string", + "description": "Agent name" + }, + "path": { + "type": "object", + "description": "Working directory context", + "required": ["cwd", "root"], + "properties": { + "cwd": { + "type": "string", + "description": "Current working directory" + }, + "root": { + "type": "string", + "description": "Project root directory" + } + } + }, + "summary": { + "type": "boolean", + "description": "Whether this message has a summary" + }, + "cost": { + "type": "number", + "description": "Total cost of this response" + }, + "tokens": { + "type": "object", + "description": "Token usage breakdown", + "required": ["input", "output", "reasoning", "cache"], + "properties": { + "input": { + "type": "number", + "description": "Input tokens used" + }, + "output": { + "type": "number", + "description": "Output tokens generated" + }, + "reasoning": { + "type": "number", + "description": "Reasoning tokens used" + }, + "cache": { + "type": "object", + "description": "Cache token usage", + "required": ["read", "write"], + "properties": { + "read": { + "type": "number", + "description": "Cache read tokens" + }, + "write": { + "type": "number", + "description": "Cache write tokens" + } + } + } + } + }, + "finish": { + "type": "string", + "description": "Finish reason" + } + } +} diff --git a/schema/auth.schema.json b/schema/auth.schema.json new file mode 100644 index 00000000000..ee2e6652949 --- /dev/null +++ b/schema/auth.schema.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/auth.json", + "$comment": "Source: packages/opencode/src/auth/index.ts @ c50f588 (2026-01-05) line 32", + "title": "Auth", + "description": "Authentication credentials (discriminated union of OAuth, API key, or well-known auth)", + "oneOf": [ + { + "$ref": "oauth.schema.json" + }, + { + "$ref": "apiAuth.schema.json" + }, + { + "$ref": "wellKnownAuth.schema.json" + } + ] +} diff --git a/schema/cacheCost.schema.json b/schema/cacheCost.schema.json new file mode 100644 index 00000000000..877f83d7313 --- /dev/null +++ b/schema/cacheCost.schema.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/cacheCost.json", + "$comment": "Source: packages/opencode/src/provider/provider.ts @ d72d7ab (2026-01-04) lines 410-470", + "title": "CacheCost", + "description": "Cache operation costs per token", + "type": "object", + "required": ["read", "write"], + "properties": { + "read": { + "type": "number", + "description": "Cache read cost per token", + "minimum": 0 + }, + "write": { + "type": "number", + "description": "Cache write cost per token", + "minimum": 0 + } + } +} diff --git a/schema/compactionPart.schema.json b/schema/compactionPart.schema.json new file mode 100644 index 00000000000..39b80eccf87 --- /dev/null +++ b/schema/compactionPart.schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/compactionPart.json", + "$comment": "Source: packages/opencode/src/session/message-v2.ts @ current lines 178-184", + "title": "CompactionPart", + "description": "Compaction marker part indicating message history was compacted", + "type": "object", + "required": ["id", "sessionID", "messageID", "type", "auto"], + "properties": { + "id": { + "type": "string", + "description": "Part identifier" + }, + "sessionID": { + "type": "string", + "description": "Session identifier" + }, + "messageID": { + "type": "string", + "description": "Message identifier" + }, + "type": { + "type": "string", + "const": "compaction", + "description": "Part type discriminator" + }, + "auto": { + "type": "boolean", + "description": "Whether compaction was automatic" + } + } +} diff --git a/schema/event.schema.json b/schema/event.schema.json new file mode 100644 index 00000000000..b09d64f4f2e --- /dev/null +++ b/schema/event.schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/event.json", + "$comment": "Source: packages/opencode/src/bus/bus-event.ts @ 146a9b8ab (2026-01-05) - BusEvent.payloads() aggregator", + "title": "Event", + "description": "Discriminated union of all SSE event types (discriminator: type)", + "oneOf": [ + { "$ref": "messageUpdatedEvent.schema.json" }, + { "$ref": "messageRemovedEvent.schema.json" }, + { "$ref": "messagePartUpdatedEvent.schema.json" }, + { "$ref": "messagePartRemovedEvent.schema.json" }, + { "$ref": "sessionCreatedEvent.schema.json" }, + { "$ref": "sessionUpdatedEvent.schema.json" }, + { "$ref": "sessionDeletedEvent.schema.json" }, + { "$ref": "sessionStatusEvent.schema.json" }, + { "$ref": "permissionAskedEvent.schema.json" }, + { "$ref": "permissionRepliedEvent.schema.json" } + ] +} diff --git a/schema/experimentalPricing.schema.json b/schema/experimentalPricing.schema.json new file mode 100644 index 00000000000..2067a60cc83 --- /dev/null +++ b/schema/experimentalPricing.schema.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/experimentalPricing.json", + "$comment": "Source: packages/opencode/src/provider/provider.ts @ d72d7ab (2026-01-04) lines 410-470", + "title": "ExperimentalPricing", + "description": "Experimental pricing tier for large contexts (over 200K tokens)", + "type": "object", + "required": ["input", "output", "cache"], + "properties": { + "input": { + "type": "number", + "description": "Input token cost at experimental tier", + "minimum": 0 + }, + "output": { + "type": "number", + "description": "Output token cost at experimental tier", + "minimum": 0 + }, + "cache": { + "$ref": "cacheCost.schema.json" + } + } +} diff --git a/schema/fileDiff.schema.json b/schema/fileDiff.schema.json new file mode 100644 index 00000000000..787057a5147 --- /dev/null +++ b/schema/fileDiff.schema.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/fileDiff.json", + "$comment": "Source: packages/opencode/src/snapshot/index.ts @ 0e08655 (2025-11-26) lines 147-157", + "title": "FileDiff", + "description": "File diff metadata showing changes (additions/deletions) for a single file", + "type": "object", + "required": ["file", "before", "after", "additions", "deletions"], + "properties": { + "file": { + "type": "string", + "description": "File path" + }, + "before": { + "type": "string", + "description": "Content hash or identifier before changes" + }, + "after": { + "type": "string", + "description": "Content hash or identifier after changes" + }, + "additions": { + "type": "number", + "description": "Number of lines added" + }, + "deletions": { + "type": "number", + "description": "Number of lines deleted" + } + } +} diff --git a/schema/filePart.schema.json b/schema/filePart.schema.json new file mode 100644 index 00000000000..0a2cc6c331a --- /dev/null +++ b/schema/filePart.schema.json @@ -0,0 +1,44 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/filePart.json", + "$comment": "Source: packages/opencode/src/session/message-v2.ts @ 21dc3c2 (2026-01-04) lines 132-141", + "title": "FilePart", + "description": "File attachment part with MIME type and URL", + "type": "object", + "required": ["id", "sessionID", "messageID", "type", "mime", "url"], + "properties": { + "id": { + "type": "string", + "description": "Part identifier" + }, + "sessionID": { + "type": "string", + "description": "Session identifier" + }, + "messageID": { + "type": "string", + "description": "Message identifier" + }, + "type": { + "type": "string", + "const": "file", + "description": "Part type discriminator" + }, + "mime": { + "type": "string", + "description": "MIME type of the file" + }, + "filename": { + "type": "string", + "description": "Original filename" + }, + "url": { + "type": "string", + "description": "URL to access the file" + }, + "source": { + "$ref": "filePartSource.schema.json", + "description": "Source information for the file" + } + } +} diff --git a/schema/filePartSource.schema.json b/schema/filePartSource.schema.json new file mode 100644 index 00000000000..7851f1a2cf1 --- /dev/null +++ b/schema/filePartSource.schema.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/filePartSource.json", + "$comment": "Source: packages/opencode/src/session/message-v2.ts @ 21dc3c2 (2026-01-04) lines 128-130", + "title": "FilePartSource", + "description": "Discriminated union of file part sources (file, symbol, or resource)", + "oneOf": [ + { "$ref": "fileSource.schema.json" }, + { "$ref": "symbolSource.schema.json" }, + { "$ref": "resourceSource.schema.json" } + ] +} diff --git a/schema/filePartSourceText.schema.json b/schema/filePartSourceText.schema.json new file mode 100644 index 00000000000..6edc30dc2be --- /dev/null +++ b/schema/filePartSourceText.schema.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/filePartSourceText.json", + "$comment": "Source: packages/opencode/src/session/message-v2.ts @ 21dc3c2 (2026-01-04) lines 92-100", + "title": "FilePartSourceText", + "description": "Text content with line range for file part sources", + "type": "object", + "required": ["value", "start", "end"], + "properties": { + "value": { + "type": "string", + "description": "The text content" + }, + "start": { + "type": "integer", + "description": "Start line number" + }, + "end": { + "type": "integer", + "description": "End line number" + } + } +} diff --git a/schema/fileSource.schema.json b/schema/fileSource.schema.json new file mode 100644 index 00000000000..5f77d95e5c0 --- /dev/null +++ b/schema/fileSource.schema.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/fileSource.json", + "$comment": "Source: packages/opencode/src/session/message-v2.ts @ 21dc3c2 (2026-01-04) lines 103-108", + "title": "FileSource", + "description": "File-based source with path and text content", + "type": "object", + "required": ["type", "path", "text"], + "properties": { + "type": { + "type": "string", + "const": "file", + "description": "Source type discriminator" + }, + "path": { + "type": "string", + "description": "File path" + }, + "text": { + "$ref": "filePartSourceText.schema.json", + "description": "Text content with line range" + } + } +} diff --git a/schema/globalEvent.schema.json b/schema/globalEvent.schema.json new file mode 100644 index 00000000000..7bcc4093bc7 --- /dev/null +++ b/schema/globalEvent.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/globalEvent.json", + "$comment": "Source: packages/opencode/src/server/server.ts @ 146a9b8ab (2026-01-05) - GET /global/event endpoint", + "title": "GlobalEvent", + "description": "Global event wrapper with directory context (from GET /global/event SSE endpoint)", + "type": "object", + "required": ["directory", "payload"], + "properties": { + "directory": { + "type": "string", + "description": "Working directory context for this event" + }, + "payload": { + "$ref": "event.schema.json", + "description": "The actual event payload" + } + }, + "additionalProperties": false +} diff --git a/schema/googleOptions.schema.json b/schema/googleOptions.schema.json new file mode 100644 index 00000000000..e801b4a377a --- /dev/null +++ b/schema/googleOptions.schema.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/googleOptions.json", + "$comment": "Source: packages/opencode/src/provider/transform.ts @ 2fd9737 (2026-01-02) lines 357-379", + "title": "GoogleOptions", + "description": "Google/Gemini-specific options", + "type": "object", + "properties": { + "thinkingConfig": { + "$ref": "thinkingConfig.schema.json" + } + } +} diff --git a/schema/ioCapabilities.schema.json b/schema/ioCapabilities.schema.json new file mode 100644 index 00000000000..d4eca51b285 --- /dev/null +++ b/schema/ioCapabilities.schema.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/ioCapabilities.json", + "$comment": "Source: packages/opencode/src/provider/models.ts @ 81fef60 (2025-12-30) lines 40-44", + "title": "IOCapabilities", + "description": "Input/output modality capabilities", + "type": "object", + "required": ["text", "audio", "image", "video", "pdf"], + "properties": { + "text": { + "type": "boolean", + "description": "Supports text input/output" + }, + "audio": { + "type": "boolean", + "description": "Supports audio input/output" + }, + "image": { + "type": "boolean", + "description": "Supports image input/output" + }, + "video": { + "type": "boolean", + "description": "Supports video input/output" + }, + "pdf": { + "type": "boolean", + "description": "Supports PDF input/output" + } + } +} diff --git a/schema/lspPosition.schema.json b/schema/lspPosition.schema.json new file mode 100644 index 00000000000..52dc31d8f4a --- /dev/null +++ b/schema/lspPosition.schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/lspPosition.json", + "$comment": "Source: packages/opencode/src/lsp/index.ts @ 21dc3c2 (2026-01-04) lines 23-26", + "title": "LspPosition", + "description": "LSP position with line and character", + "type": "object", + "required": ["line", "character"], + "properties": { + "line": { + "type": "number", + "description": "Line number (0-based)" + }, + "character": { + "type": "number", + "description": "Character offset (0-based)" + } + } +} diff --git a/schema/lspRange.schema.json b/schema/lspRange.schema.json new file mode 100644 index 00000000000..7b429d2f9b8 --- /dev/null +++ b/schema/lspRange.schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/lspRange.json", + "$comment": "Source: packages/opencode/src/lsp/index.ts @ 21dc3c2 (2026-01-04) lines 21-34", + "title": "LspRange", + "description": "LSP range with start and end positions", + "type": "object", + "required": ["start", "end"], + "properties": { + "start": { + "$ref": "lspPosition.schema.json", + "description": "Start position" + }, + "end": { + "$ref": "lspPosition.schema.json", + "description": "End position" + } + } +} diff --git a/schema/message.schema.json b/schema/message.schema.json new file mode 100644 index 00000000000..53db46865c5 --- /dev/null +++ b/schema/message.schema.json @@ -0,0 +1,11 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/message.json", + "$comment": "Source: packages/opencode/src/session/message-v2.ts @ current lines 352-354", + "title": "Message", + "description": "Discriminated union of user and assistant messages", + "oneOf": [ + { "$ref": "userMessage.schema.json" }, + { "$ref": "assistantMessage.schema.json" } + ] +} diff --git a/schema/messageError.schema.json b/schema/messageError.schema.json new file mode 100644 index 00000000000..b1ca1ba9c95 --- /dev/null +++ b/schema/messageError.schema.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/messageError.json", + "$comment": "Source: packages/opencode/src/session/message-v2.ts @ current lines 314-321 (error discriminated union)", + "title": "MessageError", + "description": "Discriminated union of all message error types", + "oneOf": [ + { "$ref": "providerAuthError.schema.json" }, + { "$ref": "unknownError.schema.json" }, + { "$ref": "outputLengthError.schema.json" }, + { "$ref": "abortedError.schema.json" }, + { "$ref": "apiError.schema.json" } + ] +} diff --git a/schema/messagePartRemovedEvent.schema.json b/schema/messagePartRemovedEvent.schema.json new file mode 100644 index 00000000000..c393021a276 --- /dev/null +++ b/schema/messagePartRemovedEvent.schema.json @@ -0,0 +1,36 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/messagePartRemovedEvent.json", + "$comment": "Source: packages/opencode/src/session/message-v2.ts @ 146a9b8ab (2026-01-05) lines 330-337", + "title": "MessagePartRemovedEvent", + "description": "Event payload for message.part.removed - part deleted", + "type": "object", + "required": ["type", "properties"], + "properties": { + "type": { + "type": "string", + "const": "message.part.removed", + "description": "Event type discriminator" + }, + "properties": { + "type": "object", + "required": ["sessionID", "messageID", "partID"], + "properties": { + "sessionID": { + "type": "string", + "description": "Session identifier" + }, + "messageID": { + "type": "string", + "description": "Message identifier" + }, + "partID": { + "type": "string", + "description": "Removed part identifier" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false +} diff --git a/schema/messagePartUpdatedEvent.schema.json b/schema/messagePartUpdatedEvent.schema.json new file mode 100644 index 00000000000..7673f233784 --- /dev/null +++ b/schema/messagePartUpdatedEvent.schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/messagePartUpdatedEvent.json", + "$comment": "Source: packages/opencode/src/session/message-v2.ts @ 146a9b8ab (2026-01-05) lines 323-329", + "title": "MessagePartUpdatedEvent", + "description": "Event payload for message.part.updated - streaming content chunk", + "type": "object", + "required": ["type", "properties"], + "properties": { + "type": { + "type": "string", + "const": "message.part.updated", + "description": "Event type discriminator" + }, + "properties": { + "type": "object", + "required": ["part"], + "properties": { + "part": { + "$ref": "part.schema.json", + "description": "Updated part content" + }, + "delta": { + "type": "string", + "description": "Optional delta text for incremental updates" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false +} diff --git a/schema/messageRemovedEvent.schema.json b/schema/messageRemovedEvent.schema.json new file mode 100644 index 00000000000..2a545f96536 --- /dev/null +++ b/schema/messageRemovedEvent.schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/messageRemovedEvent.json", + "$comment": "Source: packages/opencode/src/session/message-v2.ts @ 146a9b8ab (2026-01-05) lines 316-322", + "title": "MessageRemovedEvent", + "description": "Event payload for message.removed - message deleted", + "type": "object", + "required": ["type", "properties"], + "properties": { + "type": { + "type": "string", + "const": "message.removed", + "description": "Event type discriminator" + }, + "properties": { + "type": "object", + "required": ["sessionID", "messageID"], + "properties": { + "sessionID": { + "type": "string", + "description": "Session identifier" + }, + "messageID": { + "type": "string", + "description": "Removed message identifier" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false +} diff --git a/schema/messageUpdatedEvent.schema.json b/schema/messageUpdatedEvent.schema.json new file mode 100644 index 00000000000..c7981dd4167 --- /dev/null +++ b/schema/messageUpdatedEvent.schema.json @@ -0,0 +1,28 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/messageUpdatedEvent.json", + "$comment": "Source: packages/opencode/src/session/message-v2.ts @ 146a9b8ab (2026-01-05) lines 310-315", + "title": "MessageUpdatedEvent", + "description": "Event payload for message.updated - message metadata changed", + "type": "object", + "required": ["type", "properties"], + "properties": { + "type": { + "type": "string", + "const": "message.updated", + "description": "Event type discriminator" + }, + "properties": { + "type": "object", + "required": ["info"], + "properties": { + "info": { + "$ref": "message.schema.json", + "description": "Updated message information" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false +} diff --git a/schema/modelAPI.schema.json b/schema/modelAPI.schema.json new file mode 100644 index 00000000000..fcecbfab571 --- /dev/null +++ b/schema/modelAPI.schema.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/modelAPI.json", + "$comment": "Source: packages/opencode/src/provider/provider.ts @ d72d7ab (2026-01-04) lines 410-470", + "title": "ModelAPI", + "description": "API configuration for a model", + "type": "object", + "required": ["id", "url", "npm"], + "properties": { + "id": { + "type": "string", + "description": "API model identifier (may differ from display ID)" + }, + "url": { + "type": "string", + "format": "uri", + "description": "API endpoint URL" + }, + "npm": { + "type": "string", + "description": "npm package name for the AI SDK provider", + "examples": [ + "@ai-sdk/anthropic", + "@ai-sdk/openai", + "@ai-sdk/google", + "@ai-sdk/openai-compatible" + ] + } + } +} diff --git a/schema/modelCapabilities.schema.json b/schema/modelCapabilities.schema.json new file mode 100644 index 00000000000..6800f30afea --- /dev/null +++ b/schema/modelCapabilities.schema.json @@ -0,0 +1,53 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/modelCapabilities.json", + "$comment": "Source: packages/opencode/src/provider/provider.ts @ d72d7ab (2026-01-04) lines 410-470", + "title": "ModelCapabilities", + "description": "Model capabilities - what features the model supports (boolean flags)", + "type": "object", + "required": ["temperature", "reasoning", "attachment", "toolcall", "input", "output", "interleaved"], + "properties": { + "temperature": { + "type": "boolean", + "description": "Supports temperature parameter for controlling randomness" + }, + "reasoning": { + "type": "boolean", + "description": "Supports extended reasoning (chain-of-thought)" + }, + "attachment": { + "type": "boolean", + "description": "Supports file attachments" + }, + "toolcall": { + "type": "boolean", + "description": "Supports tool/function calling" + }, + "input": { + "$ref": "ioCapabilities.schema.json", + "description": "Input modality capabilities" + }, + "output": { + "$ref": "ioCapabilities.schema.json", + "description": "Output modality capabilities" + }, + "interleaved": { + "description": "Supports interleaved content", + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "object", + "required": ["field"], + "properties": { + "field": { + "type": "string", + "enum": ["reasoning_content", "reasoning_details"] + } + } + } + ] + } + } +} diff --git a/schema/modelCost.schema.json b/schema/modelCost.schema.json new file mode 100644 index 00000000000..87cfe3e8238 --- /dev/null +++ b/schema/modelCost.schema.json @@ -0,0 +1,28 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/modelCost.json", + "$comment": "Source: packages/opencode/src/provider/provider.ts @ d72d7ab (2026-01-04) lines 410-470", + "title": "ModelCost", + "description": "Model pricing per token", + "type": "object", + "required": ["input", "output", "cache"], + "properties": { + "input": { + "type": "number", + "description": "Input token cost", + "minimum": 0 + }, + "output": { + "type": "number", + "description": "Output token cost", + "minimum": 0 + }, + "cache": { + "$ref": "cacheCost.schema.json" + }, + "experimentalOver200K": { + "$ref": "experimentalPricing.schema.json", + "description": "Experimental pricing for contexts over 200K tokens" + } + } +} diff --git a/schema/modelInfo.schema.json b/schema/modelInfo.schema.json new file mode 100644 index 00000000000..b721e423527 --- /dev/null +++ b/schema/modelInfo.schema.json @@ -0,0 +1,89 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/modelInfo.json", + "$comment": "Source: packages/opencode/src/provider/provider.ts @ d72d7ab (2026-01-04) lines 410-470", + "title": "ModelInfo", + "description": "Comprehensive model metadata including capabilities, pricing, limits, and configurable options. Foundation schema referenced by Provider, Session, and other schemas.", + "type": "object", + "required": [ + "id", + "providerID", + "name", + "api", + "capabilities", + "cost", + "limit", + "status", + "options", + "headers", + "release_date" + ], + "properties": { + "id": { + "type": "string", + "description": "Unique model identifier", + "examples": [ + "claude-3-5-sonnet-20241022", + "gpt-4", + "gemini-2.0-flash-exp" + ] + }, + "providerID": { + "type": "string", + "description": "Provider identifier this model belongs to", + "examples": ["anthropic", "openai", "google"] + }, + "name": { + "type": "string", + "description": "Human-readable model name", + "examples": [ + "Claude 3.5 Sonnet", + "GPT-4", + "Gemini 2.0 Flash" + ] + }, + "family": { + "type": "string", + "description": "Model family name (optional)" + }, + "api": { + "$ref": "modelAPI.schema.json" + }, + "capabilities": { + "$ref": "modelCapabilities.schema.json" + }, + "cost": { + "$ref": "modelCost.schema.json" + }, + "limit": { + "$ref": "modelLimits.schema.json" + }, + "status": { + "$ref": "modelStatus.schema.json" + }, + "options": { + "type": "object", + "description": "Configurable runtime settings (provider-specific, stored as flexible map)", + "additionalProperties": true + }, + "headers": { + "type": "object", + "description": "Custom HTTP headers for API calls", + "additionalProperties": { + "type": "string" + } + }, + "release_date": { + "type": "string", + "description": "Model release date" + }, + "variants": { + "type": "object", + "description": "Model variants with custom configurations (optional)", + "additionalProperties": { + "type": "object", + "additionalProperties": true + } + } + } +} diff --git a/schema/modelLimits.schema.json b/schema/modelLimits.schema.json new file mode 100644 index 00000000000..d29b7bd143a --- /dev/null +++ b/schema/modelLimits.schema.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/modelLimits.json", + "$comment": "Source: packages/opencode/src/provider/models.ts @ 81fef60 (2025-12-30) lines 36-39\nNote: Uses number type to match server z.number()", + "title": "ModelLimits", + "description": "Model context and output limits", + "type": "object", + "required": ["context", "output"], + "properties": { + "context": { + "type": "number", + "description": "Context window size in tokens", + "minimum": 0 + }, + "output": { + "type": "number", + "description": "Maximum output tokens", + "minimum": 0 + } + } +} diff --git a/schema/modelOptions.schema.json b/schema/modelOptions.schema.json new file mode 100644 index 00000000000..6b3271b5fb4 --- /dev/null +++ b/schema/modelOptions.schema.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/modelOptions.json", + "$comment": "Source: packages/opencode/src/provider/models.ts @ 81fef60 (2025-12-30) line 48\nUsage: Composition schema aggregating all provider-specific option schemas\nNote: This is a meta-schema that references other schemas, not a direct TypeScript type. Generated into SDK types.", + "title": "ModelOptions", + "description": "Configurable runtime settings for models (provider-specific)", + "type": "object", + "properties": { + "universal": { + "$ref": "universalOptions.schema.json", + "description": "Universal options supported by all providers" + }, + "openai": { + "$ref": "openaiOptions.schema.json", + "description": "OpenAI/OpenRouter/Responses-specific options" + }, + "google": { + "$ref": "googleOptions.schema.json", + "description": "Google/Gemini-specific options" + }, + "anthropic": { + "$ref": "anthropicOptions.schema.json", + "description": "Anthropic/Claude-specific options" + } + } +} diff --git a/schema/modelSelection.schema.json b/schema/modelSelection.schema.json new file mode 100644 index 00000000000..2c3c3ce096a --- /dev/null +++ b/schema/modelSelection.schema.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/modelSelection.json", + "$comment": "Usage: API response type for session model selection, generated into SDK types\nNote: This schema defines the API response structure, not a direct TypeScript type in the server code", + "title": "ModelSelection", + "description": "Model selection for a session (references a model within a provider)", + "type": "object", + "required": ["provider", "modelId", "name"], + "properties": { + "provider": { + "$ref": "providerInfo.schema.json", + "description": "Provider with curated models list" + }, + "modelId": { + "type": "string", + "description": "Model ID (must exist in provider.models)", + "examples": ["claude-3-5-sonnet-20241022"] + }, + "name": { + "type": "string", + "description": "Display name for the selected model", + "examples": ["Claude 3.5 Sonnet"] + } + } +} diff --git a/schema/modelStatus.schema.json b/schema/modelStatus.schema.json new file mode 100644 index 00000000000..28362b06d79 --- /dev/null +++ b/schema/modelStatus.schema.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/modelStatus.json", + "$comment": "Source: packages/opencode/src/provider/models.ts @ 81fef60 (2025-12-30) line 47", + "title": "ModelStatus", + "description": "Model availability status", + "type": "string", + "enum": ["alpha", "beta", "active", "deprecated"], + "x-enum-descriptions": { + "alpha": "Early testing phase", + "beta": "Public beta", + "active": "Generally available", + "deprecated": "No longer recommended for use" + } +} diff --git a/schema/oauth.schema.json b/schema/oauth.schema.json new file mode 100644 index 00000000000..b02b0e8ca16 --- /dev/null +++ b/schema/oauth.schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/oauth.json", + "$comment": "Source: packages/opencode/src/auth/index.ts @ c50f588 (2026-01-05) lines 7-15", + "title": "OAuth", + "description": "OAuth authentication credentials with refresh and access tokens", + "type": "object", + "required": ["type", "refresh", "access", "expires"], + "properties": { + "type": { + "type": "string", + "const": "oauth", + "description": "Authentication type discriminator" + }, + "refresh": { + "type": "string", + "description": "OAuth refresh token" + }, + "access": { + "type": "string", + "description": "OAuth access token" + }, + "expires": { + "type": "number", + "description": "Token expiration time (Unix timestamp in milliseconds)" + }, + "enterpriseUrl": { + "type": "string", + "description": "Optional enterprise-specific OAuth endpoint URL" + } + } +} diff --git a/schema/openaiOptions.schema.json b/schema/openaiOptions.schema.json new file mode 100644 index 00000000000..9a6cf610b1e --- /dev/null +++ b/schema/openaiOptions.schema.json @@ -0,0 +1,90 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/openaiOptions.json", + "$comment": "Source: packages/opencode/src/provider/sdk/openai-compatible/src/responses/openai-responses-language-model.ts @ 6e2379a (2025-11-29) lines 1671-1711", + "title": "OpenAIOptions", + "description": "OpenAI/OpenRouter/Responses API-specific options", + "type": "object", + "properties": { + "reasoningEffort": { + "type": "string", + "enum": ["low", "medium", "minimal", "high"], + "description": "Reasoning effort level (reasoning models only)" + }, + "reasoningSummary": { + "type": "string", + "description": "Auto or custom reasoning summary" + }, + "textVerbosity": { + "type": "string", + "enum": ["low", "medium", "high"], + "description": "Response verbosity level" + }, + "serviceTier": { + "type": "string", + "enum": ["auto", "flex", "priority"], + "description": "Processing priority tier" + }, + "logprobs": { + "oneOf": [ + { + "type": "boolean", + "description": "Enable/disable token log probabilities" + }, + { + "type": "integer", + "minimum": 1, + "maximum": 20, + "description": "Number of top tokens to return logprobs for" + } + ] + }, + "maxToolCalls": { + "type": "integer", + "minimum": 0, + "description": "Maximum number of built-in tool calls" + }, + "parallelToolCalls": { + "type": "boolean", + "description": "Allow parallel tool execution" + }, + "store": { + "type": "boolean", + "description": "Store conversation for training purposes" + }, + "strictJsonSchema": { + "type": "boolean", + "description": "Enforce strict JSON schema validation" + }, + "promptCacheKey": { + "type": "string", + "description": "Prompt cache key for optimization" + }, + "include": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Additional response fields to include" + }, + "metadata": { + "description": "Custom metadata (any type)" + }, + "user": { + "type": "string", + "description": "End-user identifier for monitoring" + }, + "instructions": { + "type": "string", + "description": "System instructions" + }, + "safetyIdentifier": { + "type": "string", + "description": "Safety classification identifier" + }, + "previousResponseId": { + "type": "string", + "description": "Continue from previous response" + } + } +} diff --git a/schema/outputLengthError.schema.json b/schema/outputLengthError.schema.json new file mode 100644 index 00000000000..825fe1a093c --- /dev/null +++ b/schema/outputLengthError.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/outputLengthError.json", + "$comment": "Source: packages/opencode/src/session/message-v2.ts @ current line 36 (via NamedError.create)", + "title": "MessageOutputLengthError", + "description": "Message output length error (wrapped in NamedError format)", + "type": "object", + "required": ["name", "data"], + "properties": { + "name": { + "type": "string", + "const": "MessageOutputLengthError", + "description": "Error type name" + }, + "data": { + "type": "object", + "properties": {} + } + } +} diff --git a/schema/part.schema.json b/schema/part.schema.json new file mode 100644 index 00000000000..d0d7839206b --- /dev/null +++ b/schema/part.schema.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/part.json", + "$comment": "Source: packages/opencode/src/session/message-v2.ts @ current lines 288-305", + "title": "Part", + "description": "Discriminated union of all message part types", + "oneOf": [ + { "$ref": "textPart.schema.json" }, + { "$ref": "subtaskPart.schema.json" }, + { "$ref": "reasoningPart.schema.json" }, + { "$ref": "filePart.schema.json" }, + { "$ref": "toolPart.schema.json" }, + { "$ref": "stepStartPart.schema.json" }, + { "$ref": "stepFinishPart.schema.json" }, + { "$ref": "snapshotPart.schema.json" }, + { "$ref": "patchPart.schema.json" }, + { "$ref": "agentPart.schema.json" }, + { "$ref": "retryPart.schema.json" }, + { "$ref": "compactionPart.schema.json" } + ] +} diff --git a/schema/patchPart.schema.json b/schema/patchPart.schema.json new file mode 100644 index 00000000000..688eebfa838 --- /dev/null +++ b/schema/patchPart.schema.json @@ -0,0 +1,39 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/patchPart.json", + "$comment": "Source: packages/opencode/src/session/message-v2.ts @ current lines 72-79", + "title": "PatchPart", + "description": "Patch/diff part with file list", + "type": "object", + "required": ["id", "sessionID", "messageID", "type", "hash", "files"], + "properties": { + "id": { + "type": "string", + "description": "Part identifier" + }, + "sessionID": { + "type": "string", + "description": "Session identifier" + }, + "messageID": { + "type": "string", + "description": "Message identifier" + }, + "type": { + "type": "string", + "const": "patch", + "description": "Part type discriminator" + }, + "hash": { + "type": "string", + "description": "Patch hash identifier" + }, + "files": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of affected files" + } + } +} diff --git a/schema/permissionAction.schema.json b/schema/permissionAction.schema.json new file mode 100644 index 00000000000..9175a344428 --- /dev/null +++ b/schema/permissionAction.schema.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/permissionAction.json", + "$comment": "Source: packages/opencode/src/permission/next.ts @ 47c670a (2026-01-03) lines 15-17", + "title": "PermissionAction", + "description": "Permission action enum (allow, deny, ask)", + "type": "string", + "enum": ["allow", "deny", "ask"] +} diff --git a/schema/permissionAskedEvent.schema.json b/schema/permissionAskedEvent.schema.json new file mode 100644 index 00000000000..7f6c3c56d1b --- /dev/null +++ b/schema/permissionAskedEvent.schema.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/permissionAskedEvent.json", + "$comment": "Source: packages/opencode/src/permission/next.ts @ 146a9b8ab (2026-01-05) line 68", + "title": "PermissionAskedEvent", + "description": "Event payload for permission.asked - permission request created", + "type": "object", + "required": ["type", "properties"], + "properties": { + "type": { + "type": "string", + "const": "permission.asked", + "description": "Event type discriminator" + }, + "properties": { + "$ref": "permissionRequest.schema.json", + "description": "Permission request details" + } + }, + "additionalProperties": false +} diff --git a/schema/permissionRepliedEvent.schema.json b/schema/permissionRepliedEvent.schema.json new file mode 100644 index 00000000000..c578d3aa017 --- /dev/null +++ b/schema/permissionRepliedEvent.schema.json @@ -0,0 +1,36 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/permissionRepliedEvent.json", + "$comment": "Source: packages/opencode/src/permission/next.ts @ 146a9b8ab (2026-01-05) lines 69-76", + "title": "PermissionRepliedEvent", + "description": "Event payload for permission.replied - permission response received", + "type": "object", + "required": ["type", "properties"], + "properties": { + "type": { + "type": "string", + "const": "permission.replied", + "description": "Event type discriminator" + }, + "properties": { + "type": "object", + "required": ["sessionID", "requestID", "reply"], + "properties": { + "sessionID": { + "type": "string", + "description": "Session identifier" + }, + "requestID": { + "type": "string", + "description": "Permission request identifier" + }, + "reply": { + "$ref": "permissionReply.schema.json", + "description": "User's reply to the permission request" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false +} diff --git a/schema/permissionReply.schema.json b/schema/permissionReply.schema.json new file mode 100644 index 00000000000..8c5090f9f21 --- /dev/null +++ b/schema/permissionReply.schema.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/permissionReply.json", + "$comment": "Source: packages/opencode/src/permission/next.ts @ 21dc3c2 (2026-01-04) line 73", + "title": "PermissionReply", + "description": "User reply to a permission request", + "type": "string", + "enum": ["once", "always", "reject"] +} diff --git a/schema/permissionRequest.schema.json b/schema/permissionRequest.schema.json new file mode 100644 index 00000000000..06c5e2d5359 --- /dev/null +++ b/schema/permissionRequest.schema.json @@ -0,0 +1,48 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/permissionRequest.json", + "$comment": "Source: packages/opencode/src/permission/next.ts @ 21dc3c2 (2026-01-04) lines 52-69", + "title": "PermissionRequest", + "description": "Permission request from tool execution requiring user approval", + "type": "object", + "required": ["id", "sessionID", "permission", "patterns", "metadata", "always"], + "properties": { + "id": { + "type": "string", + "pattern": "^per", + "description": "Permission request identifier (prefixed with per)" + }, + "sessionID": { + "type": "string", + "pattern": "^ses", + "description": "Session identifier (prefixed with ses)" + }, + "permission": { + "type": "string", + "description": "Permission type being requested" + }, + "patterns": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Patterns to match for permission (e.g., file globs)" + }, + "metadata": { + "type": "object", + "additionalProperties": true, + "description": "Additional metadata about the permission request" + }, + "always": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Patterns to remember if user approves 'always'" + }, + "tool": { + "$ref": "permissionToolContext.schema.json", + "description": "Tool context that triggered the permission request" + } + } +} diff --git a/schema/permissionRule.schema.json b/schema/permissionRule.schema.json new file mode 100644 index 00000000000..9031ead5603 --- /dev/null +++ b/schema/permissionRule.schema.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/permissionRule.json", + "$comment": "Source: packages/opencode/src/permission/next.ts @ 47c670a (2026-01-03) lines 20-26", + "title": "PermissionRule", + "description": "Permission rule mapping a permission type and pattern to an action", + "type": "object", + "required": ["permission", "pattern", "action"], + "properties": { + "permission": { + "type": "string", + "description": "Permission type identifier" + }, + "pattern": { + "type": "string", + "description": "Pattern to match (supports wildcards)" + }, + "action": { + "$ref": "permissionAction.schema.json", + "description": "Action to take when pattern matches" + } + } +} diff --git a/schema/permissionRuleset.schema.json b/schema/permissionRuleset.schema.json new file mode 100644 index 00000000000..e9f2763df5e --- /dev/null +++ b/schema/permissionRuleset.schema.json @@ -0,0 +1,11 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/permissionRuleset.json", + "$comment": "Source: packages/opencode/src/permission/next.ts @ 47c670a (2026-01-03) lines 30-32", + "title": "PermissionRuleset", + "description": "Collection of permission rules defining access controls", + "type": "array", + "items": { + "$ref": "permissionRule.schema.json" + } +} diff --git a/schema/permissionToolContext.schema.json b/schema/permissionToolContext.schema.json new file mode 100644 index 00000000000..8ab4050e2c9 --- /dev/null +++ b/schema/permissionToolContext.schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/permissionToolContext.json", + "$comment": "Source: packages/opencode/src/permission/next.ts @ 21dc3c2 (2026-01-04) lines 60-64", + "title": "PermissionToolContext", + "description": "Tool context for permission request", + "type": "object", + "required": ["messageID", "callID"], + "properties": { + "messageID": { + "type": "string", + "description": "Message identifier containing the tool call" + }, + "callID": { + "type": "string", + "description": "Tool call identifier" + } + } +} diff --git a/schema/providerAuthError.schema.json b/schema/providerAuthError.schema.json new file mode 100644 index 00000000000..4d7244b3f89 --- /dev/null +++ b/schema/providerAuthError.schema.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/providerAuthError.json", + "$comment": "Source: packages/opencode/src/session/message-v2.ts @ current lines 38-43 (via NamedError.create)", + "title": "ProviderAuthError", + "description": "Provider authentication error (wrapped in NamedError format)", + "type": "object", + "required": ["name", "data"], + "properties": { + "name": { + "type": "string", + "const": "ProviderAuthError", + "description": "Error type name" + }, + "data": { + "type": "object", + "required": ["providerID", "message"], + "properties": { + "providerID": { + "type": "string", + "description": "Provider that failed authentication" + }, + "message": { + "type": "string", + "description": "Error message" + } + } + } + } +} diff --git a/schema/providerInfo.schema.json b/schema/providerInfo.schema.json new file mode 100644 index 00000000000..563ba08576e --- /dev/null +++ b/schema/providerInfo.schema.json @@ -0,0 +1,61 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/providerInfo.json", + "$comment": "Source: packages/opencode/src/provider/provider.ts @ d72d7ab (2026-01-04) lines 479-491", + "title": "ProviderInfo", + "description": "AI model provider metadata aggregating models and configuration. Providers expose collections of models with specific configurations, authentication requirements, and API endpoints.", + "type": "object", + "required": ["id", "name", "source", "env", "options", "models"], + "properties": { + "id": { + "type": "string", + "description": "Unique provider identifier", + "examples": [ + "anthropic", + "openai", + "google", + "azure", + "github-copilot" + ] + }, + "name": { + "type": "string", + "description": "Human-readable provider name", + "examples": ["Anthropic", "OpenAI", "Google"] + }, + "source": { + "$ref": "providerSource.schema.json" + }, + "env": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Environment variables used for authentication", + "examples": [ + ["ANTHROPIC_API_KEY"], + ["OPENAI_API_KEY"] + ] + }, + "key": { + "type": "string", + "description": "Partial/masked API key hint (for display purposes)" + }, + "options": { + "$ref": "providerOptions.schema.json" + }, + "models": { + "type": "object", + "description": "Curated models for this provider (configured/enabled models only, not all discoverable models)", + "additionalProperties": { + "$ref": "modelInfo.schema.json" + } + } + }, + "x-design-notes": { + "provider-vs-model": "A Provider (Anthropic, OpenAI) exposes multiple Models (Claude, GPT)", + "authentication": "Each provider has its own authentication, API endpoint, and configuration", + "model-sharing": "Models within a provider share the same auth credentials", + "curated-models": "models field contains only curated models (configured/enabled), not all discoverable models" + } +} diff --git a/schema/providerList.schema.json b/schema/providerList.schema.json new file mode 100644 index 00000000000..a1fa40148ed --- /dev/null +++ b/schema/providerList.schema.json @@ -0,0 +1,36 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/providerList.json", + "$comment": "Source: packages/opencode/src/server/server.ts @ 21dc3c2 (2026-01-04) lines 1698-1700 (GET /config/providers response schema)\nUsage: Generated into SDK as ProviderListResponse type (packages/sdk/js/src/v2/gen/types.gen.ts)\nNote: This schema defines the API response structure, not a direct TypeScript type", + "title": "ProviderList", + "description": "List of all available providers with their default model selections", + "type": "object", + "required": ["providers", "default"], + "properties": { + "providers": { + "type": "array", + "items": { + "$ref": "providerInfo.schema.json" + }, + "description": "All configured providers with their curated models" + }, + "default": { + "type": "object", + "description": "Default model ID per provider ID", + "additionalProperties": { + "type": "string" + }, + "examples": [ + { + "anthropic": "claude-3-5-sonnet-20241022", + "openai": "gpt-4", + "google": "gemini-2.0-flash-exp" + } + ] + } + }, + "x-api-mapping": { + "endpoint": "GET /config/providers", + "description": "Returns all configured providers with their curated model lists" + } +} diff --git a/schema/providerOptions.schema.json b/schema/providerOptions.schema.json new file mode 100644 index 00000000000..a3826c88b87 --- /dev/null +++ b/schema/providerOptions.schema.json @@ -0,0 +1,44 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/providerOptions.json", + "$comment": "Source: packages/opencode/src/provider/provider.ts @ d72d7ab (2026-01-04) lines 481, 880-920 (SDK initialization usage)\nNote: Flexible schema because different providers support different options. Common fields documented, but additional properties allowed.", + "title": "ProviderOptions", + "description": "Provider SDK initialization options (flexible - any properties allowed)", + "type": "object", + "additionalProperties": true, + "properties": { + "baseURL": { + "type": "string", + "format": "uri", + "description": "API endpoint override", + "examples": [ + "https://api.openai.com/v1", + "https://api.anthropic.com" + ] + }, + "apiKey": { + "type": "string", + "description": "API authentication key" + }, + "headers": { + "type": "object", + "description": "Custom HTTP headers (e.g., Anthropic beta feature flags)", + "additionalProperties": { + "type": "string" + }, + "examples": [ + { + "anthropic-beta": "claude-code-20250219,interleaved-thinking-2025-05-14" + } + ] + }, + "timeout": { + "type": "integer", + "minimum": 0, + "description": "Request timeout in milliseconds" + }, + "fetch": { + "description": "Custom fetch implementation (function, not serializable in JSON schema)" + } + } +} diff --git a/schema/providerSource.schema.json b/schema/providerSource.schema.json new file mode 100644 index 00000000000..94af2502a0d --- /dev/null +++ b/schema/providerSource.schema.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/providerSource.json", + "$comment": "Source: packages/opencode/src/provider/provider.ts @ d72d7ab (2026-01-04) line 481", + "title": "ProviderSource", + "description": "How the provider was configured or discovered", + "type": "string", + "enum": ["env", "config", "custom", "api"], + "x-enum-descriptions": { + "env": "Configured via environment variables", + "config": "Configured via config file", + "custom": "Custom provider definition", + "api": "Discovered via API" + } +} diff --git a/schema/reasoningPart.schema.json b/schema/reasoningPart.schema.json new file mode 100644 index 00000000000..f8c08d065fd --- /dev/null +++ b/schema/reasoningPart.schema.json @@ -0,0 +1,52 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/reasoningPart.json", + "$comment": "Source: packages/opencode/src/session/message-v2.ts @ current lines 98-109", + "title": "ReasoningPart", + "description": "Reasoning/thinking content part with required timing", + "type": "object", + "required": ["id", "sessionID", "messageID", "type", "text", "time"], + "properties": { + "id": { + "type": "string", + "description": "Part identifier" + }, + "sessionID": { + "type": "string", + "description": "Session identifier" + }, + "messageID": { + "type": "string", + "description": "Message identifier" + }, + "type": { + "type": "string", + "const": "reasoning", + "description": "Part type discriminator" + }, + "text": { + "type": "string", + "description": "Reasoning text content" + }, + "metadata": { + "type": "object", + "additionalProperties": true, + "description": "Additional metadata" + }, + "time": { + "type": "object", + "description": "Timing information", + "required": ["start"], + "properties": { + "start": { + "type": "number", + "description": "Start timestamp (ms)" + }, + "end": { + "type": "number", + "description": "End timestamp (ms)" + } + } + } + } +} diff --git a/schema/resourceSource.schema.json b/schema/resourceSource.schema.json new file mode 100644 index 00000000000..5504b60b0ba --- /dev/null +++ b/schema/resourceSource.schema.json @@ -0,0 +1,28 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/resourceSource.json", + "$comment": "Source: packages/opencode/src/session/message-v2.ts @ 21dc3c2 (2026-01-04) lines 120-126", + "title": "ResourceSource", + "description": "MCP resource-based source with client name and URI", + "type": "object", + "required": ["type", "clientName", "uri", "text"], + "properties": { + "type": { + "type": "string", + "const": "resource", + "description": "Source type discriminator" + }, + "clientName": { + "type": "string", + "description": "MCP client name" + }, + "uri": { + "type": "string", + "description": "Resource URI" + }, + "text": { + "$ref": "filePartSourceText.schema.json", + "description": "Text content with line range" + } + } +} diff --git a/schema/retryPart.schema.json b/schema/retryPart.schema.json new file mode 100644 index 00000000000..8dfdc2972da --- /dev/null +++ b/schema/retryPart.schema.json @@ -0,0 +1,47 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/retryPart.json", + "$comment": "Source: packages/opencode/src/session/message-v2.ts @ current lines 195-205", + "title": "RetryPart", + "description": "Retry marker part with attempt count and error details", + "type": "object", + "required": ["id", "sessionID", "messageID", "type", "attempt", "error", "time"], + "properties": { + "id": { + "type": "string", + "description": "Part identifier" + }, + "sessionID": { + "type": "string", + "description": "Session identifier" + }, + "messageID": { + "type": "string", + "description": "Message identifier" + }, + "type": { + "type": "string", + "const": "retry", + "description": "Part type discriminator" + }, + "attempt": { + "type": "number", + "description": "Retry attempt number" + }, + "error": { + "$ref": "apiError.schema.json", + "description": "Error that triggered the retry" + }, + "time": { + "type": "object", + "description": "Timing information", + "required": ["created"], + "properties": { + "created": { + "type": "number", + "description": "Creation timestamp (ms)" + } + } + } + } +} diff --git a/schema/sessionCreatedEvent.schema.json b/schema/sessionCreatedEvent.schema.json new file mode 100644 index 00000000000..ab5b45c4ec6 --- /dev/null +++ b/schema/sessionCreatedEvent.schema.json @@ -0,0 +1,28 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/sessionCreatedEvent.json", + "$comment": "Source: packages/opencode/src/session/index.ts @ 146a9b8ab (2026-01-05) lines 57-62", + "title": "SessionCreatedEvent", + "description": "Event payload for session.created - new session created", + "type": "object", + "required": ["type", "properties"], + "properties": { + "type": { + "type": "string", + "const": "session.created", + "description": "Event type discriminator" + }, + "properties": { + "type": "object", + "required": ["info"], + "properties": { + "info": { + "$ref": "sessionInfo.schema.json", + "description": "Created session information" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false +} diff --git a/schema/sessionDeletedEvent.schema.json b/schema/sessionDeletedEvent.schema.json new file mode 100644 index 00000000000..0b7d7cb13e7 --- /dev/null +++ b/schema/sessionDeletedEvent.schema.json @@ -0,0 +1,28 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/sessionDeletedEvent.json", + "$comment": "Source: packages/opencode/src/session/index.ts @ 146a9b8ab (2026-01-05) lines 69-74", + "title": "SessionDeletedEvent", + "description": "Event payload for session.deleted - session deleted", + "type": "object", + "required": ["type", "properties"], + "properties": { + "type": { + "type": "string", + "const": "session.deleted", + "description": "Event type discriminator" + }, + "properties": { + "type": "object", + "required": ["info"], + "properties": { + "info": { + "$ref": "sessionInfo.schema.json", + "description": "Deleted session information" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false +} diff --git a/schema/sessionInfo.schema.json b/schema/sessionInfo.schema.json new file mode 100644 index 00000000000..d3253a1d898 --- /dev/null +++ b/schema/sessionInfo.schema.json @@ -0,0 +1,57 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/sessionInfo.json", + "$comment": "Source: packages/opencode/src/session/index.ts @ 351ddee (2026-01-01) lines 39-79", + "title": "SessionInfo", + "description": "Core session identity and metadata (server-managed)", + "type": "object", + "required": ["id", "projectID", "directory", "title", "version", "time"], + "properties": { + "id": { + "type": "string", + "pattern": "^ses_", + "description": "Unique session identifier (prefixed with ses_)" + }, + "projectID": { + "type": "string", + "description": "Project identifier this session belongs to" + }, + "directory": { + "type": "string", + "description": "Working directory for this session" + }, + "parentID": { + "type": "string", + "pattern": "^ses_", + "description": "Parent session ID (for forked/child sessions, prefixed with ses_)" + }, + "summary": { + "$ref": "sessionSummary.schema.json", + "description": "Session summary statistics" + }, + "share": { + "$ref": "sessionShare.schema.json", + "description": "Share information if session is publicly shared" + }, + "title": { + "type": "string", + "description": "Session title" + }, + "version": { + "type": "string", + "description": "OpenCode version that created this session" + }, + "time": { + "$ref": "sessionTime.schema.json", + "description": "Timestamp metadata" + }, + "permission": { + "$ref": "permissionRuleset.schema.json", + "description": "Permission rules for this session" + }, + "revert": { + "$ref": "sessionRevert.schema.json", + "description": "Revert state information" + } + } +} diff --git a/schema/sessionList.schema.json b/schema/sessionList.schema.json new file mode 100644 index 00000000000..85f7d614e55 --- /dev/null +++ b/schema/sessionList.schema.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/sessionList.json", + "$comment": "Usage: API response type for listing sessions, generated into SDK types", + "title": "SessionList", + "description": "List of sessions (API response)", + "type": "object", + "required": ["sessions"], + "properties": { + "sessions": { + "type": "array", + "description": "Array of session metadata", + "items": { + "$ref": "sessionInfo.schema.json" + } + } + } +} diff --git a/schema/sessionRevert.schema.json b/schema/sessionRevert.schema.json new file mode 100644 index 00000000000..a4b5356c361 --- /dev/null +++ b/schema/sessionRevert.schema.json @@ -0,0 +1,27 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/sessionRevert.json", + "$comment": "Source: packages/opencode/src/session/index.ts @ 351ddee (2026-01-01) lines 67-73", + "title": "SessionRevert", + "description": "Session revert state information (for reverting to a previous message)", + "type": "object", + "required": ["messageID"], + "properties": { + "messageID": { + "type": "string", + "description": "Message ID to revert to" + }, + "partID": { + "type": "string", + "description": "Specific part ID within the message to revert to (optional)" + }, + "snapshot": { + "type": "string", + "description": "Snapshot identifier for the revert point (optional)" + }, + "diff": { + "type": "string", + "description": "Diff information for the revert (optional)" + } + } +} diff --git a/schema/sessionShare.schema.json b/schema/sessionShare.schema.json new file mode 100644 index 00000000000..1d416ea909f --- /dev/null +++ b/schema/sessionShare.schema.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/sessionShare.json", + "$comment": "Source: packages/opencode/src/session/index.ts @ 351ddee (2026-01-01) lines 53-56", + "title": "SessionShare", + "description": "Session share information (public URL)", + "type": "object", + "required": ["url"], + "properties": { + "url": { + "type": "string", + "description": "Public URL for shared session" + } + } +} diff --git a/schema/sessionStatus.schema.json b/schema/sessionStatus.schema.json new file mode 100644 index 00000000000..3adcfa43b5d --- /dev/null +++ b/schema/sessionStatus.schema.json @@ -0,0 +1,54 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/sessionStatus.json", + "$comment": "Source: packages/opencode/src/session/status.ts @ 146a9b8ab (2026-01-05) lines 7-25", + "title": "SessionStatus", + "description": "Session status information (idle, retry, or busy)", + "oneOf": [ + { + "type": "object", + "required": ["type"], + "properties": { + "type": { + "type": "string", + "const": "idle" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": ["type", "attempt", "message", "next"], + "properties": { + "type": { + "type": "string", + "const": "retry" + }, + "attempt": { + "type": "number", + "description": "Retry attempt number" + }, + "message": { + "type": "string", + "description": "Retry message/reason" + }, + "next": { + "type": "number", + "description": "Timestamp for next retry attempt" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": ["type"], + "properties": { + "type": { + "type": "string", + "const": "busy" + } + }, + "additionalProperties": false + } + ] +} diff --git a/schema/sessionStatusEvent.schema.json b/schema/sessionStatusEvent.schema.json new file mode 100644 index 00000000000..a76b605fe55 --- /dev/null +++ b/schema/sessionStatusEvent.schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/sessionStatusEvent.json", + "$comment": "Source: packages/opencode/src/session/status.ts @ 146a9b8ab (2026-01-05) lines 28-34", + "title": "SessionStatusEvent", + "description": "Event payload for session.status - session status changed", + "type": "object", + "required": ["type", "properties"], + "properties": { + "type": { + "type": "string", + "const": "session.status", + "description": "Event type discriminator" + }, + "properties": { + "type": "object", + "required": ["sessionID", "status"], + "properties": { + "sessionID": { + "type": "string", + "description": "Session identifier" + }, + "status": { + "$ref": "sessionStatus.schema.json", + "description": "New session status" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false +} diff --git a/schema/sessionSummary.schema.json b/schema/sessionSummary.schema.json new file mode 100644 index 00000000000..a1dae453636 --- /dev/null +++ b/schema/sessionSummary.schema.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/sessionSummary.json", + "$comment": "Source: packages/opencode/src/session/index.ts @ 351ddee (2026-01-01) lines 45-51", + "title": "SessionSummary", + "description": "Session summary statistics (file changes, additions, deletions)", + "type": "object", + "required": ["additions", "deletions", "files"], + "properties": { + "additions": { + "type": "number", + "description": "Total number of lines added across all files" + }, + "deletions": { + "type": "number", + "description": "Total number of lines deleted across all files" + }, + "files": { + "type": "number", + "description": "Number of files modified" + }, + "diffs": { + "type": "array", + "description": "Detailed file-by-file diff information (optional)", + "items": { + "$ref": "fileDiff.schema.json" + } + } + } +} diff --git a/schema/sessionTime.schema.json b/schema/sessionTime.schema.json new file mode 100644 index 00000000000..64967dea7d9 --- /dev/null +++ b/schema/sessionTime.schema.json @@ -0,0 +1,27 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/sessionTime.json", + "$comment": "Source: packages/opencode/src/session/index.ts @ 351ddee (2026-01-01) lines 60-65", + "title": "SessionTime", + "description": "Session timestamp metadata (created, updated, compacting, archived)", + "type": "object", + "required": ["created", "updated"], + "properties": { + "created": { + "type": "number", + "description": "Unix timestamp in milliseconds when session was created" + }, + "updated": { + "type": "number", + "description": "Unix timestamp in milliseconds when session was last updated" + }, + "compacting": { + "type": "number", + "description": "Unix timestamp in milliseconds when session compaction started (optional)" + }, + "archived": { + "type": "number", + "description": "Unix timestamp in milliseconds when session was archived (optional)" + } + } +} diff --git a/schema/sessionUpdatedEvent.schema.json b/schema/sessionUpdatedEvent.schema.json new file mode 100644 index 00000000000..c89d0b3d3f4 --- /dev/null +++ b/schema/sessionUpdatedEvent.schema.json @@ -0,0 +1,28 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/sessionUpdatedEvent.json", + "$comment": "Source: packages/opencode/src/session/index.ts @ 146a9b8ab (2026-01-05) lines 63-68", + "title": "SessionUpdatedEvent", + "description": "Event payload for session.updated - session metadata changed", + "type": "object", + "required": ["type", "properties"], + "properties": { + "type": { + "type": "string", + "const": "session.updated", + "description": "Event type discriminator" + }, + "properties": { + "type": "object", + "required": ["info"], + "properties": { + "info": { + "$ref": "sessionInfo.schema.json", + "description": "Updated session information" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false +} diff --git a/schema/snapshotPart.schema.json b/schema/snapshotPart.schema.json new file mode 100644 index 00000000000..614a325a2aa --- /dev/null +++ b/schema/snapshotPart.schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/snapshotPart.json", + "$comment": "Source: packages/opencode/src/session/message-v2.ts @ current lines 64-70", + "title": "SnapshotPart", + "description": "Snapshot reference part", + "type": "object", + "required": ["id", "sessionID", "messageID", "type", "snapshot"], + "properties": { + "id": { + "type": "string", + "description": "Part identifier" + }, + "sessionID": { + "type": "string", + "description": "Session identifier" + }, + "messageID": { + "type": "string", + "description": "Message identifier" + }, + "type": { + "type": "string", + "const": "snapshot", + "description": "Part type discriminator" + }, + "snapshot": { + "type": "string", + "description": "Snapshot identifier or reference" + } + } +} diff --git a/schema/stepFinishPart.schema.json b/schema/stepFinishPart.schema.json new file mode 100644 index 00000000000..bce5e583bd5 --- /dev/null +++ b/schema/stepFinishPart.schema.json @@ -0,0 +1,74 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/stepFinishPart.json", + "$comment": "Source: packages/opencode/src/session/message-v2.ts @ current lines 215-232", + "title": "StepFinishPart", + "description": "Step finish marker with token usage and cost", + "type": "object", + "required": ["id", "sessionID", "messageID", "type", "reason", "cost", "tokens"], + "properties": { + "id": { + "type": "string", + "description": "Part identifier" + }, + "sessionID": { + "type": "string", + "description": "Session identifier" + }, + "messageID": { + "type": "string", + "description": "Message identifier" + }, + "type": { + "type": "string", + "const": "step-finish", + "description": "Part type discriminator" + }, + "reason": { + "type": "string", + "description": "Reason for step completion" + }, + "snapshot": { + "type": "string", + "description": "Optional snapshot reference at step finish" + }, + "cost": { + "type": "number", + "description": "Cost of this step" + }, + "tokens": { + "type": "object", + "description": "Token usage breakdown", + "required": ["input", "output", "reasoning", "cache"], + "properties": { + "input": { + "type": "number", + "description": "Input tokens used" + }, + "output": { + "type": "number", + "description": "Output tokens generated" + }, + "reasoning": { + "type": "number", + "description": "Reasoning tokens used" + }, + "cache": { + "type": "object", + "description": "Cache token usage", + "required": ["read", "write"], + "properties": { + "read": { + "type": "number", + "description": "Cache read tokens" + }, + "write": { + "type": "number", + "description": "Cache write tokens" + } + } + } + } + } + } +} diff --git a/schema/stepStartPart.schema.json b/schema/stepStartPart.schema.json new file mode 100644 index 00000000000..728d27986ae --- /dev/null +++ b/schema/stepStartPart.schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/stepStartPart.json", + "$comment": "Source: packages/opencode/src/session/message-v2.ts @ current lines 207-213", + "title": "StepStartPart", + "description": "Step start marker with optional snapshot reference", + "type": "object", + "required": ["id", "sessionID", "messageID", "type"], + "properties": { + "id": { + "type": "string", + "description": "Part identifier" + }, + "sessionID": { + "type": "string", + "description": "Session identifier" + }, + "messageID": { + "type": "string", + "description": "Message identifier" + }, + "type": { + "type": "string", + "const": "step-start", + "description": "Part type discriminator" + }, + "snapshot": { + "type": "string", + "description": "Optional snapshot reference at step start" + } + } +} diff --git a/schema/subtaskPart.schema.json b/schema/subtaskPart.schema.json new file mode 100644 index 00000000000..a568c41434b --- /dev/null +++ b/schema/subtaskPart.schema.json @@ -0,0 +1,44 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/subtaskPart.json", + "$comment": "Source: packages/opencode/src/session/message-v2.ts @ current lines 186-193", + "title": "SubtaskPart", + "description": "Subtask reference part for delegated work", + "type": "object", + "required": ["id", "sessionID", "messageID", "type", "prompt", "description", "agent"], + "properties": { + "id": { + "type": "string", + "description": "Part identifier" + }, + "sessionID": { + "type": "string", + "description": "Session identifier" + }, + "messageID": { + "type": "string", + "description": "Message identifier" + }, + "type": { + "type": "string", + "const": "subtask", + "description": "Part type discriminator" + }, + "prompt": { + "type": "string", + "description": "Subtask prompt/instruction" + }, + "description": { + "type": "string", + "description": "Subtask description" + }, + "agent": { + "type": "string", + "description": "Agent handling the subtask" + }, + "command": { + "type": "string", + "description": "Optional command associated with subtask" + } + } +} diff --git a/schema/symbolSource.schema.json b/schema/symbolSource.schema.json new file mode 100644 index 00000000000..6c1a615216b --- /dev/null +++ b/schema/symbolSource.schema.json @@ -0,0 +1,36 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/symbolSource.json", + "$comment": "Source: packages/opencode/src/session/message-v2.ts @ 21dc3c2 (2026-01-04) lines 110-118", + "title": "SymbolSource", + "description": "Symbol-based source with LSP range information", + "type": "object", + "required": ["type", "path", "range", "name", "kind", "text"], + "properties": { + "type": { + "type": "string", + "const": "symbol", + "description": "Source type discriminator" + }, + "path": { + "type": "string", + "description": "File path containing the symbol" + }, + "range": { + "$ref": "lspRange.schema.json", + "description": "LSP range of the symbol" + }, + "name": { + "type": "string", + "description": "Symbol name" + }, + "kind": { + "type": "integer", + "description": "LSP symbol kind" + }, + "text": { + "$ref": "filePartSourceText.schema.json", + "description": "Text content with line range" + } + } +} diff --git a/schema/textPart.schema.json b/schema/textPart.schema.json new file mode 100644 index 00000000000..e5a6f1db9d4 --- /dev/null +++ b/schema/textPart.schema.json @@ -0,0 +1,60 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/textPart.json", + "$comment": "Source: packages/opencode/src/session/message-v2.ts @ current lines 81-96", + "title": "TextPart", + "description": "Text content part with optional timing and metadata", + "type": "object", + "required": ["id", "sessionID", "messageID", "type", "text"], + "properties": { + "id": { + "type": "string", + "description": "Part identifier" + }, + "sessionID": { + "type": "string", + "description": "Session identifier" + }, + "messageID": { + "type": "string", + "description": "Message identifier" + }, + "type": { + "type": "string", + "const": "text", + "description": "Part type discriminator" + }, + "text": { + "type": "string", + "description": "Text content" + }, + "synthetic": { + "type": "boolean", + "description": "Whether this text was synthetically generated" + }, + "ignored": { + "type": "boolean", + "description": "Whether this text should be ignored in processing" + }, + "time": { + "type": "object", + "description": "Timing information", + "required": ["start"], + "properties": { + "start": { + "type": "number", + "description": "Start timestamp (ms)" + }, + "end": { + "type": "number", + "description": "End timestamp (ms)" + } + } + }, + "metadata": { + "type": "object", + "additionalProperties": true, + "description": "Additional metadata" + } + } +} diff --git a/schema/thinkingConfig.schema.json b/schema/thinkingConfig.schema.json new file mode 100644 index 00000000000..44c692e4d66 --- /dev/null +++ b/schema/thinkingConfig.schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/thinkingConfig.json", + "$comment": "Source: packages/opencode/src/provider/transform.ts @ 2fd9737 (2026-01-02) lines 357-379", + "title": "ThinkingConfig", + "description": "Google Gemini thinking process configuration", + "type": "object", + "properties": { + "includeThoughts": { + "type": "boolean", + "description": "Include thinking process in response" + }, + "thinkingBudget": { + "type": "integer", + "minimum": 0, + "description": "Maximum tokens allocated for thinking (0 = minimal)" + } + } +} diff --git a/schema/thinkingOptions.schema.json b/schema/thinkingOptions.schema.json new file mode 100644 index 00000000000..f0e18573337 --- /dev/null +++ b/schema/thinkingOptions.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/thinkingOptions.json", + "$comment": "Source: packages/opencode/src/provider/transform.ts @ 2fd9737 (2026-01-02) lines 323-340", + "title": "ThinkingOptions", + "description": "Anthropic Claude extended thinking configuration", + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Thinking mode type", + "examples": ["enabled"] + }, + "budgetTokens": { + "type": "integer", + "minimum": 0, + "description": "Maximum tokens allocated for extended thinking" + } + } +} diff --git a/schema/toolPart.schema.json b/schema/toolPart.schema.json new file mode 100644 index 00000000000..d96dec28a69 --- /dev/null +++ b/schema/toolPart.schema.json @@ -0,0 +1,45 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/toolPart.json", + "$comment": "Source: packages/opencode/src/session/message-v2.ts @ 21dc3c2 (2026-01-04) lines 282-291", + "title": "ToolPart", + "description": "Tool call message part with state tracking", + "type": "object", + "required": ["id", "sessionID", "messageID", "type", "callID", "tool", "state"], + "properties": { + "id": { + "type": "string", + "description": "Part identifier" + }, + "sessionID": { + "type": "string", + "description": "Session identifier" + }, + "messageID": { + "type": "string", + "description": "Message identifier" + }, + "type": { + "type": "string", + "const": "tool", + "description": "Part type discriminator" + }, + "callID": { + "type": "string", + "description": "Tool call identifier" + }, + "tool": { + "type": "string", + "description": "Tool name (e.g., bash, read, write, glob)" + }, + "state": { + "$ref": "toolState.schema.json", + "description": "Current tool execution state" + }, + "metadata": { + "type": "object", + "additionalProperties": true, + "description": "Additional metadata" + } + } +} diff --git a/schema/toolState.schema.json b/schema/toolState.schema.json new file mode 100644 index 00000000000..9ee5865d444 --- /dev/null +++ b/schema/toolState.schema.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/toolState.json", + "$comment": "Source: packages/opencode/src/session/message-v2.ts @ 21dc3c2 (2026-01-04) lines 276-280", + "title": "ToolState", + "description": "Discriminated union of tool execution states (pending, running, completed, error)", + "oneOf": [ + { "$ref": "toolStatePending.schema.json" }, + { "$ref": "toolStateRunning.schema.json" }, + { "$ref": "toolStateCompleted.schema.json" }, + { "$ref": "toolStateError.schema.json" } + ] +} diff --git a/schema/toolStateCompleted.schema.json b/schema/toolStateCompleted.schema.json new file mode 100644 index 00000000000..1b80741be4a --- /dev/null +++ b/schema/toolStateCompleted.schema.json @@ -0,0 +1,60 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/toolStateCompleted.json", + "$comment": "Source: packages/opencode/src/session/message-v2.ts @ 21dc3c2 (2026-01-04) lines 241-258", + "title": "ToolStateCompleted", + "description": "Tool state when completed successfully", + "type": "object", + "required": ["status", "input", "output", "title", "metadata", "time"], + "properties": { + "status": { + "type": "string", + "const": "completed", + "description": "Tool state discriminator" + }, + "input": { + "type": "object", + "additionalProperties": true, + "description": "Tool input parameters" + }, + "output": { + "type": "string", + "description": "Tool output result" + }, + "title": { + "type": "string", + "description": "Display title for the tool execution" + }, + "metadata": { + "type": "object", + "additionalProperties": true, + "description": "Additional metadata" + }, + "time": { + "type": "object", + "required": ["start", "end"], + "properties": { + "start": { + "type": "number", + "description": "Start timestamp (milliseconds)" + }, + "end": { + "type": "number", + "description": "End timestamp (milliseconds)" + }, + "compacted": { + "type": "number", + "description": "Compaction timestamp (milliseconds)" + } + }, + "description": "Timing information" + }, + "attachments": { + "type": "array", + "items": { + "$ref": "filePart.schema.json" + }, + "description": "File attachments from tool execution" + } + } +} diff --git a/schema/toolStateError.schema.json b/schema/toolStateError.schema.json new file mode 100644 index 00000000000..bc07a9bcc82 --- /dev/null +++ b/schema/toolStateError.schema.json @@ -0,0 +1,45 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/toolStateError.json", + "$comment": "Source: packages/opencode/src/session/message-v2.ts @ 21dc3c2 (2026-01-04) lines 260-274", + "title": "ToolStateError", + "description": "Tool state when execution failed with error", + "type": "object", + "required": ["status", "input", "error", "time"], + "properties": { + "status": { + "type": "string", + "const": "error", + "description": "Tool state discriminator" + }, + "input": { + "type": "object", + "additionalProperties": true, + "description": "Tool input parameters" + }, + "error": { + "type": "string", + "description": "Error message" + }, + "metadata": { + "type": "object", + "additionalProperties": true, + "description": "Additional metadata" + }, + "time": { + "type": "object", + "required": ["start", "end"], + "properties": { + "start": { + "type": "number", + "description": "Start timestamp (milliseconds)" + }, + "end": { + "type": "number", + "description": "End timestamp (milliseconds)" + } + }, + "description": "Timing information" + } + } +} diff --git a/schema/toolStatePending.schema.json b/schema/toolStatePending.schema.json new file mode 100644 index 00000000000..43ff1d12fde --- /dev/null +++ b/schema/toolStatePending.schema.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/toolStatePending.json", + "$comment": "Source: packages/opencode/src/session/message-v2.ts @ 21dc3c2 (2026-01-04) lines 214-224", + "title": "ToolStatePending", + "description": "Tool state when pending execution", + "type": "object", + "required": ["status", "input", "raw"], + "properties": { + "status": { + "type": "string", + "const": "pending", + "description": "Tool state discriminator" + }, + "input": { + "type": "object", + "additionalProperties": true, + "description": "Tool input parameters" + }, + "raw": { + "type": "string", + "description": "Raw input string" + } + } +} diff --git a/schema/toolStateRunning.schema.json b/schema/toolStateRunning.schema.json new file mode 100644 index 00000000000..84bd8d5c8b0 --- /dev/null +++ b/schema/toolStateRunning.schema.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/toolStateRunning.json", + "$comment": "Source: packages/opencode/src/session/message-v2.ts @ 21dc3c2 (2026-01-04) lines 226-239", + "title": "ToolStateRunning", + "description": "Tool state when running", + "type": "object", + "required": ["status", "input", "time"], + "properties": { + "status": { + "type": "string", + "const": "running", + "description": "Tool state discriminator" + }, + "input": { + "type": "object", + "additionalProperties": true, + "description": "Tool input parameters" + }, + "title": { + "type": "string", + "description": "Display title for the tool execution" + }, + "metadata": { + "type": "object", + "additionalProperties": true, + "description": "Additional metadata" + }, + "time": { + "type": "object", + "required": ["start"], + "properties": { + "start": { + "type": "number", + "description": "Start timestamp (milliseconds)" + } + }, + "description": "Timing information" + } + } +} diff --git a/schema/universalOptions.schema.json b/schema/universalOptions.schema.json new file mode 100644 index 00000000000..2774355e2db --- /dev/null +++ b/schema/universalOptions.schema.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/universalOptions.json", + "$comment": "Source: packages/opencode/src/provider/transform.ts @ 2fd9737 (2026-01-02) lines 214, 525+", + "title": "UniversalOptions", + "description": "Universal settings supported by all models (when capabilities allow)", + "type": "object", + "properties": { + "temperature": { + "type": "number", + "minimum": 0.0, + "maximum": 2.0, + "description": "Temperature parameter for controlling randomness (only when model.capabilities.temperature == true)" + }, + "maxOutputTokens": { + "type": "integer", + "minimum": 0, + "description": "Override model's default output token limit" + } + } +} diff --git a/schema/unknownError.schema.json b/schema/unknownError.schema.json new file mode 100644 index 00000000000..ae8d8f8075a --- /dev/null +++ b/schema/unknownError.schema.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/unknownError.json", + "$comment": "Source: packages/util/src/error.ts @ current lines 48-53 (NamedError.Unknown)", + "title": "UnknownError", + "description": "Unknown error (wrapped in NamedError format)", + "type": "object", + "required": ["name", "data"], + "properties": { + "name": { + "type": "string", + "const": "UnknownError", + "description": "Error type name" + }, + "data": { + "type": "object", + "required": ["message"], + "properties": { + "message": { + "type": "string", + "description": "Error message" + } + } + } + } +} diff --git a/schema/userMessage.schema.json b/schema/userMessage.schema.json new file mode 100644 index 00000000000..2a060db2430 --- /dev/null +++ b/schema/userMessage.schema.json @@ -0,0 +1,91 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/userMessage.json", + "$comment": "Source: packages/opencode/src/session/message-v2.ts @ current lines 263-286", + "title": "UserMessage", + "description": "User message with model selection and optional summary", + "type": "object", + "required": ["id", "sessionID", "role", "time", "agent", "model"], + "properties": { + "id": { + "type": "string", + "description": "Message identifier" + }, + "sessionID": { + "type": "string", + "description": "Session identifier" + }, + "role": { + "type": "string", + "const": "user", + "description": "Message role discriminator" + }, + "time": { + "type": "object", + "description": "Message timing", + "required": ["created"], + "properties": { + "created": { + "type": "number", + "description": "Creation timestamp (ms)" + } + } + }, + "summary": { + "type": "object", + "description": "Optional message summary with diffs", + "properties": { + "title": { + "type": "string", + "description": "Summary title" + }, + "body": { + "type": "string", + "description": "Summary body" + }, + "diffs": { + "type": "array", + "items": { + "$ref": "fileDiff.schema.json" + }, + "description": "File diffs included in summary" + } + }, + "required": ["diffs"] + }, + "agent": { + "type": "string", + "description": "Agent name" + }, + "model": { + "type": "object", + "description": "Model selection", + "required": ["providerID", "modelID"], + "properties": { + "providerID": { + "type": "string", + "description": "Provider identifier" + }, + "modelID": { + "type": "string", + "description": "Model identifier" + } + } + }, + "system": { + "type": "string", + "description": "Optional system prompt override" + }, + "tools": { + "type": "object", + "additionalProperties": { + "type": "boolean" + }, + "description": "Tool enablement map" + }, + "variant": { + "type": "string", + "description": "Optional message variant" + } + } +} diff --git a/schema/wellKnownAuth.schema.json b/schema/wellKnownAuth.schema.json new file mode 100644 index 00000000000..c15e8eca620 --- /dev/null +++ b/schema/wellKnownAuth.schema.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://opencode.ai/schemas/v1/wellKnownAuth.json", + "$comment": "Source: packages/opencode/src/auth/index.ts @ c50f588 (2026-01-05) lines 24-30", + "title": "WellKnownAuth", + "description": "Well-known authentication credentials (key and token pair)", + "type": "object", + "required": ["type", "key", "token"], + "properties": { + "type": { + "type": "string", + "const": "wellknown", + "description": "Authentication type discriminator" + }, + "key": { + "type": "string", + "description": "Well-known authentication key" + }, + "token": { + "type": "string", + "description": "Well-known authentication token" + } + } +} diff --git a/script/generate-from-schemas.ts b/script/generate-from-schemas.ts new file mode 100755 index 00000000000..7b9fb826bad --- /dev/null +++ b/script/generate-from-schemas.ts @@ -0,0 +1,333 @@ +#!/usr/bin/env bun + +import { $ } from "bun" +import Ajv from "ajv" +import addFormats from "ajv-formats" +import { readdir, readFile, writeFile, mkdir } from "fs/promises" +import { join, dirname } from "path" +import { fileURLToPath } from "url" + +// Setup paths +const __dirname = dirname(fileURLToPath(import.meta.url)) +const SCHEMA_DIR = join(__dirname, "..", "schema") +const OUTPUT_DIR = join(__dirname, "..", "packages", "opencode", "generated", "validators") + +// Colors for console output +const colors = { + green: "\x1b[32m", + red: "\x1b[31m", + yellow: "\x1b[33m", + reset: "\x1b[0m", +} + +async function validateSchemas() { + console.log("šŸ” Validating JSON Schemas...") + + const ajv = new Ajv({ + allErrors: true, + strict: false, // Allow custom x- keywords + allowUnionTypes: true, + }) + addFormats(ajv) + + // Read all schema files + const files = await readdir(SCHEMA_DIR) + const schemaFiles = files.filter((f) => f.endsWith(".schema.json")) + + if (schemaFiles.length === 0) { + console.log(`${colors.red}āœ— No schema files found in ${SCHEMA_DIR}${colors.reset}`) + process.exit(1) + } + + // First pass: Load all schemas + const schemas = new Map() + for (const file of schemaFiles) { + const path = join(SCHEMA_DIR, file) + const content = await readFile(path, "utf-8") + try { + const schema = JSON.parse(content) + schemas.set(file, { schema, path }) + // Populate global cache for discriminator detection + globalSchemaCache.set(file, { schema, path }) + } catch (error) { + console.log(`${colors.red}āœ— ${file} - Invalid JSON: ${error.message}${colors.reset}`) + process.exit(1) + } + } + + // Second pass: Add all schemas to AJV without $id for file-based refs + for (const [file, { schema }] of schemas) { + try { + // Create a copy without $id so filename-based $ref works + const schemaForValidation = JSON.parse(JSON.stringify(schema)) + delete schemaForValidation.$id + delete schemaForValidation.$schema + + ajv.addSchema(schemaForValidation, file) + } catch (err) { + // Schema might already exist, that's ok + } + } + + // Third pass: Validate each schema + let allValid = true + for (const [file, { schema }] of schemas) { + try { + // Create validation copy without $id/$schema + const schemaForValidation = JSON.parse(JSON.stringify(schema)) + delete schemaForValidation.$id + delete schemaForValidation.$schema + + // Validate schema structure + ajv.compile(schemaForValidation) + console.log(`${colors.green}āœ“${colors.reset} ${file} - valid`) + } catch (error) { + console.log(`${colors.red}āœ— ${file} - ${error.message}${colors.reset}`) + allValid = false + } + } + + if (!allValid) { + console.log(`\n${colors.red}Schema validation failed!${colors.reset}`) + process.exit(1) + } + + console.log(`\n${colors.green}āœ“ All ${schemaFiles.length} schemas validated successfully${colors.reset}\n`) + return schemas +} + +// Global schema cache for resolving $ref during discriminator detection +let globalSchemaCache: Map = new Map() + +/** + * Find a discriminator property for a oneOf/anyOf union. + * A discriminator is a property that: + * 1. Exists in ALL members of the union + * 2. Has a `const` value in each member (literal type) + * 3. Each `const` value is unique across members + * + * Returns the property name if found, null otherwise. + */ +function findDiscriminator(schemas: any[], refMap: Map): string | null { + if (schemas.length < 2) return null + + // Resolve $ref schemas to get their actual definitions + const resolvedSchemas = schemas.map((s) => { + if (s.$ref) { + const refFile = s.$ref + const cached = globalSchemaCache.get(refFile) + if (cached) { + return cached.schema + } + } + return s + }) + + // Find candidate properties (properties with const in at least one schema) + const candidates = new Set() + for (const schema of resolvedSchemas) { + if (schema.properties) { + for (const [propName, propDef] of Object.entries(schema.properties)) { + if ((propDef as any).const !== undefined) { + candidates.add(propName) + } + } + } + } + + // Check each candidate to see if it's a valid discriminator + for (const candidate of candidates) { + const constValues = new Set() + let isValidDiscriminator = true + + for (const schema of resolvedSchemas) { + const prop = schema.properties?.[candidate] + if (!prop || prop.const === undefined) { + isValidDiscriminator = false + break + } + if (constValues.has(prop.const)) { + // Duplicate const value - not a valid discriminator + isValidDiscriminator = false + break + } + constValues.add(prop.const) + } + + if (isValidDiscriminator && constValues.size === resolvedSchemas.length) { + return candidate + } + } + + return null +} + +function jsonSchemaToZod(schema: any, refMap: Map, depth = 0): string { + // Handle $ref + if (schema.$ref) { + const refFile = schema.$ref + const refName = refFile.replace(".schema.json", "") + return refName + "Schema" + } + + // Handle basic types + if (schema.type === "string") { + if (schema.const !== undefined) { + return `z.literal("${schema.const}")` + } + if (schema.enum) { + const values = schema.enum.map((v) => `"${v}"`).join(", ") + return `z.enum([${values}])` + } + if (schema.pattern) { + // Escape backslashes in the pattern for JavaScript regex + const escapedPattern = schema.pattern.replace(/\\/g, "\\\\") + return `z.string().regex(/${escapedPattern}/)` + } + return "z.string()" + } + + if (schema.type === "number" || schema.type === "integer") { + if (schema.const !== undefined) { + return `z.literal(${schema.const})` + } + // Build number schema with optional constraints + let result = "z.number()" + // Use .int() for integer type to match z.number().int() behavior + if (schema.type === "integer") { + result += ".int()" + } + // Add minimum constraint if present + if (schema.minimum !== undefined) { + result += `.min(${schema.minimum})` + } + // Add maximum constraint if present + if (schema.maximum !== undefined) { + result += `.max(${schema.maximum})` + } + return result + } + + if (schema.type === "boolean") { + if (schema.const !== undefined) { + return `z.literal(${schema.const})` + } + return "z.boolean()" + } + + if (schema.type === "array") { + const items = schema.items ? jsonSchemaToZod(schema.items, refMap, depth + 1) : "z.any()" + return `z.array(${items})` + } + + if (schema.type === "object") { + if (schema.additionalProperties === true) { + return "z.record(z.string(), z.any())" + } + if (schema.additionalProperties) { + const valueType = jsonSchemaToZod(schema.additionalProperties, refMap, depth + 1) + return `z.record(z.string(), ${valueType})` + } + if (schema.properties) { + const props = Object.entries(schema.properties).map(([key, prop]: [string, any]) => { + let zodType = jsonSchemaToZod(prop, refMap, depth + 1) + const required = schema.required?.includes(key) + if (!required) { + zodType += ".optional()" + } + return ` ${key}: ${zodType},` + }) + return `z.object({\n${props.join("\n")}\n })` + } + return "z.record(z.string(), z.any())" + } + + // Handle unions + if (schema.oneOf || schema.anyOf) { + const schemas = schema.oneOf || schema.anyOf + const types = schemas.map((s: any) => jsonSchemaToZod(s, refMap, depth + 1)) + + // Check if this can be a discriminated union + // A discriminated union requires all members to have a common property with const literal + const discriminator = findDiscriminator(schemas, refMap) + if (discriminator) { + return `z.discriminatedUnion("${discriminator}", [${types.join(", ")}])` + } + + return `z.union([${types.join(", ")}])` + } + + return "z.any()" +} + +async function generateZodValidators(schemas: Map) { + console.log("šŸ”§ Generating Zod validators...") + + // Create output directory + await mkdir(OUTPUT_DIR, { recursive: true }) + + // Build reference map (filename -> schema name) + const refMap = new Map() + for (const [file, { schema }] of schemas) { + const name = file.replace(".schema.json", "") + refMap.set(file, name) + } + + // Generate each schema + for (const [file, { schema }] of schemas) { + const schemaName = file.replace(".schema.json", "") + const outputPath = join(OUTPUT_DIR, `${schemaName}.ts`) + + // Collect all $refs in this schema to generate imports + const refs = new Set() + function collectRefs(obj: any) { + if (!obj || typeof obj !== "object") return + if (obj.$ref) { + refs.add(obj.$ref.replace(".schema.json", "")) + } + for (const value of Object.values(obj)) { + collectRefs(value) + } + } + collectRefs(schema) + + // Generate imports + const imports = Array.from(refs) + .filter((ref) => ref !== schemaName) + .map((ref) => `import { ${ref}Schema } from "./${ref}"`) + .join("\n") + + // Generate Zod schema + const zodSchema = jsonSchemaToZod(schema, refMap) + + // Generate type export + const exportName = `${schemaName}Schema` + const typeName = schemaName.charAt(0).toUpperCase() + schemaName.slice(1) + + const content = `import { z } from "zod" +${imports ? imports + "\n" : ""} +export const ${exportName} = ${zodSchema} + +export type ${typeName} = z.infer +` + + await writeFile(outputPath, content) + console.log(`${colors.green}āœ“${colors.reset} ${schemaName}.ts - generated`) + } + + console.log(`\n${colors.green}āœ“ Code generation complete${colors.reset}`) +} + +async function main() { + console.log("šŸš€ JSON Schema Validation & Code Generation\n") + + const schemas = await validateSchemas() + await generateZodValidators(schemas) + + console.log("\n✨ Done!") +} + +main().catch((error) => { + console.error(`${colors.red}Error: ${error.message}${colors.reset}`) + process.exit(1) +}) diff --git a/script/generate.ts b/script/generate.ts index 8fc251d89d4..74b23e0b1a2 100755 --- a/script/generate.ts +++ b/script/generate.ts @@ -2,6 +2,8 @@ import { $ } from "bun" +await $`bun ./script/generate-from-schemas.ts` + await $`bun ./packages/sdk/js/script/build.ts` await $`bun dev generate > ../sdk/openapi.json`.cwd("packages/opencode")