import { type AssistantMessage, type JsonSchema } from "./anthropic.js"; import { type ValidationError } from "./validator.js"; import { type CredentialResolution } from "./authEnv.js"; import { type UsageAccumulator } from "./usage-observer.js"; import { type CredentialWalkOptions, type CredentialWalkOutcome } from "./credential-select.js"; import type { ResolvedAttempt } from "./resolved-attempt.js"; export interface ReshapeRequest { /** The declared tools (name → schema|null) so the reshaper knows the contract. */ tools: Map; /** The backend's failing assistant message. */ rawAssistant: AssistantMessage; /** Why it failed validation. */ errors: ValidationError[]; backendModel: string | null; /** Caller response lifetime; deliberately distinct from this reshaper's timeout. */ signal?: AbortSignal; } export type ReshapeResult = { kind: "message"; message: AssistantMessage; } | { kind: "refuse"; reason: string; }; export interface Reshaper { reshape(req: ReshapeRequest, hooks?: ReshaperAccountingHooks): Promise; } export interface ReshaperAccountingCompletion { readonly outcome: "success" | "error" | "cancelled"; readonly failureKind: "timeout" | "provider_error" | "auth_error" | "rate_limit" | "aborted" | "protocol" | "unknown" | null; readonly usage: UsageAccumulator; readonly endedAt: number; } export interface ReshaperAccountingAttempt { /** * MUST be called exactly once, on EVERY exit path (success, refusal, transport * error, cancellation). A dropped handle leaves the server-side request * finalizer waiting on a still-active attempt, so the request never records * its `request-completed` event and stalls until store eviction. */ complete(completion: ReshaperAccountingCompletion): void; } /** * Server-owned request accounting can observe real repair egress without * making the reusable reshaper depend on server lifecycle state. */ export interface ReshaperAccountingHooks { /** * The returned handle is load-bearing: it must be completed exactly once on * every exit path, or request finalization stalls until LRU eviction. Return * null only when no attempt was actually started. */ startRepairAttempt(options: { readonly resolvedAttempt: ResolvedAttempt | null; readonly credentialState: CredentialResolution["state"]; readonly provider: string | null; readonly model: string | null; readonly credentialId: string | null; readonly startedAt: number; }): ReshaperAccountingAttempt | null; /** A closed caller response must never start a late repair egress. */ isRequestClosed?(): boolean; } /** * Build the reshaper prompt body. * * A reshape is a cross-provider egress: the failing call's ARGUMENTS and the tool * SCHEMAS leave for whatever provider `config.reshaper` names, which is often not * the provider that served the response. That is inherent — a model cannot correct * arguments it is not shown — so the mitigation is minimisation, not avoidance: * only the schemas of tools actually NAMED by a failing call are sent, instead of * the request's entire declared tool set (a Claude Code session declares dozens, * none of which the reshaper needs to fix one call). Destructive calls never get * here at all: `repair()` refuses them before the reshaper is asked. */ export declare function buildUserContent(req: ReshapeRequest): string; /** * A transport-level reshaper failure: network error, timeout, non-2xx HTTP, unparseable * HTTP body. The model never rendered a judgement, so failover MAY try another candidate. * Distinct from a `refuse` result, which is a judgement and must never be shopped around. */ export declare class ReshaperTransportError extends Error { readonly outcome: CredentialWalkOutcome; constructor(message: string, outcome?: CredentialWalkOutcome); } export type CorrectedInputs = { kind: "inputs"; inputs: Record; } | { kind: "refuse"; reason: string; }; /** Parse the reshaper model's text into a per-id corrected-inputs map. */ export declare function parseCorrectedInputs(text: string): CorrectedInputs; /** * Rebuild the assistant message, replacing each failing tool_use's input by id. * * Only `input` is ever taken from the reshaper — ids, names, block order and every * non-tool block come from `raw`, so a reshaper cannot add, drop or re-point a call. * `stop_reason` is normalised to "tool_use" whenever the rebuilt content bears a * tool_use block: that is protocol form the content fully determines (the harness * will not execute a tool announced under "end_turn"), and preserving the backend's * wrong value here made an otherwise-repaired message fail re-validation and burn * every remaining attempt. * * ⚠ Everything else is CARRIED OVER from `raw` — `id`, `model`, `stop_sequence`, `usage`. * This function returned only `{ content, stop_reason }`, so a message that went through * repair reached `emitSse` stripped of the backend's own identity and got a synthesized id * and no usage, while a message that merely passed validation kept both. Repair must not be * observable in the response envelope; the only field it is allowed to change is the one it * repaired. Absent fields stay absent — nothing here invents an id or zero-fills usage. */ export declare function reconstruct(raw: AssistantMessage, inputs: Record): AssistantMessage; /** Reshaper backed by an Anthropic- or OpenAI-compatible endpoint. */ /** * Tries several reshapers in ranked order so repair does not depend on one model staying servable. * * Only *transport* failures advance to the next candidate. A reshaper that answers with `refuse` * is a real judgement — the model looked at the call and declined to guess — so it is returned * as-is. Retrying a refusal on another model would be shopping for a more compliant answer, which * is exactly how a fabricated tool call gets through. * * Exhausting every candidate THROWS `ReshaperTransportError` for the same reason: nobody answered, * so there is no judgement to report. Returning `refuse` there re-crossed the one line this class * exists to hold — it labelled a total outage as a model's decision, and `repair()` logged the * turn as `refused` (a model declined) rather than `failed` (nothing was reachable). */ export declare class FailoverReshaper implements Reshaper { private readonly delegates; constructor(delegates: Reshaper[]); reshape(req: ReshapeRequest, hooks?: ReshaperAccountingHooks): Promise; } interface HttpReshaperConfig { base: string; model: string; kind: "anthropic" | "openai"; /** Retained as non-secret topology metadata; HttpReshaper never resolves either field. */ provider?: string; authEnv?: string; authHeader: "x-api-key" | "authorization"; timeoutMs: number; } export declare class HttpReshaper implements Reshaper { private readonly cfg; private readonly credential; private readonly fetchFn; private readonly beforeFetch?; private readonly resolvedAttempt; constructor(cfg: HttpReshaperConfig, credential: CredentialResolution, fetchFn?: typeof fetch, beforeFetch?: (() => void) | undefined, resolvedAttempt?: ResolvedAttempt | null); reshape(req: ReshapeRequest, hooks?: ReshaperAccountingHooks): Promise; } /** Request-local, fleet-aware reshaper selection using the shared credential walk policy. */ export declare class CredentialWalkReshaper implements Reshaper { private readonly fetchFn; private readonly walk; private readonly lru; /** * A recognized reshaper response proves this credential can egress, but only `repair()` can * decide whether the corrected message satisfies the caller's schema. Keep that attempt pending * so a semantic retry reuses it; only a later transport outcome may advance the same walk. */ private pinnedAttempt; constructor(attempts: readonly ResolvedAttempt[], walkOptions?: CredentialWalkOptions, fetchFn?: typeof fetch); reshape(req: ReshapeRequest, hooks?: ReshaperAccountingHooks): Promise; } export {};