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 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."