/** * Provider-shared types for the pi harness. * * One unified message + event shape regardless of which underlying LLM API * (Google Gemini, OpenAI-compatible /v1/chat/completions, Anthropic Messages) * is handling the request. Each provider implements `streamProvider(req): AsyncIterable` * and the session loop consumes the events without knowing the flavor. * * Modelled after pi-ai's event vocabulary (text_start/delta/end, toolcall_*, * done, error) so we can copy fixes from upstream if needed, but only the * fields bloby actually consumes are kept. */ export type PiRole = 'user' | 'assistant' | 'tool'; /** A single content block inside a message. */ export type PiContentBlock = | { type: 'text'; text: string } | { type: 'image'; mediaType: string; data: string } // base64 // Native document block (PDF). Only the flavors with native document support // (anthropic-messages, google-gemini) ever receive one — buildUserMessage // gates it on canNativeDocument; openai-completions degrades it to a text // note rather than crashing if one ever reaches it. | { type: 'document'; mediaType: string; data: string; name?: string } // base64 // `thoughtSignature` is a Gemini 3.x thinking-model field. Pi-flavored // providers that emit reasoning attach it to function-call parts; the API // rejects the next turn with HTTP 400 if we don't echo it back verbatim. | { type: 'tool_use'; id: string; name: string; input: any; thoughtSignature?: string } | { type: 'tool_result'; toolUseId: string; content: string; isError?: boolean }; export interface PiMessage { role: PiRole; content: PiContentBlock[]; } /** Schema for one tool the model can call. Plain JSON Schema for input. */ export interface PiToolDef { name: string; description: string; inputSchema: Record; } export interface PiStreamRequest { modelId: string; baseUrl: string; apiKey: string; systemPrompt: string; messages: PiMessage[]; tools?: PiToolDef[]; /** Hard cap on output tokens for a single turn. */ maxOutputTokens?: number; /** * Which request field carries the output cap on the openai-completions * flavor. OpenAI's reasoning models (gpt-5.x, o-series) reject the legacy * `max_tokens` — the openai-api sub-provider sets `max_completion_tokens` * (accepted by ALL OpenAI models); other vendors stay on `max_tokens`. */ maxTokensField?: 'max_tokens' | 'max_completion_tokens'; /** * openai-completions flavor: set false for strict-schema vendors (Mistral) * that 422 on the `stream_options.include_usage` opt-in. Default true. */ includeStreamUsage?: boolean; /** * 'none' forbids tool calls for this request (mapped per flavor: OpenAI * tool_choice:'none', Anthropic {type:'none'}, Gemini functionCallingConfig * mode NONE). Used by the session's round-cap wrap-up round, where the model * must summarize instead of starting more work. */ toolChoice?: 'auto' | 'none'; /** Optional abort signal so the session can interrupt in-flight requests. */ signal?: AbortSignal; } export type PiStopReason = 'end_turn' | 'tool_use' | 'max_tokens' | 'error' | 'aborted'; /** * Coarse error classification so the session/harness can react without * string-matching: retry transient rounds, tear down on auth/overflow, and * show actionable messages instead of raw provider JSON. */ export type PiErrorKind = | 'auth' | 'context-overflow' | 'rate-limit' | 'billing' | 'transient' /** The model rejected an image/vision/modality block (a text-only model 400/ * 415/422s on the attached image). The session reacts by disabling vision * for the rest of the session and re-running the round with images * downgraded to placeholders — self-healing for dynamic/unknown models whose * catalog can't tell us up front whether they see images. */ | 'image-unsupported' | 'other'; export type PiStreamEvent = | { type: 'text_delta'; delta: string } | { type: 'text_end'; text: string } /** Emitted when the model starts (visibly) reasoning — a liveness pulse for * thinking models so the chat doesn't look hung. Reasoning TEXT is never * forwarded (it would corrupt the streamed-text == response contract). */ | { type: 'thinking' } | { type: 'tool_use'; id: string; name: string; input: any; thoughtSignature?: string } | { type: 'done'; stopReason: PiStopReason; usage?: PiUsage } | { type: 'error'; error: string; status?: number; kind?: PiErrorKind; retryable?: boolean }; export interface PiUsage { /** Non-cached prompt tokens. NOTE: Anthropic's input_tokens EXCLUDES cache * reads/writes — prompt occupancy is input + cacheRead + cacheCreation * (Gemini's promptTokenCount and OpenAI's prompt_tokens already include * cached tokens, so their providers leave the cache fields unset). */ inputTokens?: number; outputTokens?: number; cacheReadTokens?: number; cacheCreationTokens?: number; }