From 9279769a66ab5f8b9454ebf4ed242aca8b8eb4ac Mon Sep 17 00:00:00 2001 From: Janghoon Lee <44862514+savagemanage@users.noreply.github.com> Date: Wed, 23 Sep 2026 05:57:13 +0000 Subject: [PATCH 1/2] feat(protocol): document the chat-completions SSE response The route answers with `application/json` or with `text/event-stream` depending on `stream`, and HttpApi can only declare one success schema. So the generated spec described only the JSON, and a client built from it looks for choices[].message.content on a streaming response and finds nothing on every frame - because a chunk carries choices[].delta instead. That is the same gap /event has, and the same fix site. Adds ChatCompletionChunk, which is genuinely a different shape rather than a reuse: the object discriminator is chat.completion.chunk, and each choice carries a partial delta instead of a complete message. Every delta field is optional because OpenAI's stream uses the shape sparsely - the first frame typically carries only role, middle frames only content, a tool call arrives spread across frames, and the terminator carries an empty delta with finish_reason set. A schema that required content would reject the terminator. Declared for documentation and codegen only. The handler stays handleRaw and writes its own SSE frames, because choosing between JSON and a stream per request is exactly what HttpApi cannot express. Both content types are declared on the one 200 rather than the SSE replacing the JSON, so the spec says what actually happens instead of lying in the other direction. Registered through HttpApi.AdditionalSchemas. A $ref to a schema no endpoint references dangles otherwise, and a dangling ref generates a client with a missing type rather than failing loudly - which is why one of the three new tests asserts the ref resolves to a registered component rather than only that the ref is present. Coverage 211/0/0, route and wire tests 28/28, openapi tests 21/21, protocol and redrob typecheck clean. The generated artifacts are NOT regenerated here. `bun script/generate.ts` also rewrites packages/sdk/openapi.json, which is stale on clean develop by 991 lines including routes that are not mine, so committing it would sweep another session's unregenerated surface into this change. It needs its own regeneration commit, which is being raised separately. --- .../protocol/src/groups/chat-completion.ts | 43 ++++++++++++++++++ .../src/server/routes/instance/httpapi/api.ts | 6 +++ .../server/routes/instance/httpapi/public.ts | 22 +++++++++ .../server/httpapi-public-openapi.test.ts | 45 +++++++++++++++++++ 4 files changed, 116 insertions(+) diff --git a/packages/protocol/src/groups/chat-completion.ts b/packages/protocol/src/groups/chat-completion.ts index dc1ebdde43..373baea9d8 100644 --- a/packages/protocol/src/groups/chat-completion.ts +++ b/packages/protocol/src/groups/chat-completion.ts @@ -132,6 +132,49 @@ export const ChatCompletionResponse = Schema.Struct({ }).annotate({ identifier: "ChatCompletionResponse" }) export type ChatCompletionResponse = typeof ChatCompletionResponse.Type +/** + * One frame of the `stream: true` response. + * + * A DIFFERENT shape from `ChatCompletionResponse`, which is why documenting the streaming + * path needed this rather than reusing the JSON one: the object discriminator is + * `chat.completion.chunk`, and each choice carries a partial `delta` instead of a complete + * `message`. A generated client that assumed the non-streaming shape would look for + * `choices[].message.content` and find nothing on every frame. + * + * Declared for documentation and codegen only — the handler is `handleRaw` and writes SSE + * frames itself, because one request answers with JSON and another with + * `text/event-stream`. This type is what makes the OpenAPI honest about the second. + * + * Every field on `delta` is optional because OpenAI's stream uses the shape sparsely: the + * first frame typically carries only `role`, middle frames only `content`, a tool call + * arrives spread across frames, and the terminator carries an empty delta with + * `finish_reason` set. A schema that required `content` would reject the terminator. + */ +export const ChatCompletionChunk = Schema.Struct({ + id: Schema.String, + object: Schema.Literal("chat.completion.chunk"), + created: Schema.Int, + model: Schema.String, + choices: Schema.Array( + Schema.Struct({ + index: Schema.Int, + delta: Schema.Struct({ + role: Schema.optional(Schema.Literal("assistant")), + content: Schema.optional(Schema.String), + tool_calls: Schema.optional(Schema.Array(ToolCall)), + reasoning_content: Schema.optional(Schema.String), + }), + finish_reason: Schema.NullOr(Schema.String), + }), + ), + /** + * Sent on the final frame by providers that report it. Optional because many do not, + * and a client must not wait for a frame that never comes. + */ + usage: Schema.optional(Usage), +}).annotate({ identifier: "ChatCompletionChunk" }) +export type ChatCompletionChunk = typeof ChatCompletionChunk.Type + /** * The OpenAI error envelope. * diff --git a/packages/redrob/src/server/routes/instance/httpapi/api.ts b/packages/redrob/src/server/routes/instance/httpapi/api.ts index 8010729d18..93418db36e 100644 --- a/packages/redrob/src/server/routes/instance/httpapi/api.ts +++ b/packages/redrob/src/server/routes/instance/httpapi/api.ts @@ -28,6 +28,7 @@ import { WorkspaceApi } from "./groups/workspace" import { makeApi } from "@redrob-code/protocol/api" import { LocationMiddleware } from "@redrob-code/server/location" import { SessionLocationMiddleware } from "@redrob-code/server/middleware/session-location" +import { ChatCompletionChunk } from "@redrob-code/protocol/groups/chat-completion" import { GlobalApi } from "./groups/global" import { Authorization } from "./middleware/authorization" import { SchemaErrorMiddleware } from "./middleware/schema-error" @@ -91,6 +92,11 @@ export const RedrobHttpApi = HttpApi.make("redrob") Integration.Method, Integration.Ref, SkillV2.Source, + // Not referenced by any endpoint's declared success type -- the streaming shape is + // patched onto /v1/chat/completions in public.ts, because HttpApi cannot express "JSON + // or SSE depending on a request field". Registering it here is what makes that patch's + // $ref resolve instead of dangling in the generated spec. + ChatCompletionChunk, ]) export type RootHttpApiType = typeof RootHttpApi diff --git a/packages/redrob/src/server/routes/instance/httpapi/public.ts b/packages/redrob/src/server/routes/instance/httpapi/public.ts index 27d43b226c..9c64dd3729 100644 --- a/packages/redrob/src/server/routes/instance/httpapi/public.ts +++ b/packages/redrob/src/server/routes/instance/httpapi/public.ts @@ -169,6 +169,28 @@ function matchLegacyOpenApi(input: Record) { }, } } + if (path === "/v1/chat/completions" && method === "post") { + // Same gap, and worse consequences here: this route answers with JSON or with + // text/event-stream depending on `stream`, and the declared success schema only + // describes the JSON. A generated client built from the unpatched spec looks for + // choices[].message.content on a streaming response and finds nothing on every + // frame, because a chunk carries choices[].delta instead. + // + // Both content types are declared on the one 200 so the spec says what actually + // happens, rather than replacing the JSON shape and lying in the other direction. + const existing = operation.responses?.["200"] as + | { description?: string; content?: Record } + | undefined + operation.responses!["200"] = { + description: + "Chat completion. `application/json` when `stream` is false or absent; " + + "`text/event-stream` of `ChatCompletionChunk` frames terminated by `data: [DONE]` when true.", + content: { + ...(existing?.content ?? {}), + "text/event-stream": { schema: { $ref: "#/components/schemas/ChatCompletionChunk" } }, + }, + } + } const route = `${method.toUpperCase()} ${path}` for (const param of operation.parameters ?? []) normalizeParameter(param, route) } diff --git a/packages/redrob/test/server/httpapi-public-openapi.test.ts b/packages/redrob/test/server/httpapi-public-openapi.test.ts index 310ebae595..9b43c09aec 100644 --- a/packages/redrob/test/server/httpapi-public-openapi.test.ts +++ b/packages/redrob/test/server/httpapi-public-openapi.test.ts @@ -10,6 +10,8 @@ type OpenApiSchema = { readonly enum?: readonly unknown[] readonly properties?: Record readonly required?: readonly string[] + /** Element schema of an array property, e.g. a chunk's `choices`. */ + readonly items?: OpenApiSchema readonly contentSchema?: OpenApiSchema readonly contentMediaType?: string } @@ -70,6 +72,49 @@ function isBuiltInEndpointError(name: string) { } describe("PublicApi OpenAPI v2 errors", () => { + test("documents both content types on the chat-completions 200", () => { + // The route answers with JSON or with text/event-stream depending on `stream`, and + // HttpApi can only declare one success schema. Without the patch the spec describes + // only the JSON, so a generated client looks for choices[].message.content on a + // streaming response and finds nothing on every frame -- a chunk carries + // choices[].delta instead. + const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec + const response = spec.paths["/v1/chat/completions"]?.post?.responses?.["200"] + + expect(Object.keys(response?.content ?? {}).sort()).toEqual(["application/json", "text/event-stream"]) + // Both, not one replacing the other: dropping the JSON would lie in the other direction. + expect(response?.content?.["application/json"]).toBeDefined() + expect(response?.description).toContain("[DONE]") + }) + + test("the chat-completions SSE ref resolves to a registered component", () => { + // A $ref to a schema no endpoint references dangles unless it is registered through + // HttpApi.AdditionalSchemas. A dangling ref generates a client with a missing type + // rather than failing loudly, so this is the assertion that keeps the patch honest. + const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec + const ref = spec.paths["/v1/chat/completions"]?.post?.responses?.["200"]?.content?.[ + "text/event-stream" + ]?.schema?.$ref + + expect(ref).toBe("#/components/schemas/ChatCompletionChunk") + expect(Object.keys(spec.components.schemas)).toContain("ChatCompletionChunk") + }) + + test("a chunk is shaped as a delta, not as a complete message", () => { + const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec + const chunk = spec.components.schemas.ChatCompletionChunk + const choice = chunk?.properties?.choices?.items as OpenApiSchema | undefined + + expect(choice?.properties?.delta).toBeDefined() + // The discriminator differs from the non-streaming object, which is the whole reason a + // separate schema was needed. + expect(chunk?.properties?.object?.enum).toEqual(["chat.completion.chunk"]) + // Every delta field is optional: the first frame carries only role, middle frames only + // content, and the terminator an empty delta. Requiring content would reject the + // terminator. + expect(choice?.properties?.delta?.required ?? []).toEqual([]) + }) + test("includes plugin-facing core schemas", () => { const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec From 3efcd43971b5a3cb5f92917e5f4358545b22bfae Mon Sep 17 00:00:00 2001 From: Janghoon Lee <44862514+savagemanage@users.noreply.github.com> Date: Wed, 23 Sep 2026 06:00:33 +0000 Subject: [PATCH 2/2] chore(sdk): regenerate the stale generated artifacts No source change. `bun script/generate.ts` output only. Two things land together, and the split matters for review: The chat-completions SSE documentation from the previous commit - the 200 now declares both application/json and text/event-stream, and ChatCompletionChunk appears as a component. Pre-existing drift that was already in the repo before this branch. packages/sdk/openapi.json was stale on clean develop by roughly 900 lines, including routes that are not mine (/api/variant/paraphrase among them). Those were regenerated here because there is no way to regenerate only part of the file, and leaving them would mean the committed spec still does not describe the served API. This is why it is a separate commit: the source change is reviewable on its own, and this one is verifiable by re-running the generator. There is no CI check for this drift, which is why it accumulated. The client generator IS checked (packages/client check:generated fails on any diff) and passes; the SDK spec is not. Worth adding a check, but that is a change to CI rather than to artifacts and does not belong here. Also deliberately NOT committed: the ~54 files `generate.ts` reformats as a side effect, mostly markdown table alignment and line wrapping in untouched sources. Those are the generator running a formatter over the repo, not generated output, and sweeping them in would bury the three files that actually matter. --- packages/sdk/js/src/v2/gen/sdk.gen.ts | 41 + packages/sdk/js/src/v2/gen/types.gen.ts | 143 ++++ packages/sdk/openapi.json | 1022 +++++++++++++++++++++-- 3 files changed, 1145 insertions(+), 61 deletions(-) diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 0b90a58f55..ce3837420c 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -15,6 +15,7 @@ import type { AuthRemoveResponses, AuthSetErrors, AuthSetResponses, + ChatCompletionRequest, CommandListErrors, CommandListResponses, Config as Config3, @@ -263,6 +264,8 @@ import type { TuiShowToastResponses, TuiSubmitPromptErrors, TuiSubmitPromptResponses, + V1ChatCompletionsErrors, + V1ChatCompletionsResponses, V2AgentListErrors, V2AgentListResponses, V2CommandListErrors, @@ -7137,6 +7140,39 @@ export class V2 extends HeyApiClient { } } +export class Chat extends HeyApiClient { + /** + * Create a chat completion + * + * OpenAI-compatible inference against the engine's own credential and provider registry. Declaring `tools` makes the caller responsible for executing them: the turn ends with finish_reason tool_calls and the results come back as role:tool messages. `stream: true` returns text/event-stream instead of this JSON body. Failures use OpenAI's error envelope -- {error:{message,type,code,param}} -- with 400 invalid_request_error, 401 authentication_error (code engine_not_authenticated, the only status a client should offer a sign-in action for), 429 rate_limit_error or insufficient_quota, and 502 api_error. They are written by the handler rather than declared as endpoint errors, because this is a raw handler and the envelope carries no discriminator field for codegen to branch on. + */ + public completions( + parameters?: { + chatCompletionRequest?: ChatCompletionRequest + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ key: "chatCompletionRequest", map: "body" }] }]) + return (options?.client ?? this.client).post({ + url: "/v1/chat/completions", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + +export class V1 extends HeyApiClient { + private _chat?: Chat + get chat(): Chat { + return (this._chat ??= new Chat({ client: this.client })) + } +} + export class RedrobClient extends HeyApiClient { public static readonly __registry = new HeyApiRegistry() @@ -7279,4 +7315,9 @@ export class RedrobClient extends HeyApiClient { get v2(): V2 { return (this._v2 ??= new V2({ client: this.client })) } + + private _v1?: V1 + get v1(): V1 { + return (this._v1 ??= new V1({ client: this.client })) + } } diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index ac9249b06d..11e8c0238a 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -106,6 +106,35 @@ export type QuestionRejected = { requestID: string } +export type ChatCompletionChunk = { + id: string + object: "chat.completion.chunk" + created: number + model: string + choices: Array<{ + index: number + delta: { + role?: "assistant" + content?: string + tool_calls?: Array<{ + id: string + type: "function" + function: { + name: string + arguments: string + } + }> + reasoning_content?: string + } + finish_reason: string + }> + usage?: { + prompt_tokens: number + completion_tokens: number + total_tokens: number + } +} + export type OAuth = { type: "oauth" refresh: string @@ -2142,8 +2171,15 @@ export type Provider = { } } +export type ChatCompletionsCapability = { + version: number + callerTools: boolean + stream: boolean +} + export type ExperimentalCapabilities = { backgroundSubagents: boolean + chatCompletions: ChatCompletionsCapability } export type ConsoleState = { @@ -2973,6 +3009,88 @@ export type ProjectCopyError = { } } +export type ChatCompletionRequest = { + model: string + messages: Array<{ + role: "system" | "developer" | "user" | "assistant" | "tool" + content?: + | string + | Array<{ + [key: string]: unknown + }> + tool_calls?: Array<{ + id: string + type: "function" + function: { + name: string + arguments: string + } + }> + tool_call_id?: string + name?: string + }> + tools?: Array<{ + type: "function" + function: { + name: string + description?: string + parameters?: { + [key: string]: unknown + } + } + }> + tool_choice?: + | "auto" + | "none" + | "required" + | { + type: "function" + function: { + name: string + } + } + stream?: boolean + max_tokens?: number + max_completion_tokens?: number + temperature?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" + top_p?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" + stop?: string | Array + seed?: number + frequency_penalty?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" + presence_penalty?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" + reasoning_effort?: string + user?: string +} + +export type ChatCompletionResponse = { + id: string + object: "chat.completion" + created: number + model: string + choices: Array<{ + index: number + message: { + role: "assistant" + content: string + tool_calls?: Array<{ + id: string + type: "function" + function: { + name: string + arguments: string + } + }> + reasoning_content?: string + } + finish_reason: string + }> + usage?: { + prompt_tokens: number + completion_tokens: number + total_tokens: number + } +} + export type EffectHttpApiErrorForbidden = { _tag: "Forbidden" } @@ -13720,6 +13838,31 @@ export type V2ProjectCopyRefreshResponses = { export type V2ProjectCopyRefreshResponse = V2ProjectCopyRefreshResponses[keyof V2ProjectCopyRefreshResponses] +export type V1ChatCompletionsData = { + body?: ChatCompletionRequest + path?: never + query?: never + url: "/v1/chat/completions" +} + +export type V1ChatCompletionsErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type V1ChatCompletionsError = V1ChatCompletionsErrors[keyof V1ChatCompletionsErrors] + +export type V1ChatCompletionsResponses = { + /** + * Chat completion. `application/json` when `stream` is false or absent; `text/event-stream` of `ChatCompletionChunk` frames terminated by `data: [DONE]` when true. + */ + 200: ChatCompletionResponse +} + +export type V1ChatCompletionsResponse = V1ChatCompletionsResponses[keyof V1ChatCompletionsResponses] + export type PtyConnectData = { body?: never path: { diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 2e5d02c714..cd55bbe820 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -11947,6 +11947,190 @@ ] } }, + "/api/variant/paraphrase": { + "post": { + "tags": ["variants"], + "operationId": "v2.variant.paraphrase", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Variant.Result", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VariantResult" + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestError" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/UnauthorizedError" + }, + { + "$ref": "#/components/schemas/UnauthorizedError" + } + ] + } + } + } + }, + "404": { + "description": "ProviderNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProviderNotFoundError" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableError" + } + } + } + } + }, + "description": "Send the same text to two or more models and get each rewrite back, with the model that actually answered and what that slot cost.", + "summary": "Rewrite one text with several models", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VariantParaphraseRequest" + } + } + }, + "required": true + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createRedrobClient } from \"@redrob-labs/sdk\n\nconst client = createRedrobClient()\nawait client.v2.variant.paraphrase({\n ...\n})" + } + ] + } + }, + "/api/variant/compare": { + "post": { + "tags": ["variants"], + "operationId": "v2.variant.compare", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Variant.Result", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VariantResult" + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestError" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/UnauthorizedError" + }, + { + "$ref": "#/components/schemas/UnauthorizedError" + } + ] + } + } + } + }, + "404": { + "description": "ProviderNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProviderNotFoundError" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableError" + } + } + } + } + }, + "description": "Send the same messages to two or more models and get each answer back, so a caller can offer them as a choice.", + "summary": "Answer one conversation with several models", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VariantCompareRequest" + } + } + }, + "required": true + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createRedrobClient } from \"@redrob-labs/sdk\n\nconst client = createRedrobClient()\nawait client.v2.variant.compare({\n ...\n})" + } + ] + } + }, "/api/integration": { "get": { "tags": ["integrations"], @@ -15169,6 +15353,57 @@ ] } }, + "/v1/chat/completions": { + "post": { + "tags": ["chat completions"], + "operationId": "v1.chat.completions", + "parameters": [], + "responses": { + "200": { + "description": "Chat completion. `application/json` when `stream` is false or absent; `text/event-stream` of `ChatCompletionChunk` frames terminated by `data: [DONE]` when true.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChatCompletionResponse" + } + }, + "text/event-stream": { + "schema": { + "$ref": "#/components/schemas/ChatCompletionChunk" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + } + }, + "description": "OpenAI-compatible inference against the engine's own credential and provider registry. Declaring `tools` makes the caller responsible for executing them: the turn ends with finish_reason tool_calls and the results come back as role:tool messages. `stream: true` returns text/event-stream instead of this JSON body. Failures use OpenAI's error envelope -- {error:{message,type,code,param}} -- with 400 invalid_request_error, 401 authentication_error (code engine_not_authenticated, the only status a client should offer a sign-in action for), 429 rate_limit_error or insufficient_quota, and 502 api_error. They are written by the handler rather than declared as endpoint errors, because this is a raw handler and the envelope carries no discriminator field for codegen to branch on.", + "summary": "Create a chat completion", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChatCompletionRequest" + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createRedrobClient } from \"@redrob-labs/sdk\n\nconst client = createRedrobClient()\nawait client.v1.chat.completions({\n ...\n})" + } + ] + } + }, "/pty/{ptyID}/connect": { "get": { "tags": ["pty"], @@ -15565,6 +15800,104 @@ "required": ["sessionID", "requestID"], "additionalProperties": false }, + "ChatCompletionChunk": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "object": { + "type": "string", + "enum": ["chat.completion.chunk"] + }, + "created": { + "type": "integer" + }, + "model": { + "type": "string" + }, + "choices": { + "type": "array", + "items": { + "type": "object", + "properties": { + "index": { + "type": "integer" + }, + "delta": { + "type": "object", + "properties": { + "role": { + "type": "string", + "enum": ["assistant"] + }, + "content": { + "type": "string" + }, + "tool_calls": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["function"] + }, + "function": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "arguments": { + "type": "string" + } + }, + "required": ["name", "arguments"], + "additionalProperties": false + } + }, + "required": ["id", "type", "function"], + "additionalProperties": false + } + }, + "reasoning_content": { + "type": "string" + } + }, + "additionalProperties": false + }, + "finish_reason": { + "type": "string" + } + }, + "required": ["index", "delta", "finish_reason"], + "additionalProperties": false + } + }, + "usage": { + "type": "object", + "properties": { + "prompt_tokens": { + "type": "integer" + }, + "completion_tokens": { + "type": "integer" + }, + "total_tokens": { + "type": "integer" + } + }, + "required": ["prompt_tokens", "completion_tokens", "total_tokens"], + "additionalProperties": false + } + }, + "required": ["id", "object", "created", "model", "choices"], + "additionalProperties": false + }, "OAuth": { "type": "object", "properties": { @@ -16324,6 +16657,12 @@ "cost": { "type": "number" }, + "routedModel": { + "type": "string" + }, + "upstreamProvider": { + "type": "string" + }, "tokens": { "type": "object", "properties": { @@ -17271,7 +17610,65 @@ "properties": { "type": { "type": "string", - "enum": ["idle"] + "enum": ["idle"] + } + }, + "required": ["type"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["retry"] + }, + "attempt": { + "type": "integer", + "minimum": 0 + }, + "message": { + "type": "string" + }, + "action": { + "type": "object", + "properties": { + "reason": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "title": { + "type": "string" + }, + "message": { + "type": "string" + }, + "label": { + "type": "string" + }, + "link": { + "type": "string" + } + }, + "required": ["reason", "provider", "title", "message", "label"], + "additionalProperties": false + }, + "next": { + "type": "integer", + "minimum": 0 + } + }, + "required": ["type", "attempt", "message", "next"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["busy"] } }, "required": ["type"], @@ -17282,11 +17679,7 @@ "properties": { "type": { "type": "string", - "enum": ["retry"] - }, - "attempt": { - "type": "integer", - "minimum": 0 + "enum": ["blocked"] }, "message": { "type": "string" @@ -17315,24 +17708,9 @@ }, "required": ["reason", "provider", "title", "message", "label"], "additionalProperties": false - }, - "next": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["type", "attempt", "message", "next"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["busy"] } }, - "required": ["type"], + "required": ["type", "message"], "additionalProperties": false } ] @@ -21515,6 +21893,13 @@ "reserved": { "type": "integer", "minimum": 0 + }, + "threshold": { + "type": "integer", + "minimum": 0 + }, + "maxTurnInputCostUsd": { + "type": "number" } }, "additionalProperties": false @@ -21859,14 +22244,34 @@ "required": ["id", "name", "source", "env", "options", "models"], "additionalProperties": false }, + "ChatCompletionsCapability": { + "type": "object", + "properties": { + "version": { + "type": "integer", + "minimum": 0 + }, + "callerTools": { + "type": "boolean" + }, + "stream": { + "type": "boolean" + } + }, + "required": ["version", "callerTools", "stream"], + "additionalProperties": false + }, "ExperimentalCapabilities": { "type": "object", "properties": { "backgroundSubagents": { "type": "boolean" + }, + "chatCompletions": { + "$ref": "#/components/schemas/ChatCompletionsCapability" } }, - "required": ["backgroundSubagents"], + "required": ["backgroundSubagents", "chatCompletions"], "additionalProperties": false }, "ConsoleState": { @@ -24252,66 +24657,420 @@ { "$ref": "#/components/schemas/WorkspaceFailed" }, - { - "$ref": "#/components/schemas/WorkspaceStatus" + { + "$ref": "#/components/schemas/WorkspaceStatus" + }, + { + "$ref": "#/components/schemas/WorktreeReady" + }, + { + "$ref": "#/components/schemas/WorktreeFailed" + }, + { + "$ref": "#/components/schemas/ServerConnected" + }, + { + "$ref": "#/components/schemas/GlobalDisposed" + } + ] + }, + "V2EventStream": { + "type": "string", + "contentSchema": { + "$ref": "#/components/schemas/V2Event" + }, + "contentMediaType": "application/json" + }, + "ForbiddenError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["ForbiddenError"] + }, + "message": { + "type": "string" + } + }, + "required": ["_tag", "message"], + "additionalProperties": false + }, + "ProjectCopyError": { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": ["ProjectCopyError"] + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "forceRequired": { + "type": "boolean" + } + }, + "required": ["message"], + "additionalProperties": false + } + }, + "required": ["name", "data"], + "additionalProperties": false + }, + "ChatCompletionRequest": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "messages": { + "type": "array", + "items": { + "type": "object", + "properties": { + "role": { + "type": "string", + "enum": ["system", "developer", "user", "assistant", "tool"] + }, + "content": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "object" + } + } + ] + }, + "tool_calls": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["function"] + }, + "function": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "arguments": { + "type": "string" + } + }, + "required": ["name", "arguments"], + "additionalProperties": false + } + }, + "required": ["id", "type", "function"], + "additionalProperties": false + } + }, + "tool_call_id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": ["role"], + "additionalProperties": false + } + }, + "tools": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["function"] + }, + "function": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "parameters": { + "type": "object" + } + }, + "required": ["name"], + "additionalProperties": false + } + }, + "required": ["type", "function"], + "additionalProperties": false + } + }, + "tool_choice": { + "anyOf": [ + { + "type": "string", + "enum": ["auto", "none", "required"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["function"] + }, + "function": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": ["name"], + "additionalProperties": false + } + }, + "required": ["type", "function"], + "additionalProperties": false + } + ] + }, + "stream": { + "type": "boolean" + }, + "max_tokens": { + "type": "integer" + }, + "max_completion_tokens": { + "type": "integer" + }, + "temperature": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "top_p": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "stop": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] }, - { - "$ref": "#/components/schemas/WorktreeReady" + "seed": { + "type": "integer" }, - { - "$ref": "#/components/schemas/WorktreeFailed" + "frequency_penalty": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] }, - { - "$ref": "#/components/schemas/ServerConnected" + "presence_penalty": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] }, - { - "$ref": "#/components/schemas/GlobalDisposed" - } - ] - }, - "V2EventStream": { - "type": "string", - "contentSchema": { - "$ref": "#/components/schemas/V2Event" - }, - "contentMediaType": "application/json" - }, - "ForbiddenError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["ForbiddenError"] + "reasoning_effort": { + "type": "string" }, - "message": { + "user": { "type": "string" } }, - "required": ["_tag", "message"], + "required": ["model", "messages"], "additionalProperties": false }, - "ProjectCopyError": { + "ChatCompletionResponse": { "type": "object", "properties": { - "name": { + "id": { + "type": "string" + }, + "object": { "type": "string", - "enum": ["ProjectCopyError"] + "enum": ["chat.completion"] }, - "data": { + "created": { + "type": "integer" + }, + "model": { + "type": "string" + }, + "choices": { + "type": "array", + "items": { + "type": "object", + "properties": { + "index": { + "type": "integer" + }, + "message": { + "type": "object", + "properties": { + "role": { + "type": "string", + "enum": ["assistant"] + }, + "content": { + "type": "string" + }, + "tool_calls": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["function"] + }, + "function": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "arguments": { + "type": "string" + } + }, + "required": ["name", "arguments"], + "additionalProperties": false + } + }, + "required": ["id", "type", "function"], + "additionalProperties": false + } + }, + "reasoning_content": { + "type": "string" + } + }, + "required": ["role", "content"], + "additionalProperties": false + }, + "finish_reason": { + "type": "string" + } + }, + "required": ["index", "message", "finish_reason"], + "additionalProperties": false + } + }, + "usage": { "type": "object", "properties": { - "message": { - "type": "string" + "prompt_tokens": { + "type": "integer" }, - "forceRequired": { - "type": "boolean" + "completion_tokens": { + "type": "integer" + }, + "total_tokens": { + "type": "integer" } }, - "required": ["message"], + "required": ["prompt_tokens", "completion_tokens", "total_tokens"], "additionalProperties": false } }, - "required": ["name", "data"], + "required": ["id", "object", "created", "model", "choices"], "additionalProperties": false }, "effect_HttpApiError_Forbidden": { @@ -27873,6 +28632,12 @@ "cost": { "type": "number" }, + "routedModel": { + "type": "string" + }, + "upstreamProvider": { + "type": "string" + }, "tokens": { "type": "object", "properties": { @@ -30039,6 +30804,133 @@ "required": ["id", "name", "api", "request"], "additionalProperties": false }, + "VariantModel": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "required": ["model"], + "additionalProperties": false + }, + "VariantParaphraseRequest": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "models": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VariantModel" + } + }, + "requestId": { + "type": "string" + } + }, + "required": ["text", "models"], + "additionalProperties": false + }, + "VariantProvenance": { + "type": "object", + "properties": { + "requestId": { + "type": "string" + }, + "routedModel": { + "type": "string" + }, + "upstreamProvider": { + "type": "string" + }, + "latencyMs": { + "type": "number" + }, + "costUsd": { + "type": "number" + } + }, + "additionalProperties": false + }, + "VariantSlot": { + "type": "object", + "properties": { + "slot": { + "type": "integer" + }, + "model": { + "type": "string" + }, + "text": { + "type": "string" + }, + "error": { + "type": "string" + }, + "redrob": { + "$ref": "#/components/schemas/VariantProvenance" + } + }, + "required": ["slot", "model"], + "additionalProperties": false + }, + "VariantResult": { + "type": "object", + "properties": { + "variants": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VariantSlot" + } + }, + "totalCostUsd": { + "type": "number" + } + }, + "required": ["variants", "totalCostUsd"], + "additionalProperties": false + }, + "VariantMessage": { + "type": "object", + "properties": { + "role": { + "type": "string", + "enum": ["system", "user", "assistant"] + }, + "content": { + "type": "string" + } + }, + "required": ["role", "content"], + "additionalProperties": false + }, + "VariantCompareRequest": { + "type": "object", + "properties": { + "messages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VariantMessage" + } + }, + "models": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VariantModel" + } + }, + "requestId": { + "type": "string" + } + }, + "required": ["messages", "models"], + "additionalProperties": false + }, "IntegrationWhen": { "type": "object", "properties": { @@ -36898,6 +37790,10 @@ "name": "providers", "description": "Experimental provider routes." }, + { + "name": "variants", + "description": "Experimental multi-model routes. Available while the Redrob provider is connected." + }, { "name": "integrations", "description": "Integration discovery and authentication routes." @@ -36942,6 +37838,10 @@ "name": "projectCopy", "description": "Project copy management routes." }, + { + "name": "chat completions", + "description": "OpenAI-compatible inference route. The shared engine surface for Redrob products." + }, { "name": "pty", "description": "PTY websocket route."