@oh-my-pi/pi-ai 17.3.7 → 17.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +1 -1
  3. package/dist/types/auth/sqlite-credential-store.d.ts +2 -15
  4. package/dist/types/auth-storage.d.ts +4 -2
  5. package/dist/types/dialect/thinking-fence-strip.d.ts +39 -0
  6. package/dist/types/error/finalize.d.ts +2 -0
  7. package/dist/types/error/flags.d.ts +9 -0
  8. package/dist/types/providers/cursor/exec-modern.d.ts +1 -1
  9. package/dist/types/providers/cursor/interaction-query.d.ts +15 -0
  10. package/dist/types/providers/cursor-pi-args.d.ts +1 -1
  11. package/dist/types/providers/cursor.d.ts +14 -5
  12. package/dist/types/providers/openai-reasoning-fallback.d.ts +10 -2
  13. package/dist/types/providers/openai-shared.d.ts +32 -0
  14. package/dist/types/registry/oauth/opencode.d.ts +16 -9
  15. package/dist/types/types.d.ts +8 -1
  16. package/dist/types/usage.d.ts +10 -0
  17. package/dist/types/utils/schema/json-schema-validator.d.ts +5 -5
  18. package/package.json +5 -7
  19. package/src/auth/sqlite-credential-store.ts +11 -24
  20. package/src/auth-gateway/server.ts +29 -3
  21. package/src/auth-storage.ts +62 -10
  22. package/src/dialect/thinking-fence-strip.ts +92 -0
  23. package/src/error/finalize.ts +4 -0
  24. package/src/error/flags.ts +77 -4
  25. package/src/oneshot-retry.ts +4 -0
  26. package/src/providers/amazon-bedrock.ts +3 -2
  27. package/src/providers/anthropic.ts +6 -0
  28. package/src/providers/cursor/exec-modern.ts +2 -2
  29. package/src/providers/cursor/interaction-query.ts +212 -0
  30. package/src/providers/cursor/proto/agent.proto +25 -3
  31. package/src/providers/cursor-pi-args.ts +1 -1
  32. package/src/providers/cursor.ts +124 -27
  33. package/src/providers/devin.ts +10 -16
  34. package/src/providers/google-shared.ts +24 -7
  35. package/src/providers/openai-codex-responses.ts +2 -0
  36. package/src/providers/openai-completions.ts +8 -7
  37. package/src/providers/openai-reasoning-fallback.ts +86 -4
  38. package/src/providers/openai-responses.ts +5 -0
  39. package/src/providers/openai-shared.ts +120 -3
  40. package/src/registry/oauth/callback-server.ts +31 -0
  41. package/src/registry/oauth/opencode.ts +27 -13
  42. package/src/registry/oauth/perplexity.ts +6 -3
  43. package/src/registry/opencode-go.ts +1 -1
  44. package/src/registry/opencode-zen.ts +1 -1
  45. package/src/stream.ts +35 -10
  46. package/src/types.ts +8 -1
  47. package/src/usage/claude.ts +9 -2
  48. package/src/usage.ts +10 -0
  49. package/src/utils/openai-http.ts +30 -0
  50. package/src/utils/schema/json-schema-validator.ts +54 -17
  51. package/src/utils/validation.ts +212 -5
  52. package/src/providers/cursor/proto/buf.gen.yaml +0 -6
  53. package/src/providers/cursor/proto/buf.yaml +0 -17
  54. package/src/providers/devin/proto/buf.gen.yaml +0 -33
  55. package/src/providers/devin/proto/buf.yaml +0 -17
