Clicking "fetch available models" on a built-in provider went to the network. That is the wrong source: pi-ai's registry is the authoritative list for its own providers, and it carries the context windows and output caps a `GET /models` listing does not disclose. Asking api.deepseek.com what DeepSeek serves is both slower and worse, and against an endpoint that answers a different shape it failed outright. Interrogation is still keyed by settings namespace — the provider being added has no route — but the request may now name the route it is editing. An adapter that already describes that route answers from what it knows, needs no endpoint at all, and never touches the network; only a route the catalog does not describe reaches the wire, and one naming no endpoint is told to set one or enter its models by hand. `ConfigurableProviderView` gained `supportsDiscovery` so a surface offers the action where a namespace can answer instead of hardcoding an adapter family. Three narrower corrections ride along. Discovery no longer claims Azure or Codex: Azure authenticates with an `api-key` header and an `api-version` query despite its OpenAI lineage, and Codex uses OAuth, so both reported an authentication failure as a provider with no models. Cancellation during the body read escaped as the raw abort reason rather than a coded ABORTED. And the schema comment claiming the probe key is never logged overstated it: the host neither stores nor returns it, but it rides the client's outgoing envelope like every other secret-bearing payload, and redacting that tap is a configuration-plane-wide change.
131 lines
8.2 KiB
TypeScript
131 lines
8.2 KiB
TypeScript
/**
|
|
* Message-layer zod schemas: the four wire full forms + error body +
|
|
* carrier receipt. The payload slot is unknown in the full-form schemas — business payloads
|
|
* get a second parse dispatched by method (two-level parse discipline).
|
|
* Brand cast point: rpcIdSchema, and only there.
|
|
*/
|
|
|
|
import { z } from 'zod'
|
|
import type { z as zCore } from 'zod'
|
|
type ZodIssue = zCore.core.$ZodIssue
|
|
import type { ClientRequest, ClientResponse, RpcError, RpcId, RpcReceipt, ServerRequest, ServerResponse } from './rpc.ts'
|
|
|
|
/**
|
|
* Wire widening of a contract type: widens every property (deeply) to `original | undefined`.
|
|
* The repo enables exactOptionalPropertyTypes while zod `.optional()` outputs `T | undefined`,
|
|
* so `satisfies z.ZodType<ContractType>` is unusable across the board; anchoring is always
|
|
* written `satisfies z.ZodType<Wire<ContractType>>` — the widening only adds undefined, so
|
|
* missing fields / wrong types still fail to compile. On the JSON wire, "absent" and
|
|
* "value undefined" serialize identically, so the widening loses no validation semantics.
|
|
*/
|
|
export type Wire<T> = T extends readonly (infer E)[] ? Wire<E>[]
|
|
: T extends object ? { [K in keyof T]: Wire<T[K]> | undefined }
|
|
: T
|
|
|
|
/**
|
|
* RpcId: one brand cast after shape validation (the only cast point in this
|
|
* file). No min-length: the id is an opaque echo token, and rejecting shapes
|
|
* here would only turn a correlatable error report into a client-side parse
|
|
* failure (the handler substitutes a sentinel when a request's id is unreadable).
|
|
*/
|
|
export const rpcIdSchema = z.string() as unknown as z.ZodType<RpcId>
|
|
|
|
/** Error body: discriminated by code, per-branch details aligned to RpcErrorDetailsMap; details is required. */
|
|
export const rpcErrorSchema: z.ZodType<RpcError> = z.discriminatedUnion('code', [
|
|
z.object({ code: z.literal('bad-request'), message: z.string(), details: z.object({ issues: z.array(z.custom<ZodIssue>()) }) }),
|
|
z.object({ code: z.literal('cancelled'), message: z.string(), details: z.object({}) }),
|
|
z.object({ code: z.literal('session-not-found'), message: z.string(), details: z.object({ sessionId: z.string() }) }),
|
|
z.object({ code: z.literal('model-unavailable'), message: z.string(), details: z.object({ provider: z.string(), model: z.string() }) }),
|
|
z.object({ code: z.literal('session-conflict'), message: z.string(), details: z.object({ sessionId: z.string(), requestedCwd: z.string(), existingCwd: z.string().optional() }) }),
|
|
z.object({ code: z.literal('workspace-attach-failed'), message: z.string(), details: z.object({ sessionId: z.string(), workspaceId: z.string() }) }),
|
|
z.object({ code: z.literal('workspace-not-found'), message: z.string(), details: z.object({ workspaceId: z.string() }) }),
|
|
z.object({ code: z.literal('workspace-invalid-path'), message: z.string(), details: z.object({ path: z.string() }) }),
|
|
z.object({ code: z.literal('workspace-name-conflict'), message: z.string(), details: z.object({ name: z.string() }) }),
|
|
z.object({ code: z.literal('workspace-move-invalid'), message: z.string(), details: z.object({ workspaceId: z.string(), sessionId: z.string(), beforeSessionId: z.string().optional() }) }),
|
|
z.object({ code: z.literal('directory-unreadable'), message: z.string(), details: z.object({ path: z.string() }) }),
|
|
z.object({ code: z.literal('directory-exists'), message: z.string(), details: z.object({ path: z.string() }) }),
|
|
z.object({ code: z.literal('directory-create-failed'), message: z.string(), details: z.object({ path: z.string() }) }),
|
|
z.object({ code: z.literal('directory-picker-unavailable'), message: z.string(), details: z.object({ capability: z.string() }) }),
|
|
z.object({ code: z.literal('agent-busy'), message: z.string(), details: z.object({ reason: z.string() }) }),
|
|
z.object({ code: z.literal('queue-item-not-found'), message: z.string(), details: z.object({ itemId: z.string() }) }),
|
|
z.object({ code: z.literal('steer-unavailable'), message: z.string(), details: z.object({ itemId: z.string() }) }),
|
|
z.object({ code: z.literal('command-error'), message: z.string(), details: z.object({}) }),
|
|
z.object({ code: z.literal('unknown-command'), message: z.string(), details: z.object({}) }),
|
|
z.object({ code: z.literal('settings-rejected'), message: z.string(), details: z.object({ ns: z.string() }) }),
|
|
z.object({ code: z.literal('settings-not-exposed'), message: z.string(), details: z.object({ ns: z.string() }) }),
|
|
z.object({ code: z.literal('settings-conflict'), message: z.string(), details: z.object({ ns: z.string(), expected: z.number(), actual: z.number() }) }),
|
|
z.object({ code: z.literal('credential-rejected'), message: z.string(), details: z.object({ ref: z.string() }) }),
|
|
z.object({ code: z.literal('model-discovery-failed'), message: z.string(), details: z.object({ settingsNs: z.string(), baseURL: z.string().optional() }) }),
|
|
z.object({ code: z.literal('title-invalid'), message: z.string(), details: z.object({ sessionId: z.string() }) }),
|
|
z.object({ code: z.literal('fork-unavailable'), message: z.string(), details: z.object({ sessionId: z.string() }) }),
|
|
z.object({ code: z.literal('subagent-parent-unavailable'), message: z.string(), details: z.object({ parentSessionId: z.string() }) }),
|
|
z.object({ code: z.literal('subagent-not-found'), message: z.string(), details: z.object({ parentSessionId: z.string(), childSessionId: z.string() }) }),
|
|
z.object({ code: z.literal('subagent-catalog-diagnostic'), message: z.string(), details: z.object({
|
|
parentSessionId: z.string(),
|
|
childSessionId: z.string(),
|
|
reason: z.union([z.literal('corrupt'), z.literal('unsupported'), z.literal('unavailable')]),
|
|
}) }),
|
|
z.object({ code: z.literal('subagent-not-resumable'), message: z.string(), details: z.object({ childSessionId: z.string() }) }),
|
|
z.object({ code: z.literal('subagent-unauthorized'), message: z.string(), details: z.object({ childSessionId: z.string() }) }),
|
|
z.object({ code: z.literal('subagent-delivery-unavailable'), message: z.string(), details: z.object({ childSessionId: z.string() }) }),
|
|
z.object({ code: z.literal('internal'), message: z.string(), details: z.object({}) }),
|
|
]) as unknown as z.ZodType<RpcError>
|
|
|
|
/**
|
|
* Business success/failure result schema (generic, reusable).
|
|
* @param value - Schema for the business value.
|
|
* @returns Schema for RpcResult<T>.
|
|
*/
|
|
export function rpcResultSchema<T>(value: z.ZodType<T>): z.ZodUnion<readonly [z.ZodType, z.ZodType]> {
|
|
return z.union([
|
|
z.object({ ok: z.literal(true), value }),
|
|
z.object({ ok: z.literal(false), error: rpcErrorSchema }),
|
|
])
|
|
}
|
|
|
|
// ---- The four wire full-form schemas (payload/result.value slots stay wide — business layer does the second parse) ----
|
|
|
|
/** ClientRequest full form (payload stays wide — the business layer runs the second parse). */
|
|
export const clientRequestSchema = z.object({
|
|
type: z.literal('client-request'),
|
|
rpcId: rpcIdSchema,
|
|
method: z.string(),
|
|
payload: z.unknown(),
|
|
}) as unknown as z.ZodType<ClientRequest>
|
|
|
|
/** ServerResponse full form (result.value stays wide). */
|
|
export const serverResponseSchema = z.object({
|
|
type: z.literal('server-response'),
|
|
rpcId: rpcIdSchema,
|
|
result: rpcResultSchema(z.unknown()),
|
|
}) as unknown as z.ZodType<ServerResponse>
|
|
|
|
/** ServerRequest full form (payload stays wide). */
|
|
export const serverRequestSchema = z.object({
|
|
type: z.literal('server-request'),
|
|
rpcId: rpcIdSchema,
|
|
method: z.string(),
|
|
payload: z.unknown(),
|
|
}) as unknown as z.ZodType<ServerRequest>
|
|
|
|
/** ClientResponse full form (result.value stays wide). */
|
|
export const clientResponseSchema = z.object({
|
|
type: z.literal('client-response'),
|
|
rpcId: rpcIdSchema,
|
|
result: rpcResultSchema(z.unknown()),
|
|
}) as unknown as z.ZodType<ClientResponse>
|
|
|
|
/** Wire full-form union (discriminated by type). */
|
|
export const rpcMessageSchema = z.discriminatedUnion('type', [
|
|
clientRequestSchema as unknown as z.ZodObject<z.ZodRawShape>,
|
|
serverResponseSchema as unknown as z.ZodObject<z.ZodRawShape>,
|
|
serverRequestSchema as unknown as z.ZodObject<z.ZodRawShape>,
|
|
clientResponseSchema as unknown as z.ZodObject<z.ZodRawShape>,
|
|
])
|
|
|
|
/** Carrier receipt schema. */
|
|
export const rpcReceiptSchema = z.union([
|
|
z.object({ accepted: z.literal(true) }),
|
|
z.object({ accepted: z.literal(false), reason: z.union([z.literal('not-pending'), z.literal('bad-response')]) }),
|
|
]) satisfies z.ZodType<Wire<RpcReceipt>>
|