UNPKG

@juspay/neurolink

Version:

The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002

284 lines (283 loc) • 9.62 kB
/** * Wire-neutral intermediate representation for proxy protocol translation. * * Translation between provider wire formats was previously pairwise and ad hoc: * `codexFallback.ts` converts an Anthropic request into a Codex one and casts a * role to reach Claude Code's inline-system convention inside a converter that is * meant to be neutral. Adding the reverse direction pairwise would double that, * and a third provider would square it. * * So each format gets a codec to and from this IR, and adding a provider is a new * codec rather than new branching. The IR is deliberately not `ClaudeRequest`: * that type is simultaneously the literal Anthropic wire shape, so using it as * the hub makes every other provider's quirks leak into the Anthropic hot path. * `ClaudeRequest` and `ClaudeResponse` stay untouched. * * Every union here is discriminated on a literal `kind`, `status` or `source`, so * `assertProxyIRExhaustive` turns an unhandled variant into a compile error. That * is the point of the IR: a field that cannot be represented must be stated, via * `ProxyIRUnmappedPart`, rather than dropped silently. */ import type { CodexReasoningEffort } from "./codex.js"; import type { UsageContext } from "./proxy.js"; /** * A wire dialect a codec can speak. * * Distinct from `ProxyFormat` ("claude" | "openai" | "gemini"), which selects a * request-translation surface for a different subsystem. These must not be * conflated: this one names the two dialects the fallback codecs translate. */ export type ProxyIRWireFormat = "anthropic-messages" | "codex-responses"; /** * Message roles across both dialects. * * `developer` has no Anthropic equivalent and is native Codex's own role for * instruction blocks that sit inside the ordered input array rather than in a * leading system field. It is kept distinct from `system` so a codec targeting * Anthropic can decide where it belongs instead of inheriting a lossy merge. */ export type ProxyIRRole = "system" | "developer" | "user" | "assistant"; /** * A request to cache the prefix up to and including the annotated element. * * Only meaningful for Anthropic, whose caching is explicit and breakpoint-driven. * A codec targeting a dialect with automatic caching ignores it, which is correct * rather than lossy: nothing on that wire can carry it. */ export type ProxyIRCacheHint = { ttl?: "5m" | "1h"; }; export type ProxyIRTextPart = { kind: "text"; text: string; cacheHint?: ProxyIRCacheHint; }; /** `signature` is reserved for Anthropic's signed thinking blocks and unused today. */ export type ProxyIRThinkingPart = { kind: "thinking"; text: string; signature?: string; }; export type ProxyIRImagePart = { kind: "image"; encoding: "base64" | "url"; mediaType?: string; data: string; }; /** * A model-emitted call to a tool. * * `argumentsRaw` is authoritative and always present; `argumentsJson` is a * convenience for the common case where the raw text parses. Keeping the raw text * means a call whose arguments are streamed in fragments, or are not valid JSON, * survives translation instead of being normalised into something the next hop * never sent. */ export type ProxyIRToolCallPart = { kind: "tool_call"; callId: string; toolName: string; argumentsRaw: string; argumentsJson?: Record<string, unknown>; }; export type ProxyIRToolResultPart = { kind: "tool_result"; callId: string; content: (ProxyIRTextPart | ProxyIRImagePart)[]; isError?: boolean; cacheHint?: ProxyIRCacheHint; }; /** * A wire element no codec could represent, carried forward verbatim. * * The alternative is dropping it, which is how a translation layer loses tool * calls: silently, and only visibly as a downstream rejection. `raw` keeps the * original so a renderer can pass it through or a test can assert on it, and * `reason` records why it could not be mapped. */ export type ProxyIRUnmappedPart = { kind: "unmapped"; sourceKind: string; reason: string; raw: unknown; }; export type ProxyIRContentPart = ProxyIRTextPart | ProxyIRThinkingPart | ProxyIRImagePart | ProxyIRToolCallPart | ProxyIRToolResultPart | ProxyIRUnmappedPart; /** * One message, with system and developer messages kept in sequence. * * There is no separate bucket for leading instructions: native Codex sends * instruction blocks as ordered `developer` items inside the input array, and * lifting them out would lose their position relative to the conversation. A * codec that needs a leading system field extracts it; the IR does not presume * one exists. */ export type ProxyIRMessage = { role: ProxyIRRole; content: ProxyIRContentPart[]; }; export type ProxyIRFunctionToolDeclaration = { kind: "function"; name: string; description?: string; parametersSchema: Record<string, unknown>; cacheHint?: ProxyIRCacheHint; }; /** A tool constrained by a grammar rather than a JSON schema; Codex-only today. */ export type ProxyIRCustomGrammarToolDeclaration = { kind: "custom_grammar"; name: string; description?: string; grammar: string; }; export type ProxyIRToolDeclaration = ProxyIRFunctionToolDeclaration | ProxyIRCustomGrammarToolDeclaration; export type ProxyIRToolChoice = { kind: "auto"; } | { kind: "none"; } | { kind: "any"; } | { kind: "named"; toolName: string; }; /** * Reasoning configuration, tagged by the dialect that expressed it. * * Anthropic budgets thinking in tokens; Codex selects a named effort level. There * is no faithful numeric mapping between them, so the source is recorded and each * codec decides how to honour the other's form rather than a lossy conversion * happening once, invisibly, in the middle. */ export type ProxyIRReasoning = { source: "none"; } | { source: "anthropic_thinking"; type: string; budgetTokens?: number; } | { source: "codex_effort"; effort: CodexReasoningEffort; }; export type ProxyIRRequest = { sourceFormat: ProxyIRWireFormat; sourceModel: string; originSessionId?: string; originThreadId?: string; messages: ProxyIRMessage[]; tools: ProxyIRToolDeclaration[]; toolChoice?: ProxyIRToolChoice; reasoning: ProxyIRReasoning; stream: boolean; /** Prefix-derived routing hint. Never serialized onto any wire. */ promptCachePrefixKey?: string; }; export type ProxyIRRequestTargetOptions = { model: string; reasoningEffort?: CodexReasoningEffort; }; export type ProxyIRProviderError = { code: string; httpStatus?: number; retryable?: boolean; message?: string; }; /** * How a response ended. * * `finishReason: "other"` carries `rawFinishReason` because Anthropic's * `stop_reason` is typed `string | null` on the wire, deliberately open. A closed * union here would make any legitimate but unmodelled stop reason an * exhaustiveness throw at runtime, turning a successful upstream response into a * proxy failure. */ export type ProxyIRTerminalOutcome = { status: "completed"; finishReason: "end_turn" | "tool_use" | "max_tokens" | "stop_sequence"; } | { status: "completed"; finishReason: "other"; rawFinishReason: string; } | { status: "failed"; error: ProxyIRProviderError; } | { status: "incomplete"; reason: string; }; export type ProxyIRResponseEvent = { kind: "text_delta"; text: string; } | { kind: "thinking_delta"; text: string; } | { kind: "tool_call_start"; callId: string; toolName: string; } | { kind: "tool_call_arguments_delta"; callId: string; partialArguments: string; } | { kind: "tool_call_done"; callId: string; } | { kind: "item_done"; } | { kind: "usage"; usage: UsageContext; } | { kind: "terminal"; outcome: ProxyIRTerminalOutcome; }; export type ProxyIROutputItem = { kind: "text"; text: string; } | { kind: "thinking"; text: string; } | { kind: "tool_call"; callId: string; toolName: string; argumentsRaw: string; argumentsJson?: Record<string, unknown>; }; export type ProxyIRResponse = { items: ProxyIROutputItem[]; usage?: UsageContext; outcome: ProxyIRTerminalOutcome; }; /** * Codec capabilities, kept separate rather than bundled into one interface. * * No dialect needs all four in every direction: for outbound Codex fallback, the * Codex side is only ever parsed from and rendered to, never dispatched to, so a * single monolithic codec type would force stub implementations whose only * possible body is a throw. */ export type ProxyIRRequestParser<TWireRequest> = { format: ProxyIRWireFormat; parseRequest(wire: TWireRequest): ProxyIRRequest; }; export type ProxyIRRequestBuilder<TWireRequest> = { format: ProxyIRWireFormat; buildRequest(ir: ProxyIRRequest, target: ProxyIRRequestTargetOptions): TWireRequest; }; export type ProxyIRResponseParser = { format: ProxyIRWireFormat; parseBufferedResponse(wireText: string): ProxyIRResponse; parseResponseStream(upstream: Response): AsyncGenerator<ProxyIRResponseEvent, ProxyIRResponse>; }; export type ProxyIRResponseRenderer = { format: ProxyIRWireFormat; renderResponseEvent(event: ProxyIRResponseEvent): string[]; renderTerminalOutcome(outcome: ProxyIRTerminalOutcome, usage?: UsageContext): string[]; }; /** Marks a value a codec refused to map, for callers that collect rather than throw. */ export type ProxyIRUnmappableValue = { proxyIRUnmappable: true; sourceKind: string; reason: string; raw: unknown; };