package/CHANGELOG.md CHANGED
@@ -2,6 +2,41 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [17.4.0] - 2026-08-20
6
+
7
+ ### Added
8
+
9
+ - Added model metadata fields (`context_length`, `max_output_tokens`, `input_modalities`, etc.) to auth gateway model listing responses
10
+
11
+ ### Fixed
12
+
13
+ - Fixed tool-argument repair applying lossy transformations (such as stringifying objects or stripping unrecognized keys) when validating union schemas (`anyOf`/`oneOf`), preventing corrupted tool call and subagent payloads
14
+ - Fixed 400 errors when communicating with local OpenAI-compatible inference servers that reject `chat_template_kwargs.reasoning_effort` by improving reasoning effort parameter fallback and compatibility handling
15
+ - Fixed DeepSeek-family models on hosts like Fireworks losing reasoning whenever tools were offered: a redundant `tool_choice: "auto"` is now omitted so the provider keeps thinking enabled; forced and `"none"` selectors still take priority ([#1207](https://github.com/can1357/oh-my-pi/issues/1207))
16
+
17
+ ## [17.3.8] - 2026-08-19
18
+
19
+ ### Changed
20
+
21
+ - Fixed Gemini thought summaries occasionally leaking a raw `` ```thinking `` / `` ``````thinking `` fence delimiter into the reasoning block, so it no longer shows up as fence spam in the thinking display or persisted transcripts ([#8719](https://github.com/can1357/oh-my-pi/issues/8719)).
22
+ - Fixed the OpenCode Go login prompting for an "OpenCode Zen API key": the shared login flow now names the provider you selected, so connecting OpenCode Go asks for an OpenCode Go key (the `opencode.ai/auth` console is still shared, as documented upstream) ([#8738](https://github.com/can1357/oh-my-pi/issues/8738)).
23
+ - Fixed Anthropic-compatible endpoints with strict prompt validation (e.g. Z.AI GLM `api.z.ai/api/anthropic`, which rejects the whole request with `400 code 1213 "The prompt parameter was not received normally"`) failing sessions once a tool returned empty output on a vision-capable model: empty successful `tool_result` blocks now encode as `content: ""` instead of `content: []`, which both the official API and strict compatible endpoints accept.
24
+ - Fixed `retry.usageReservePct` (Reserve Margin) ignoring Claude Fable/Mythos weekly tier usage until it hit 100%, so a Fable model kept serving turns past the configured reserve; reserve health now honors the mapped tier row while credential-wide hard blocks still require confirmed exhaustion ([#8773](https://github.com/can1357/oh-my-pi/issues/8773)).
25
+ - Fixed `cursor-agent` streams stalling with "Provider stream stalled while waiting for the next event" when Cursor asked the client to approve a hosted WebFetch / web search (reproduced on `cursor-grok-4.6-xhigh` after "I'll fetch the page…"). Those `interaction_query` frames — including the newer WebFetch field 9 this proto did not name — were dropped, so the server waited forever and the idle watchdog aborted a live connection. Permission queries are now answered; hosted search/fetch is approved, unnamed permission fields get an `approved` reply on the same field number, and prompts this client cannot serve are rejected so the turn can continue.
26
+
27
+ ### Fixed
28
+
29
+ - Fixed thinking effort selections being ignored for local Qwen 3.8+ models on llama.cpp and vLLM: the Qwen chat-completions dialects only toggled `enable_thinking`, so the chat template always reasoned at its `xhigh` default no matter which level was selected. The encoder now routes the requested effort onto the template's `reasoning_effort` kwarg (`chat_template_kwargs` for both Qwen dialects, plus the top-level field newer llama.cpp builds map natively).
30
+ - Fixed OpenAI Completions, Amazon Bedrock, and Cursor providers ignoring `onPayload` replacement payloads. The hook now transforms the actual request body sent upstream on these providers, matching the Anthropic/Gemini/OpenAI Responses replacement contract. `devin-agent` still does not fire the hook (its payload is a protobuf object).
31
+ - Fixed Codex requests failing outright when the signed-in ChatGPT account is not entitled to the requested model; the exact model denial is now classified as an account-policy error so credential rotation can reach an entitled sibling account
32
+ - Fixed Perplexity email-OTP login after its verification response renamed the encrypted session token from `token` to `challenge_token`.
33
+ - Cloud Code Assist Gemini 3.6/3.7 Flash requests at `minimal` now send `thinkingLevel: LOW` on the aliased `-low` SKU instead of `MINIMAL`, which the API rejects with HTTP 400.
34
+ - Answer Cursor `interaction_query` permission gates (hosted web search, Exa, unnamed field-9 WebFetch) so the Run RPC continues instead of sitting silent until the 300s idle watchdog.
35
+ - Fixed provider tool calls arriving with flattened array argument paths (e.g. Gemini's `questions[0].id`) being stripped and rejected by argument validation; well-formed flattened paths are now rebuilt into the nested arrays the tool schema expects ([#8886](https://github.com/can1357/oh-my-pi/issues/8886)).
36
+ - Fixed opencode-go (Console Go) rejecting Responses turns with `400 No tool output found for tool call …` (naming a random call of the batch on each retry) when a model streamed a trailing text/thinking block after its tool calls: `buildResponsesInput` emitted that block as an assistant `message` item wedged between the `function_call` batch and its `function_call_output` items. Such interleaved messages are now hoisted ahead of their call batch (canonical `message(s) → calls → outputs`), which the strict gateway validator accepts; content is unchanged ([#8789](https://github.com/can1357/oh-my-pi/issues/8789)).
37
+ - Fixed the OpenAI-wire transport sleeping on a LiteLLM concurrency-admission 429 (`rate_limit_type: max_parallel_requests`, `Retry-After: 60`) and retrying it up to 6 times (~300s) before session recovery saw the error. Because a 60s hint equals the transport's `maxDelayMs` cap, `fetchWithRetry` kept sleeping and retrying; the request now surfaces on the first attempt so `TurnRecovery`'s concurrency backoff/model fallback runs promptly. Genuine RPM/quota 429s (no such marker) still honor `Retry-After` ([#8854](https://github.com/can1357/oh-my-pi/issues/8854)).
38
+ - Fixed OAuth login (Codex `localhost:1455`, and any `localhost` callback flow) failing on hosts with IPv6 disabled at the kernel (`ipv6.disable=1`). The `::1` companion listener added in #8081 fails there with Bun's generic "Is port X in use?" message (oven-sh/bun#7187), which the in-use check misread as a real collision — tearing down the healthy IPv4 listener and surfacing a bogus "port 1455 is in use" error. The dual-bind path now detects the missing IPv6 loopback up front and serves IPv4 alone ([#8814](https://github.com/can1357/oh-my-pi/issues/8814)).
39
+
5
40
  ## [17.3.7] - 2026-08-17
6
41
 
7
42
  ### Changed
package/README.md CHANGED
@@ -634,7 +634,7 @@ All providers accept the base `StreamOptions` (in addition to provider-specific
634
634
  - `headers`: Extra request headers merged on top of model-defined headers
635
635
  - `sessionId`: Provider-specific session identifier (prompt caching/routing)
636
636
  - `signal`: Abort in-flight requests
637
- - `onPayload`: Callback invoked with the provider request payload just before sending
637
+ - `onPayload`: Callback invoked with the provider request payload just before sending. Return a replacement payload object (sync or async) to send it instead of the original; return `undefined` to keep the original. The replacement is applied by every provider that fires the hook — all of them except `devin-agent`, whose payload is a protobuf object and does not fire the hook yet.
638
638
 
639
639
  Example:
640
640
 
@@ -5,6 +5,7 @@
5
5
  * and remote stores share the same contract.
6
6
  */
7
7
  import { Database } from "bun:sqlite";
8
+ import { isSqliteBusyError, isSqliteCorruptionError } from "@oh-my-pi/pi-utils";
8
9
  import type { AuthCredential, AuthCredentialStore, CredentialRefreshLeaseFence, DisabledCredentialSummary, StoredAuthCredential, StoredCredentialBlock } from "../auth-storage.js";
9
10
  import type { OAuthCredentials } from "../registry/oauth/types.js";
10
11
  import type { ClientUsageReport, ClientUsageSummary, UsageHistoryEntry, UsageHistoryQuery } from "../usage.js";
@@ -14,20 +15,7 @@ type SerializedCredentialRecord = {
14
15
  data: string;
15
16
  identityKey: string | null;
16
17
  };
17
- /**
18
- * SQLite's busy result code family — base `SQLITE_BUSY` plus the extended
19
- * variants `SQLITE_BUSY_RECOVERY` (concurrent WAL recovery), `SQLITE_BUSY_SNAPSHOT`,
20
- * and `SQLITE_BUSY_TIMEOUT`. All warrant the same backoff-and-retry treatment.
21
- */
22
- export declare function isSqliteBusyError(err: unknown): boolean;
23
- /**
24
- * SQLite's unrecoverable-corruption result codes — the `SQLITE_CORRUPT` family
25
- * (base plus extended variants like `SQLITE_CORRUPT_VTAB` / `SQLITE_CORRUPT_INDEX`)
26
- * and `SQLITE_NOTADB` (the file header is not a database). Unlike
27
- * {@link isSqliteBusyError}, these never clear by retrying: the store must be
28
- * repaired or replaced, so callers latch and stop touching it.
29
- */
30
- export declare function isSqliteCorruptionError(err: unknown): boolean;
18
+ export { isSqliteBusyError, isSqliteCorruptionError };
31
19
  export declare function serializeCredential(provider: string, credential: AuthCredential): SerializedCredentialRecord | null;
32
20
  export declare function resolveCredentialIdentityKey(provider: string, credential: AuthCredential): string | null;
33
21
  /**
@@ -112,4 +100,3 @@ export declare class SqliteAuthCredentialStore implements AuthCredentialStore {
112
100
  acknowledgeLocalChanges(): void;
113
101
  close(): void;
114
102
  }
115
- export {};
@@ -1142,8 +1142,10 @@ export declare class AuthStorage {
1142
1142
  * - usage-limit / account-rate-limit error → {@link AuthStorage.markUsageLimitReached}
1143
1143
  * (temporary block via its own backoff — default plus server usage-report
1144
1144
  * reset; sticky left intact so the next resolve re-ranks around the block).
1145
- * - account-scoped policy denial → temporarily block that account without
1146
- * marking its credential suspect, then rotate through eligible siblings.
1145
+ * - exact Codex model-entitlement denial → temporarily block only that
1146
+ * requested model after provider/model identity matches, then rotate.
1147
+ * - other account-scoped policy denial → temporarily block that account
1148
+ * without marking its credential suspect, then rotate through siblings.
1147
1149
  * - otherwise (hard 401 / auth failure) → mark the credential suspect (or
1148
1150
  * reload when no broker hook is wired) and block it, then drop matching
1149
1151
  * sticky state.
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Strip self-referential reasoning-fence delimiters that a provider leaks
3
+ * *inside* a structured thinking part.
4
+ *
5
+ * The visible-channel healers ({@link ../utils/leaked-thinking-stream},
6
+ * {@link ./fenced-thinking}) split leaked ` ```thinking ` fences out of the
7
+ * *text* stream. They never run over parts a provider already flags as
8
+ * thinking, so when a model (observed on Gemini thought summaries — issue
9
+ * #8719) emits a bare ` ```thinking ` / ` ``````thinking ` opener line between
10
+ * summaries, that delimiter reaches display and persistence verbatim as fence
11
+ * spam inside the reasoning block.
12
+ *
13
+ * This stripper removes only a *standalone* reasoning-fence opener line — a line
14
+ * that is nothing but a run of ≥3 backticks immediately followed by the info
15
+ * string `thinking` or `reasoning`. Such a line is always redundant inside an
16
+ * already-structured thinking block and never carries content. Inline mentions
17
+ * (prose on the same line), language-tagged code fences (` ```rs `), and bare
18
+ * closers (` ``` `) are left untouched so legitimate fenced code inside the
19
+ * reasoning survives.
20
+ *
21
+ * Streaming-safe: deltas may split a line anywhere. A trailing partial line is
22
+ * held only while it remains a viable opener prefix; the moment it cannot be an
23
+ * opener it is flushed and the rest of the line passes through character-level.
24
+ * Correctness never depends on the prefix heuristic — every held line is
25
+ * classified strictly on its newline (or on {@link ThinkingFenceStripper.flush})
26
+ * before it is dropped.
27
+ */
28
+ /**
29
+ * Stateful, line-oriented stripper for leaked reasoning-fence openers in one
30
+ * structured thinking block. One instance per thinking block; feed every
31
+ * thinking delta through {@link push} and drain the tail with {@link flush}.
32
+ */
33
+ export declare class ThinkingFenceStripper {
34
+ #private;
35
+ /** Consume one thinking delta; returns the sanitized text to emit (may be empty). */
36
+ push(chunk: string): string;
37
+ /** Drain any held partial line at block end; returns text to emit (may be empty). */
38
+ flush(): string;
39
+ }
@@ -7,6 +7,8 @@ export interface FinalizeOptions {
7
7
  api?: Api;
8
8
  /** Provider id; forwarded to the message formatter for copilot rewrites. */
9
9
  provider?: string;
10
+ /** Requested model id; paired with provider for model-entitlement classification. */
11
+ model?: string;
10
12
  /** Caller signal, for providers that don't run an abort tracker. */
11
13
  signal?: AbortSignal;
12
14
  /** Abort tracker, preferred over `signal`: distinguishes caller vs. local aborts. */
@@ -58,6 +58,13 @@ export declare function classify(error: unknown, api?: Api): number;
58
58
  export declare function isUsageLimit(error: unknown, api?: Api): boolean;
59
59
  /** Whether an upstream rejection is an account-scoped policy denial worth retrying with a sibling credential. */
60
60
  export declare function isAccountPolicyError(error: unknown, api?: Api): boolean;
61
+ /**
62
+ * Model id from Codex's exact ChatGPT-account entitlement denial. Generic
63
+ * unsupported-model invalid requests deliberately do not match.
64
+ */
65
+ export declare function codexChatGPTAccountPolicyModel(error: unknown, depth?: number): string | undefined;
66
+ /** Whether the exact Codex entitlement denial applies to this provider and requested model. */
67
+ export declare function isCodexChatGPTAccountPolicyError(error: unknown, provider: string, modelId: string | undefined): boolean;
61
68
  /**
62
69
  * Strict-tool rejection: grammar too large, schema too complex, or structured
63
70
  * outputs unsupported by the model/endpoint.
@@ -78,6 +85,8 @@ export declare function isFastModeUnsupported(error: unknown): boolean;
78
85
  export declare function isCopilotTransientModelError(error: unknown): boolean;
79
86
  export declare function classifyMessage(message: {
80
87
  api?: Api;
88
+ provider?: string;
89
+ model?: string;
81
90
  errorId?: number;
82
91
  errorMessage?: string;
83
92
  errorStatus?: number;
@@ -9,7 +9,7 @@
9
9
  * `ExecClientMessage` carrying a result with an unset oneof is a fake success
10
10
  * the server reads as "the tool ran and produced nothing".
11
11
  */
12
- import { type ExecuteHookRequest, type ExecuteHookResult, type McpStateExecResult, type McpToolDefinition, type PiBashExecResult, type PiEditExecResult, type PiFindExecResult, type PiGrepExecResult, type PiLsExecResult, type PiReadExecResult, type PiTruncation, type PiWriteExecResult } from "@oh-my-pi/pi-catalog/discovery/cursor-gen/agent_pb";
12
+ import { type ExecuteHookRequest, type ExecuteHookResult, type McpStateExecResult, type McpToolDefinition, type PiBashExecResult, type PiEditExecResult, type PiFindExecResult, type PiGrepExecResult, type PiLsExecResult, type PiReadExecResult, type PiTruncation, type PiWriteExecResult } from "@oh-my-pi/pi-catalog/discovery/cursor-proto";
13
13
  import type { ToolResultMessage } from "../../types.js";
14
14
  /**
15
15
  * The pure arg translation lives in `../cursor-pi-args` so the legacy pi shim
@@ -0,0 +1,15 @@
1
+ import type http2 from "node:http2";
2
+ import { type InteractionQuery } from "@oh-my-pi/pi-catalog/discovery/cursor-proto";
3
+ /**
4
+ * Answer a Cursor `interaction_query` so the Run RPC can continue.
5
+ *
6
+ * Hosted web search / Exa / unnamed permission gates (field 9 = WebFetch on
7
+ * current Cursor builds) block the turn until the client writes an
8
+ * `interaction_response`. Dropping the frame leaves the HTTP/2 stream alive
9
+ * on heartbeats that are not semantic progress; the lazy idle watchdog then
10
+ * aborts with "Provider stream stalled while waiting for the next event".
11
+ *
12
+ * Unsupported interactive queries are rejected so the server is not stranded.
13
+ * VM setup is left unanswered rather than reporting a fake success.
14
+ */
15
+ export declare function handleInteractionQuery(query: InteractionQuery, h2Request: http2.ClientHttp2Stream): void;
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * Kept apart from `cursor/exec-modern.ts` on purpose: these are pure
11
11
  * string/path functions with no protobuf coupling, while that module pulls in
12
- * `@bufbuild/protobuf` and the generated `agent_pb` graph. The legacy shim is
12
+ * the generated cursor protobuf graph. The legacy shim is
13
13
  * compiled into the bundled virtual module registry, so importing it from a
14
14
  * nested path would drag the whole exec implementation in with it — and
15
15
  * `./providers/*` is a single-segment wildcard export that cannot serve a
@@ -1,8 +1,8 @@
1
1
  import http2 from "node:http2";
2
- import { type JsonValue } from "@bufbuild/protobuf";
3
- import type { McpToolDefinition } from "@oh-my-pi/pi-catalog/discovery/cursor-gen/agent_pb";
4
- import { type AgentServerMessage, type ConversationStateStructure } from "@oh-my-pi/pi-catalog/discovery/cursor-gen/agent_pb";
5
- import type { AssistantMessage, CursorExecHandlerResult, CursorExecHandlers, CursorExecPairing, CursorTodoSyncHandler, CursorToolResultHandler, Message, StreamFunction, StreamOptions, TextContent, ThinkingContent, Tool, ToolCall, ToolResultMessage } from "../types.js";
2
+ import type { McpToolDefinition } from "@oh-my-pi/pi-catalog/discovery/cursor-proto";
3
+ import { type AgentServerMessage, type ConversationStateStructure } from "@oh-my-pi/pi-catalog/discovery/cursor-proto";
4
+ import { type JsonValue } from "@oh-my-pi/pi-catalog/discovery/protobuf";
5
+ import type { AssistantMessage, Context, CursorExecHandlerResult, CursorExecHandlers, CursorExecPairing, CursorTodoSyncHandler, CursorToolResultHandler, Message, Model, StreamFunction, StreamOptions, TextContent, ThinkingContent, Tool, ToolCall, ToolResultMessage } from "../types.js";
6
6
  import { kCursorExecResolved, kStreamingBlockIndex, kStreamingBlockKind, kStreamingEnvelopeId, kStreamingLastParseLen, kStreamingPartialJson } from "../utils/block-symbols.js";
7
7
  import { AssistantMessageEventStream } from "../utils/event-stream.js";
8
8
  export declare const CURSOR_API_URL = "https://api2.cursor.sh";
@@ -32,7 +32,7 @@ export type ToolCallState = ToolCall & {
32
32
  [kStreamingBlockIndex]: number;
33
33
  [kStreamingPartialJson]?: string;
34
34
  [kStreamingLastParseLen]?: number;
35
- [kStreamingBlockKind]: "mcp" | "todo" | "cursor-exec" | "connect-scm";
35
+ [kStreamingBlockKind]: "mcp" | "todo" | "cursor-exec" | "connect-scm" | "web-fetch";
36
36
  [kStreamingEnvelopeId]?: string;
37
37
  [kCursorExecResolved]?: true;
38
38
  };
@@ -219,3 +219,12 @@ export declare function buildCursorHistoryForTest(messages: Message[], activeUse
219
219
  turnUserMessagesJson: JsonValue[];
220
220
  turnStepMessagesJson: JsonValue[][];
221
221
  };
222
+ export declare function buildGrpcRequest(model: Model<"cursor-agent">, context: Context, options: CursorOptions | undefined, state: {
223
+ conversationId: string;
224
+ blobStore: Map<string, Uint8Array>;
225
+ conversationState?: ConversationStateStructure;
226
+ }): Promise<{
227
+ requestBytes: Uint8Array;
228
+ blobStore: Map<string, Uint8Array>;
229
+ conversationState: ConversationStateStructure;
230
+ }>;
@@ -1,6 +1,14 @@
1
1
  import type { CapturedHttpErrorResponse } from "../utils/http-inspector.js";
2
- /** @internal */
3
- export type OpenAIReasoningEffortFallback = string | null;
2
+ /**
3
+ * Fallback marker: the server rejected the `chat_template_kwargs.reasoning_effort`
4
+ * spelling itself (strict kwargs whitelists — Ninfer-style servers), not the
5
+ * effort value. Apply strips the kwarg and hoists the value onto the top-level
6
+ * `reasoning_effort` field when that spelling is absent.
7
+ * @internal
8
+ */
9
+ export declare const STRIP_TEMPLATE_KWARG_REASONING_EFFORT: unique symbol;
10
+ /** @internal */
11
+ export type OpenAIReasoningEffortFallback = string | null | typeof STRIP_TEMPLATE_KWARG_REASONING_EFFORT;
4
12
  /** @internal */
5
13
  export interface OpenAIReasoningEffortFallbackState {
6
14
  reasoningEffortFallbacks: Map<string, OpenAIReasoningEffortFallback>;
@@ -231,6 +231,7 @@ export type OpenAICompletionsParams = Omit<ChatCompletionCreateParamsStreaming,
231
231
  chat_template_kwargs?: {
232
232
  enable_thinking?: boolean;
233
233
  preserve_thinking?: boolean;
234
+ reasoning_effort?: string;
234
235
  };
235
236
  reasoning?: {
236
237
  effort?: string;
@@ -314,6 +315,20 @@ export interface OpenAICompatPolicy {
314
315
  export declare function mapOpenAIReasoningEffort(model: Pick<Model, "thinking">, compat: {
315
316
  reasoningEffortMap?: Partial<Record<Effort, string>>;
316
317
  } | undefined, effort: string): string;
318
+ /**
319
+ * Whether a redundant `tool_choice: "auto"` should be dropped to keep
320
+ * reasoning alive. Hosts with `disableReasoningOnToolChoice` (DeepSeek family
321
+ * on e.g. Fireworks) silently turn reasoning off whenever any `tool_choice`
322
+ * is present. "auto" is already the provider default, so omitting it is
323
+ * wire-neutral for tool selection; forced and "none" choices are semantic and
324
+ * still win over reasoning (#1207).
325
+ */
326
+ export declare function shouldDropAutoToolChoiceForReasoning(model: Pick<Model, "reasoning">, compat: {
327
+ disableReasoningOnToolChoice: boolean;
328
+ }, toolChoice: unknown, options: {
329
+ reasoning?: string;
330
+ disableReasoning?: boolean;
331
+ } | undefined): boolean;
317
332
  export declare function resolveOpenAICompatPolicy<TApi extends Api>(model: Model<TApi>, options: ResolveOpenAICompatPolicyOptions): OpenAICompatPolicy;
318
333
  export declare function applyChatCompletionsCompatPolicy(params: OpenAICompletionsParams, policy: OpenAICompatPolicy): void;
319
334
  export declare function applyChatCompletionsReasoningParams(params: OpenAICompletionsParams, model: Model<"openai-completions">, compat: ResolvedOpenAICompat, options: (ChatCompletionsReasoningOptions & {
@@ -403,6 +418,23 @@ export declare function repairOrphanResponsesToolOutputs(input: ResponseInput):
403
418
  * {@link repairOrphanResponsesToolOutputs}.
404
419
  */
405
420
  export declare function repairOrphanResponsesToolCalls(input: ResponseInput): ResponseInput;
421
+ /**
422
+ * Relocate assistant `message` items wedged inside a tool-call → tool-output
423
+ * batch to before the batch, yielding canonical `message(s) → calls → outputs`
424
+ * order. Idempotent; returns the same array reference when nothing moves.
425
+ *
426
+ * OpenAI's Responses API pairs tool outputs by `call_id` and tolerates any item
427
+ * order, but stricter gateways (notably opencode-go's "Console Go") reject a
428
+ * shape where an assistant message interrupts a `function_call` →
429
+ * `function_call_output` run, 400ing with `No tool output found for tool call …`
430
+ * (naming a random call of the batch on each retry). This arises whenever a
431
+ * model streams a trailing text / demoted-thinking block *after* its tool calls:
432
+ * the block-encode path preserves stream order, emitting the message between the
433
+ * calls and the outputs appended afterward. Moving the already-model-owned
434
+ * message ahead of its call batch keeps content identical while satisfying the
435
+ * strict validator. See #8789.
436
+ */
437
+ export declare function hoistInterleavedResponsesToolBatchMessages<T extends object>(items: readonly T[]): T[];
406
438
  export declare function convertResponsesInputContent(content: string | Array<TextContent | ImageContent>, supportsImages: boolean, supportsImageDetailOriginal: boolean, escapeControlTokens?: boolean): ResponseInputContent[] | undefined;
407
439
  export interface BuildResponsesInputOptions<TApi extends Api> {
408
440
  model: Model<TApi>;
@@ -1,18 +1,25 @@
1
1
  /**
2
- * OpenCode Zen login flow.
2
+ * OpenCode login flow, shared by the OpenCode Zen and OpenCode Go providers.
3
3
  *
4
- * OpenCode Zen is a subscription service that provides access to various AI models
5
- * (GPT-5.x, Claude 4.x, Gemini 3, etc.) through a unified API at opencode.ai/zen.
6
- * This is not OAuth - it's a simple API key flow:
4
+ * Both are subscription services whose API keys are issued from the same
5
+ * OpenCode Zen console at https://opencode.ai/auth OpenCode Go keys are
6
+ * minted there after subscribing to Go (see https://opencode.ai/docs/go).
7
+ * This is not OAuth; it's a simple paste-the-API-key flow:
7
8
  * 1. Open browser to https://opencode.ai/auth
8
- * 2. User logs in and copies their API key
9
+ * 2. User logs in (and subscribes to Go, for OpenCode Go) and copies the key
9
10
  * 3. User pastes the API key back into the CLI
10
11
  */
11
12
  import type { OAuthController } from "./types.js";
12
13
  /**
13
- * Login to OpenCode Zen.
14
+ * Log in to an OpenCode subscription provider.
14
15
  *
15
- * Opens browser to auth page, prompts user to paste their API key.
16
- * Returns the API key directly (not OAuthCredentials - this isn't OAuth).
16
+ * Opens the browser to the OpenCode Zen console, prompts the user to paste
17
+ * their API key, and returns it directly (not OAuthCredentials this isn't
18
+ * OAuth).
19
+ *
20
+ * @param providerName Display name of the provider being connected
21
+ * ("OpenCode Zen" or "OpenCode Go"). Used verbatim in the paste prompt so
22
+ * the CLI reflects the provider the user actually selected instead of always
23
+ * asking for a Zen key.
17
24
  */
18
- export declare function loginOpenCode(options: OAuthController): Promise<string>;
25
+ export declare function loginOpenCode(options: OAuthController, providerName?: string): Promise<string>;
@@ -1,7 +1,7 @@
1
1
  export * from "@oh-my-pi/pi-catalog/effort";
2
2
  export * from "@oh-my-pi/pi-catalog/types";
3
3
  import type { Type } from "@oh-my-pi/omptype";
4
- import type { DeleteArgs, DeleteResult, DiagnosticsArgs, DiagnosticsResult, GrepArgs, GrepResult, LsArgs, LsResult, McpResult, PiBashExecArgs, PiBashExecResult, PiEditExecArgs, PiEditExecResult, PiFindExecArgs, PiFindExecResult, PiGrepExecArgs, PiGrepExecResult, PiLsExecArgs, PiLsExecResult, PiReadExecArgs, PiReadExecResult, PiWriteExecArgs, PiWriteExecResult, ReadArgs, ReadResult, ShellArgs, ShellResult, WriteArgs, WriteResult } from "@oh-my-pi/pi-catalog/discovery/cursor-gen/agent_pb";
4
+ import type { DeleteArgs, DeleteResult, DiagnosticsArgs, DiagnosticsResult, GrepArgs, GrepResult, LsArgs, LsResult, McpResult, PiBashExecArgs, PiBashExecResult, PiEditExecArgs, PiEditExecResult, PiFindExecArgs, PiFindExecResult, PiGrepExecArgs, PiGrepExecResult, PiLsExecArgs, PiLsExecResult, PiReadExecArgs, PiReadExecResult, PiWriteExecArgs, PiWriteExecResult, ReadArgs, ReadResult, ShellArgs, ShellResult, WriteArgs, WriteResult } from "@oh-my-pi/pi-catalog/discovery/cursor-proto";
5
5
  import type { Effort } from "@oh-my-pi/pi-catalog/effort";
6
6
  import type { Api, FetchImpl, Model, Provider, ThinkingBudgets, Usage } from "@oh-my-pi/pi-catalog/types";
7
7
  import type { ApiKey } from "./auth-retry.js";
@@ -701,6 +701,13 @@ export interface ContextSnapshot {
701
701
  nonMessageTokens: number;
702
702
  /** Estimated prompt tokens removed by local history rewrites after this provider snapshot was recorded. */
703
703
  historyRewriteTokensRemoved?: number;
704
+ /**
705
+ * Compaction epoch current when this snapshot's provider request was recorded.
706
+ * A later compaction bumps the session epoch, so an anchor whose epoch is
707
+ * older than the current in-flight snapshot describes pre-compaction history
708
+ * and must not override the rebased estimate.
709
+ */
710
+ compactionEpoch?: number;
704
711
  lastMessageTimestamp?: number;
705
712
  }
706
713
  export interface AssistantMessage {
@@ -482,6 +482,16 @@ export interface CredentialRankingStrategy {
482
482
  * account-wide quotas can omit this and use all limits.
483
483
  */
484
484
  scopeLimits?(report: UsageReport, context?: CredentialRankingContext): UsageLimit[];
485
+ /**
486
+ * Restrict limits for the opt-in, non-destructive usage-reserve health
487
+ * check ({@link AuthStorage.getModelUsageHealth}). Distinct from
488
+ * {@link scopeLimits}, which gates credential-wide hard blocks: a provider
489
+ * whose model/tier counters are trusted only at confirmed exhaustion for
490
+ * hard-blocking can still expose them here so the reserve margin protects
491
+ * the mapped quota before it hits the cap. Falls back to {@link scopeLimits}
492
+ * when omitted.
493
+ */
494
+ scopeLimitsForReserve?(report: UsageReport, context?: CredentialRankingContext): UsageLimit[];
485
495
  /**
486
496
  * Return a provider-local backoff scope for the requested model. Providers
487
497
  * with backend-specific quotas use this so one exhausted model family does
@@ -4,11 +4,11 @@ export interface JsonSchemaValidationIssue {
4
4
  expectedTypes?: string[];
5
5
  keyword?: string;
6
6
  /**
7
- * Marks issues that originate inside a failed `anyOf` / `oneOf` branch.
8
- * Consumers such as the tool-argument coercion layer use this to avoid
9
- * applying type repairs (e.g. singleton-array wrapping) that would be
10
- * authoritative outside of a combinator but are only one candidate
11
- * branch's expectation here.
7
+ * Marks issues surfaced from a failed `anyOf` / `oneOf` branch (at any
8
+ * depth). Such a diagnosis is one candidate branch's guess, not
9
+ * authoritative: the tool-argument coercion layer keeps lossy repairs
10
+ * (container stringification, unrecognized-key deletion, singleton-array
11
+ * wrapping) off for these while still applying lossless ones.
12
12
  */
13
13
  fromUnionBranch?: boolean;
14
14
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-ai",
4
- "version": "17.3.7",
4
+ "version": "17.4.0",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -37,14 +37,12 @@
37
37
  "fmt": "biome format --write ."
38
38
  },
39
39
  "dependencies": {
40
- "@bufbuild/protobuf": "^2.12.1",
41
- "@oh-my-pi/omptype": "17.3.7",
42
- "@oh-my-pi/pi-catalog": "17.3.7",
43
- "@oh-my-pi/pi-utils": "17.3.7",
44
- "@oh-my-pi/pi-wire": "17.3.7"
40
+ "@oh-my-pi/omptype": "17.4.0",
41
+ "@oh-my-pi/pi-catalog": "17.4.0",
42
+ "@oh-my-pi/pi-utils": "17.4.0",
43
+ "@oh-my-pi/pi-wire": "17.4.0"
45
44
  },
46
45
  "devDependencies": {
47
- "@bufbuild/protoc-gen-es": "^2.12.1",
48
46
  "@types/bun": "^1.3.14"
49
47
  },
50
48
  "engines": {
@@ -8,7 +8,13 @@ import { Database, type Statement } from "bun:sqlite";
8
8
  import * as fs from "node:fs/promises";
9
9
  import * as path from "node:path";
10
10
  import { parseAlibabaTokenPlanCredential } from "@oh-my-pi/pi-catalog/wire/alibaba-token-plan";
11
- import { getAgentDbPath, getDbBusyTimeoutMs, logger } from "@oh-my-pi/pi-utils";
11
+ import {
12
+ getAgentDbPath,
13
+ getDbBusyTimeoutMs,
14
+ isSqliteBusyError,
15
+ isSqliteCorruptionError,
16
+ logger,
17
+ } from "@oh-my-pi/pi-utils";
12
18
  import type {
13
19
  AuthCredential,
14
20
  AuthCredentialStore,
@@ -87,29 +93,10 @@ const LEGACY_CODEX_BLOCK_PROVIDER_KEY = "openai-codex:oauth";
87
93
  const LEGACY_CODEX_BLOCK_SCOPE = "shared";
88
94
  const CODEX_METER_BLOCK_SCOPES = ["chat", "spark"] as const;
89
95
 
90
- /**
91
- * SQLite's busy result code family base `SQLITE_BUSY` plus the extended
92
- * variants `SQLITE_BUSY_RECOVERY` (concurrent WAL recovery), `SQLITE_BUSY_SNAPSHOT`,
93
- * and `SQLITE_BUSY_TIMEOUT`. All warrant the same backoff-and-retry treatment.
94
- */
95
- export function isSqliteBusyError(err: unknown): boolean {
96
- if (err === null || typeof err !== "object") return false;
97
- const code = (err as { code?: unknown }).code;
98
- return typeof code === "string" && code.startsWith("SQLITE_BUSY");
99
- }
100
-
101
- /**
102
- * SQLite's unrecoverable-corruption result codes — the `SQLITE_CORRUPT` family
103
- * (base plus extended variants like `SQLITE_CORRUPT_VTAB` / `SQLITE_CORRUPT_INDEX`)
104
- * and `SQLITE_NOTADB` (the file header is not a database). Unlike
105
- * {@link isSqliteBusyError}, these never clear by retrying: the store must be
106
- * repaired or replaced, so callers latch and stop touching it.
107
- */
108
- export function isSqliteCorruptionError(err: unknown): boolean {
109
- if (err === null || typeof err !== "object" || !("code" in err)) return false;
110
- const code = err.code;
111
- return typeof code === "string" && (code.startsWith("SQLITE_CORRUPT") || code === "SQLITE_NOTADB");
112
- }
96
+ // SQLite error classifiers live in pi-utils so the credential store and the
97
+ // model cache share one implementation; re-exported here to preserve the
98
+ // pre-existing `@oh-my-pi/pi-ai/auth-storage` surface.
99
+ export { isSqliteBusyError, isSqliteCorruptionError };
113
100
 
114
101
  function normalizeStoredAccountId(accountId: string | null | undefined): string | null {
115
102
  const normalized = accountId?.trim();
@@ -730,19 +730,45 @@ async function handleCredentialsCheck(storage: AuthStorage, signal: AbortSignal)
730
730
  return json(200, { generatedAt: Date.now(), credentials });
731
731
  }
732
732
 
733
+ /**
734
+ * Row shape for `GET /v1/models`. Beyond the OpenAI-standard `id`/`object`/
735
+ * `owned_by`, rows advertise the catalog metadata OpenAI-compatible clients
736
+ * (omp's own proxy discovery, Zed's openai_compatible provider, ...) read to
737
+ * size and capability-gate discovered models: `context_length`,
738
+ * `max_output_tokens`, `input_modalities`, and `supports_tools` (only emitted
739
+ * when the catalog explicitly reports `false`; absent means usable).
740
+ */
741
+ interface ModelListRow {
742
+ id: string;
743
+ object: "model";
744
+ owned_by: string;
745
+ api: Api;
746
+ display_name: string;
747
+ context_length?: number;
748
+ max_output_tokens?: number;
749
+ input_modalities: ("text" | "image")[];
750
+ supports_tools?: boolean;
751
+ }
752
+
733
753
  function handleModelsList(opts: AuthGatewayBootOptions): Response {
734
754
  const seen = new Set<string>();
735
- const data: Array<{ id: string; object: "model"; owned_by: string; api: Api }> = [];
755
+ const data: ModelListRow[] = [];
736
756
  for (const model of opts.listModels?.() ?? []) {
737
757
  const id = `${model.provider}/${model.id}`;
738
758
  if (seen.has(id)) continue;
739
759
  seen.add(id);
740
- data.push({
760
+ const row: ModelListRow = {
741
761
  id,
742
762
  object: "model",
743
763
  owned_by: model.provider,
744
764
  api: model.api,
745
- });
765
+ display_name: model.name,
766
+ input_modalities: model.input,
767
+ };
768
+ if (model.contextWindow != null) row.context_length = model.contextWindow;
769
+ if (model.maxTokens != null) row.max_output_tokens = model.maxTokens;
770
+ if (model.supportsTools === false) row.supports_tools = false;
771
+ data.push(row);
746
772
  }
747
773
  return json(200, { object: "list", data });
748
774
  }