From eaf28b341daaad8734d0289740413711883b85b1 Mon Sep 17 00:00:00 2001 From: David Dembeck Date: Tue, 31 Mar 2026 21:41:29 -0500 Subject: [PATCH 01/19] fix(ai): omp commit fails with HTTP 400 on OpenAI-compatible endpoints when reasoning enabled (#577) * fix(ai): Added developer role support for reasoning-enabled OpenAI models - Added supportsDeveloperRole function to check developer role support based on provider and base URL. - Updated system prompt role logic to use developer role only when supported. - Improved reasoning content handling with provider-specific role enforcement. * feat(ai): Added developer role support for reasoning models with provider fallback - Exported supportsDeveloperRole function to enable cross-provider usage. - Updated Azure provider to check developer role support before assignment. - Implemented system role fallback when developer role is unsupported. - Fixed role assignment to use developer role when supported. * feat(ai): added support for Azure OpenAI inference endpoint - Allow Azure OpenAI Inference endpoint (azure.com/openai) via `includes()` matching on baseUrl, enabling broader Azure-compatibility. * test: add test file for developer role handler in OpenAI responses provider * fix(ai): fixed developer role validation to reject custom base URLs - Changed supportsDeveloperRole to use base URL pattern matching instead of provider short-circuit. - Added tests to verify developer role support returns false for custom proxy base URLs. * fix(ai): Updated GitHub Copilot endpoint detection - Expands GitHub Copilot endpoint detection to support multiple GitHub Copilot enterprise and copilot-api domains. - Adds 3 new test scenarios wrapping unsupported/correct patterns for github-copilot provider endpoints. * chore(ci): Add *.tmp pattern in OS section - Adds *.tmp to ignore all temporary files with .tmp extension - Groups with other OS temp files (.DS_Store) * docs(ai): Consolidate changelog entry for developer role fix * feat(ai/providers): Added baseUrl resolution support across OpenAI providers - Resolve baseUrl before buildParams calls in azure-openai-responses.ts for correct system role logic. - Add baseUrl resolution to openai-responses.ts and openai-completions.ts. - Add optional resolvedBaseUrl to getCompat function for better compatibility detection. - Update supportsDeveloperRole to accept both Model objects and baseUrl strings. --- .gitignore | 1 + packages/ai/CHANGELOG.md | 2 + .../src/providers/azure-openai-responses.ts | 12 ++- .../providers/openai-completions-compat.ts | 18 ++++- .../ai/src/providers/openai-completions.ts | 17 ++-- packages/ai/src/providers/openai-responses.ts | 18 ++++- .../openai-responses-developer-role.test.ts | 78 +++++++++++++++++++ 7 files changed, 131 insertions(+), 15 deletions(-) create mode 100644 packages/ai/test/openai-responses-developer-role.test.ts diff --git a/.gitignore b/.gitignore index b1193c8af..a4cb0245a 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ target/ *~ # OS +*.tmp .DS_Store # Logs & profiling diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index ddbd867c6..74d34e49e 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,8 @@ ## [Unreleased] +### Fixed +- Fixed `omp commit` failing with HTTP 400 errors when using reasoning-enabled models on OpenAI-compatible endpoints that don't support the `developer` role (e.g., GitHub Copilot, custom proxies). Now falls back to `system` role when `developer` is unsupported. ## [13.17.0] - 2026-03-30 ### Changed diff --git a/packages/ai/src/providers/azure-openai-responses.ts b/packages/ai/src/providers/azure-openai-responses.ts index 0459cbcfc..f2a11f880 100644 --- a/packages/ai/src/providers/azure-openai-responses.ts +++ b/packages/ai/src/providers/azure-openai-responses.ts @@ -22,6 +22,7 @@ import { AssistantMessageEventStream } from "../utils/event-stream"; import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector"; import { getOpenAIStreamIdleTimeoutMs, iterateWithIdleTimeout } from "../utils/idle-iterator"; import { mapToOpenAIResponsesToolChoice } from "../utils/tool-choice"; +import { supportsDeveloperRole } from "./openai-responses"; import { appendResponsesToolResultMessages, convertResponsesAssistantMessage, @@ -113,7 +114,8 @@ export const streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses" // Create Azure OpenAI client const apiKey = options?.apiKey || getEnvApiKey(model.provider) || ""; const client = createClient(model, apiKey, options); - const params = buildParams(model, context, options, deploymentName); + const { baseUrl } = resolveAzureConfig(model, options); + const params = buildParams(model, context, options, deploymentName, baseUrl); const requestAbortController = new AbortController(); const requestSignal = options?.signal ? AbortSignal.any([options.signal, requestAbortController.signal]) @@ -124,7 +126,7 @@ export const streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses" api: output.api, model: model.id, method: "POST", - url: `${resolveAzureConfig(model, options).baseUrl}/responses`, + url: `${baseUrl}/responses`, body: params, }; const openaiStream = await client.responses.create(params, { signal: requestSignal }); @@ -245,8 +247,9 @@ function buildParams( context: Context, options: AzureOpenAIResponsesOptions | undefined, deploymentName: string, + resolvedBaseUrl?: string, ) { - const messages = convertMessages(model, context, true); + const messages = convertMessages(model, context, true, resolvedBaseUrl); const params: AzureOpenAIResponsesSamplingParams = { model: deploymentName, @@ -323,13 +326,14 @@ function convertMessages( model: Model<"azure-openai-responses">, context: Context, strictResponsesPairing: boolean, + resolvedBaseUrl?: string, ): ResponseInput { const messages: ResponseInput = []; const transformedMessages = transformMessages(context.messages, model, normalizeResponsesToolCallIdForTransform); const knownCallIds = new Set(); if (context.systemPrompt) { - const role = model.reasoning ? "developer" : "system"; + const role = model.reasoning && supportsDeveloperRole(resolvedBaseUrl ?? model) ? "developer" : "system"; messages.push({ role, content: context.systemPrompt.toWellFormed(), diff --git a/packages/ai/src/providers/openai-completions-compat.ts b/packages/ai/src/providers/openai-completions-compat.ts index d6ff51bcb..f18adc84a 100644 --- a/packages/ai/src/providers/openai-completions-compat.ts +++ b/packages/ai/src/providers/openai-completions-compat.ts @@ -36,10 +36,14 @@ function detectStrictModeSupport(provider: string, baseUrl: string): boolean { /** * Detect compatibility settings from provider and baseUrl for known providers. * Provider takes precedence over URL-based detection since it's explicitly configured. + * @param model - The model configuration + * @param resolvedBaseUrl - Optional resolved base URL (e.g., after GitHub Copilot proxy-ep resolution). + * If provided, this takes precedence over model.baseUrl for URL-based checks. */ -export function detectOpenAICompat(model: Model<"openai-completions">): ResolvedOpenAICompat { +export function detectOpenAICompat(model: Model<"openai-completions">, resolvedBaseUrl?: string): ResolvedOpenAICompat { const provider = model.provider; - const baseUrl = model.baseUrl; + // Use resolvedBaseUrl if provided (e.g., after GitHub Copilot proxy-ep resolution) + const baseUrl = resolvedBaseUrl ?? model.baseUrl; const isCerebras = provider === "cerebras" || baseUrl.includes("cerebras.ai"); const isZai = provider === "zai" || baseUrl.includes("api.z.ai"); @@ -109,9 +113,15 @@ export function detectOpenAICompat(model: Model<"openai-completions">): Resolved /** * Resolve compatibility settings by layering explicit model.compat overrides onto * the detected defaults. This is the canonical compat view for both metadata and transport. + * @param model - The model configuration + * @param resolvedBaseUrl - Optional resolved base URL (e.g., after GitHub Copilot proxy-ep resolution). + * If provided, this takes precedence over model.baseUrl for URL-based checks. */ -export function resolveOpenAICompat(model: Model<"openai-completions">): ResolvedOpenAICompat { - const detected = detectOpenAICompat(model); +export function resolveOpenAICompat( + model: Model<"openai-completions">, + resolvedBaseUrl?: string, +): ResolvedOpenAICompat { + const detected = detectOpenAICompat(model, resolvedBaseUrl); if (!model.compat) { return detected; } diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index f26545224..ab28559d0 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -199,7 +199,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = ( options?.headers, options?.initiatorOverride, ); - const params = buildParams(model, context, options); + const params = buildParams(model, context, options, baseUrl); options?.onPayload?.(params); rawRequestDump = { provider: model.provider, @@ -554,8 +554,13 @@ async function createClient( }; } -function buildParams(model: Model<"openai-completions">, context: Context, options?: OpenAICompletionsOptions) { - const compat = getCompat(model); +function buildParams( + model: Model<"openai-completions">, + context: Context, + options: OpenAICompletionsOptions | undefined, + resolvedBaseUrl?: string, +) { + const compat = getCompat(model, resolvedBaseUrl); const messages = convertMessages(model, context, compat); maybeAddOpenRouterAnthropicCacheControl(model, messages); @@ -1118,7 +1123,9 @@ export function detectCompat(model: Model<"openai-completions">): ResolvedOpenAI /** * Get resolved compatibility settings for a model. * Uses explicit model.compat if provided, otherwise auto-detects from provider/URL. + * @param model - The model configuration + * @param resolvedBaseUrl - Optional resolved base URL (e.g., after GitHub Copilot proxy-ep resolution). */ -function getCompat(model: Model<"openai-completions">): ResolvedOpenAICompat { - return resolveOpenAICompat(model); +function getCompat(model: Model<"openai-completions">, resolvedBaseUrl?: string): ResolvedOpenAICompat { + return resolveOpenAICompat(model, resolvedBaseUrl); } diff --git a/packages/ai/src/providers/openai-responses.ts b/packages/ai/src/providers/openai-responses.ts index 1fd19a59e..2c76cc094 100644 --- a/packages/ai/src/providers/openai-responses.ts +++ b/packages/ai/src/providers/openai-responses.ts @@ -167,7 +167,7 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = ( options?.initiatorOverride, ); const providerSessionState = getOpenAIResponsesProviderSessionState(model, options?.providerSessionState); - const { params } = buildParams(model, context, options, providerSessionState); + const { params } = buildParams(model, context, options, providerSessionState, baseUrl); const requestAbortController = new AbortController(); const requestSignal = options?.signal ? AbortSignal.any([options.signal, requestAbortController.signal]) @@ -290,6 +290,7 @@ function buildParams( context: Context, options: OpenAIResponsesOptions | undefined, providerSessionState: OpenAIResponsesProviderSessionState | undefined, + resolvedBaseUrl?: string, ): { conversationMessages: ResponseInput; params: OpenAIResponsesSamplingParams } { const strictResponsesPairing = options?.strictResponsesPairing ?? @@ -303,7 +304,7 @@ function buildParams( const messages: ResponseInput = [...conversationMessages]; if (context.systemPrompt) { - const role = model.reasoning ? "developer" : "system"; + const role = model.reasoning && supportsDeveloperRole(resolvedBaseUrl ?? model) ? "developer" : "system"; messages.unshift({ role, content: context.systemPrompt.toWellFormed(), @@ -398,6 +399,19 @@ function supportsStrictMode(model: Model<"openai-responses">): boolean { ); } +export function supportsDeveloperRole(modelOrBaseUrl: Pick | string): boolean { + const baseUrl = + typeof modelOrBaseUrl === "string" ? modelOrBaseUrl.toLowerCase() : (modelOrBaseUrl.baseUrl ?? "").toLowerCase(); + return ( + baseUrl.includes("api.openai.com") || + baseUrl.includes(".openai.azure.com") || + baseUrl.includes("azure.com/openai") || + baseUrl.includes("models.inference.ai.azure.com") || + baseUrl.includes("githubcopilot.com") || + baseUrl.includes("copilot-api.") + ); +} + function convertConversationMessages( model: Model<"openai-responses">, context: Context, diff --git a/packages/ai/test/openai-responses-developer-role.test.ts b/packages/ai/test/openai-responses-developer-role.test.ts new file mode 100644 index 000000000..76f6f9f3f --- /dev/null +++ b/packages/ai/test/openai-responses-developer-role.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, it } from "bun:test"; +import { supportsDeveloperRole } from "../src/providers/openai-responses"; +import type { Model } from "../src/types"; + +describe("supportsDeveloperRole", () => { + it("returns true for openai provider with official API base URL", () => { + const model = { provider: "openai", baseUrl: "https://api.openai.com/v1" } as Model; + expect(supportsDeveloperRole(model)).toBe(true); + }); + + it("returns false for openai provider with custom proxy base URL", () => { + const model = { provider: "openai", baseUrl: "https://my-proxy.example.com/v1" } as Model; + expect(supportsDeveloperRole(model)).toBe(false); + }); + + it("returns true for github-copilot provider", () => { + const model = { provider: "github-copilot", baseUrl: "https://api.githubcopilot.com" } as Model; + expect(supportsDeveloperRole(model)).toBe(true); + }); + + it("returns false for github-copilot provider with custom proxy base URL", () => { + const model = { provider: "github-copilot", baseUrl: "https://proxy.example.com/v1" } as Model; + expect(supportsDeveloperRole(model)).toBe(false); + }); + + it("returns true for Azure OpenAI base URL", () => { + const model = { provider: "azure-openai", baseUrl: "https://my-resource.openai.azure.com/openai" } as Model; + expect(supportsDeveloperRole(model)).toBe(true); + }); + + it("returns true for Azure AI Inference base URL", () => { + const model = { + provider: "azure-openai", + baseUrl: "https://models.inference.ai.azure.com/v1/chat/completions", + } as Model; + expect(supportsDeveloperRole(model)).toBe(true); + }); + + it("returns true for api.openai.com base URL", () => { + const model = { provider: "custom", baseUrl: "https://api.openai.com/v1/chat/completions" } as Model; + expect(supportsDeveloperRole(model)).toBe(true); + }); + + it("returns false for generic third-party provider", () => { + const model = { provider: "custom", baseUrl: "https://api.example.com/v1" } as Model; + expect(supportsDeveloperRole(model)).toBe(false); + }); + + it("returns false for local/localhost endpoints", () => { + const model = { provider: "custom", baseUrl: "http://localhost:8080/v1" } as Model; + expect(supportsDeveloperRole(model)).toBe(false); + }); + + it("is case-insensitive for base URL matching", () => { + const model = { provider: "custom", baseUrl: "https://API.OPENAI.COM/v1" } as Model; + expect(supportsDeveloperRole(model)).toBe(true); + }); + + it("returns true for azure.com/openai base URL", () => { + const model = { provider: "custom", baseUrl: "https://azure.com/openai/deployments/my-model" } as Model; + expect(supportsDeveloperRole(model)).toBe(true); + }); + + it("returns true for github-copilot provider with api.individual.githubcopilot.com", () => { + const model = { provider: "github-copilot", baseUrl: "https://api.individual.githubcopilot.com" } as Model; + expect(supportsDeveloperRole(model)).toBe(true); + }); + + it("returns true for github-copilot provider with api.enterprise.githubcopilot.com", () => { + const model = { provider: "github-copilot", baseUrl: "https://api.enterprise.githubcopilot.com" } as Model; + expect(supportsDeveloperRole(model)).toBe(true); + }); + + it("returns true for github-copilot provider with copilot-api enterprise domain", () => { + const model = { provider: "github-copilot", baseUrl: "https://copilot-api.mycompany.com" } as Model; + expect(supportsDeveloperRole(model)).toBe(true); + }); +}); From ba54d3aabb8da4c1c4a81078e362adf4aa2987aa Mon Sep 17 00:00:00 2001 From: Matan Kushner Date: Tue, 31 Mar 2026 19:42:17 -0700 Subject: [PATCH 02/19] feat(ai): add vercel-ai-gateway to /login providers (#580) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(ai): add vercel-ai-gateway to /login providers * docs: add vercel-ai-gateway to README provider table and /login list * chore(ai): add changelog entry for vercel-ai-gateway login --------- Co-authored-by: Can Bölük --- README.md | 2 + packages/ai/CHANGELOG.md | 5 ++ packages/ai/src/auth-storage.ts | 6 +++ packages/ai/src/utils/oauth/index.ts | 8 +++ packages/ai/src/utils/oauth/types.ts | 1 + .../ai/src/utils/oauth/vercel-ai-gateway.ts | 51 +++++++++++++++++++ 6 files changed, 73 insertions(+) create mode 100644 packages/ai/src/utils/oauth/vercel-ai-gateway.ts diff --git a/README.md b/README.md index 83b23504f..3ceaabe29 100644 --- a/README.md +++ b/README.md @@ -507,6 +507,7 @@ return config | Qwen Portal (`qwen-portal`) | `QWEN_OAUTH_TOKEN` or `QWEN_PORTAL_API_KEY` | | vLLM (`vllm`) | `VLLM_API_KEY` | | Cloudflare AI Gateway (`cloudflare-ai-gateway`) | `CLOUDFLARE_AI_GATEWAY_API_KEY` | +| Vercel AI Gateway (`vercel-ai-gateway`) | `AI_GATEWAY_API_KEY` | | Qianfan (`qianfan`) | `QIANFAN_API_KEY` | See [Environment Variables](docs/environment-variables.md) for the full list. @@ -544,6 +545,7 @@ Use `/login` with supported providers: - MiniMax Coding Plan (International / China) - Qwen Portal (`qwen-portal`) - Cloudflare AI Gateway (`cloudflare-ai-gateway`) +- Vercel AI Gateway (`vercel-ai-gateway`) For `ollama`, API key is optional. Leave it unset for local no-auth instances, or set `OLLAMA_API_KEY` for authenticated hosts. For `llama.cpp`, API key is optional. Leave it unset for local no-auth instances, or set `LLAMA_CPP_API_KEY` for authenticated hosts. diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 74d34e49e..d9a5aa244 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,8 +2,13 @@ ## [Unreleased] +### Added + +- Added Vercel AI Gateway to `/login` providers for interactive API key setup + ### Fixed - Fixed `omp commit` failing with HTTP 400 errors when using reasoning-enabled models on OpenAI-compatible endpoints that don't support the `developer` role (e.g., GitHub Copilot, custom proxies). Now falls back to `system` role when `developer` is unsupported. + ## [13.17.0] - 2026-03-30 ### Changed diff --git a/packages/ai/src/auth-storage.ts b/packages/ai/src/auth-storage.ts index bc6c511c3..9fdb4473b 100644 --- a/packages/ai/src/auth-storage.ts +++ b/packages/ai/src/auth-storage.ts @@ -62,6 +62,7 @@ import { loginTavily } from "./utils/oauth/tavily"; import { loginTogether } from "./utils/oauth/together"; import type { OAuthController, OAuthCredentials, OAuthProvider, OAuthProviderId } from "./utils/oauth/types"; import { loginVenice } from "./utils/oauth/venice"; +import { loginVercelAiGateway } from "./utils/oauth/vercel-ai-gateway"; import { loginVllm } from "./utils/oauth/vllm"; import { loginXiaomi } from "./utils/oauth/xiaomi"; import { loginZai } from "./utils/oauth/zai"; @@ -903,6 +904,11 @@ export class AuthStorage { await saveApiKeyCredential(apiKey); return; } + case "vercel-ai-gateway": { + const apiKey = await loginVercelAiGateway(ctrl); + await saveApiKeyCredential(apiKey); + return; + } case "vllm": { const apiKey = await loginVllm(ctrl); await saveApiKeyCredential(apiKey); diff --git a/packages/ai/src/utils/oauth/index.ts b/packages/ai/src/utils/oauth/index.ts index 05b2268d6..5500ee3c5 100644 --- a/packages/ai/src/utils/oauth/index.ts +++ b/packages/ai/src/utils/oauth/index.ts @@ -113,6 +113,8 @@ export { loginTogether } from "./together"; export * from "./types"; // Venice (API key) export { loginVenice } from "./venice"; +// Vercel AI Gateway (API key) +export { loginVercelAiGateway } from "./vercel-ai-gateway"; // vLLM (API key) export { loginVllm } from "./vllm"; // Xiaomi MiMo (API key) @@ -303,6 +305,11 @@ const builtInOAuthProviders: OAuthProviderInfo[] = [ name: "Cloudflare AI Gateway", available: true, }, + { + id: "vercel-ai-gateway", + name: "Vercel AI Gateway", + available: true, + }, ]; const customOAuthProviders = new Map(); @@ -400,6 +407,7 @@ export async function refreshOAuthToken( case "moonshot": case "kagi": case "cloudflare-ai-gateway": + case "vercel-ai-gateway": case "qwen-portal": case "zenmux": case "vllm": diff --git a/packages/ai/src/utils/oauth/types.ts b/packages/ai/src/utils/oauth/types.ts index 898e6e17c..51175bcde 100644 --- a/packages/ai/src/utils/oauth/types.ts +++ b/packages/ai/src/utils/oauth/types.ts @@ -41,6 +41,7 @@ export type OAuthProvider = | "tavily" | "together" | "venice" + | "vercel-ai-gateway" | "vllm" | "xiaomi" | "zenmux" diff --git a/packages/ai/src/utils/oauth/vercel-ai-gateway.ts b/packages/ai/src/utils/oauth/vercel-ai-gateway.ts new file mode 100644 index 000000000..0ecfe6234 --- /dev/null +++ b/packages/ai/src/utils/oauth/vercel-ai-gateway.ts @@ -0,0 +1,51 @@ +/** + * Vercel AI Gateway login flow. + * + * Vercel AI Gateway proxies upstream model providers through a unified endpoint. + * + * This is not OAuth - it's a simple API key flow: + * 1. Open Vercel AI Gateway docs + * 2. User copies their API key + * 3. User pastes the API key into the CLI + */ + +import type { OAuthController } from "./types"; + +const AUTH_URL = + "https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai-gateway%2Fapi-keys&title=AI+Gateway+API+Keys"; + +/** + * Login to Vercel AI Gateway. + * + * Opens browser to Vercel AI Gateway docs and prompts for an API key. + * Returns the API key directly (not OAuthCredentials - this isn't OAuth). + */ +export async function loginVercelAiGateway( + options: OAuthController, +): Promise { + if (!options.onPrompt) { + throw new Error("Vercel AI Gateway login requires onPrompt callback"); + } + + options.onAuth?.({ + url: AUTH_URL, + instructions: + "Copy your Vercel AI Gateway API key from the Vercel dashboard", + }); + + const apiKey = await options.onPrompt({ + message: "Paste your Vercel AI Gateway API key", + placeholder: "vck_...", + }); + + if (options.signal?.aborted) { + throw new Error("Login cancelled"); + } + + const trimmed = apiKey.trim(); + if (!trimmed) { + throw new Error("API key is required"); + } + + return trimmed; +} From 4c6a62fd55a36836d9d3a633c98679c81bbad31b Mon Sep 17 00:00:00 2001 From: sundbp Date: Wed, 1 Apr 2026 03:47:42 +0100 Subject: [PATCH 03/19] feat: Add clojure support for ast_grep and ast_edit. (#579) --- Cargo.lock | 12 ++++++++++++ crates/pi-natives/Cargo.toml | 1 + crates/pi-natives/src/ast.rs | 6 ++++++ crates/pi-natives/src/language/mod.rs | 12 +++++++++--- crates/pi-natives/src/language/parsers.rs | 3 +++ 5 files changed, 31 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 644b8cf9c..b9fc6408c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2716,6 +2716,7 @@ dependencies = [ "tree-sitter-bash", "tree-sitter-c", "tree-sitter-c-sharp", + "tree-sitter-clojure", "tree-sitter-cpp", "tree-sitter-css", "tree-sitter-diff", @@ -3829,6 +3830,17 @@ dependencies = [ "tree-sitter-language", ] +[[package]] +name = "tree-sitter-clojure" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4004884cc509449a1d78fa3e1f02b4e953d0a8065984445304795e72e885338c" +dependencies = [ + "cc", + "tree-sitter", + "tree-sitter-language", +] + [[package]] name = "tree-sitter-cpp" version = "0.23.4" diff --git a/crates/pi-natives/Cargo.toml b/crates/pi-natives/Cargo.toml index 91011651f..ed4616191 100644 --- a/crates/pi-natives/Cargo.toml +++ b/crates/pi-natives/Cargo.toml @@ -40,6 +40,7 @@ ast-grep-core = { version = "0.39", default-features = false, features = [ tree-sitter = "0.25" tree-sitter-bash = "0.25" tree-sitter-c = "0.24" +tree-sitter-clojure = "0.1" tree-sitter-c-sharp = "0.23" tree-sitter-cpp = "0.23" tree-sitter-css = "0.25" diff --git a/crates/pi-natives/src/ast.rs b/crates/pi-natives/src/ast.rs index 75e194570..8ba6c36c1 100644 --- a/crates/pi-natives/src/ast.rs +++ b/crates/pi-natives/src/ast.rs @@ -162,6 +162,12 @@ static LANG_ALIASES: phf::Map<&'static str, SupportLang> = phf::phf_map! { "c#" => SupportLang::CSharp, "cs" => SupportLang::CSharp, "css" => SupportLang::Css, + "clj" => SupportLang::Clojure, + "cljc" => SupportLang::Clojure, + "cljs" => SupportLang::Clojure, + "clojure" => SupportLang::Clojure, + "clojurescript" => SupportLang::Clojure, + "edn" => SupportLang::Clojure, "diff" => SupportLang::Diff, "patch" => SupportLang::Diff, "elixir" => SupportLang::Elixir, diff --git a/crates/pi-natives/src/language/mod.rs b/crates/pi-natives/src/language/mod.rs index 51f180665..ac4b94c08 100644 --- a/crates/pi-natives/src/language/mod.rs +++ b/crates/pi-natives/src/language/mod.rs @@ -131,6 +131,7 @@ impl_lang_expando!(Zig, language_zig, 'µ'); // ── Stub languages ($ accepted in grammar) ────────────────────────────── impl_lang!(Bash, language_bash); +impl_lang!(Clojure, language_clojure); impl_lang!(Java, language_java); impl_lang!(JavaScript, language_javascript); impl_lang!(Json, language_json); @@ -252,6 +253,7 @@ pub enum SupportLang { C, Cpp, CSharp, + Clojure, Css, Diff, Elixir, @@ -292,9 +294,10 @@ impl SupportLang { pub const fn all_langs() -> &'static [Self] { use SupportLang::*; &[ - Bash, C, Cpp, CSharp, Css, Diff, Elixir, Go, Haskell, Hcl, Html, Java, JavaScript, Json, - Julia, Kotlin, Lua, Make, Markdown, Nix, ObjC, Odin, Php, Python, Regex, Ruby, Rust, - Scala, Solidity, Starlark, Swift, Toml, Tsx, TypeScript, Verilog, Xml, Yaml, Zig, + Bash, C, Cpp, CSharp, Clojure, Css, Diff, Elixir, Go, Haskell, Hcl, Html, Java, + JavaScript, Json, Julia, Kotlin, Lua, Make, Markdown, Nix, ObjC, Odin, Php, Python, Regex, + Ruby, Rust, Scala, Solidity, Starlark, Swift, Toml, Tsx, TypeScript, Verilog, Xml, Yaml, + Zig, ] } @@ -306,6 +309,7 @@ impl SupportLang { Self::C => "c", Self::Cpp => "cpp", Self::CSharp => "csharp", + Self::Clojure => "clojure", Self::Css => "css", Self::Diff => "diff", Self::Elixir => "elixir", @@ -360,6 +364,7 @@ macro_rules! execute_lang_method { S::C => C.$method($($pname,)*), S::Cpp => Cpp.$method($($pname,)*), S::CSharp => CSharp.$method($($pname,)*), + S::Clojure => Clojure.$method($($pname,)*), S::Css => Css.$method($($pname,)*), S::Diff => Diff.$method($($pname,)*), S::Elixir => Elixir.$method($($pname,)*), @@ -456,6 +461,7 @@ const fn extensions(lang: SupportLang) -> &'static [&'static str] { C => &["c", "h"], Cpp => &["cc", "hpp", "cpp", "c++", "hh", "cxx", "cu", "ino"], CSharp => &["cs"], + Clojure => &["clj", "cljs", "cljc", "edn"], Css => &["css", "scss"], Diff => &["diff", "patch"], Elixir => &["ex", "exs"], diff --git a/crates/pi-natives/src/language/parsers.rs b/crates/pi-natives/src/language/parsers.rs index 5c31b31ca..1c7901d4a 100644 --- a/crates/pi-natives/src/language/parsers.rs +++ b/crates/pi-natives/src/language/parsers.rs @@ -8,6 +8,9 @@ pub fn language_bash() -> TSLanguage { pub fn language_c() -> TSLanguage { tree_sitter_c::LANGUAGE.into() } +pub fn language_clojure() -> TSLanguage { + tree_sitter_clojure::LANGUAGE.into() +} pub fn language_cpp() -> TSLanguage { tree_sitter_cpp::LANGUAGE.into() } From c4f9216674e83a5320d82970ff65c5851658c453 Mon Sep 17 00:00:00 2001 From: David Dembeck Date: Tue, 31 Mar 2026 21:48:12 -0500 Subject: [PATCH 04/19] fix(git): preserve empty context lines in joinPatch to prevent corrupt patch errors (#583) --- packages/coding-agent/src/commit/git/index.ts | 7 ++-- packages/coding-agent/test/join-patch.test.ts | 40 +++++++++++++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 packages/coding-agent/test/join-patch.test.ts diff --git a/packages/coding-agent/src/commit/git/index.ts b/packages/coding-agent/src/commit/git/index.ts index 8dcfabbfe..3c634400e 100644 --- a/packages/coding-agent/src/commit/git/index.ts +++ b/packages/coding-agent/src/commit/git/index.ts @@ -189,12 +189,11 @@ function extractFileHeader(diff: string): string { return headerLines.join("\n"); } -function joinPatch(parts: string[]): string { - return parts +export function joinPatch(parts: string[]): string { + return `${parts .map(part => (part.endsWith("\n") ? part : `${part}\n`)) .join("\n") - .trimEnd() - .concat("\n"); + .replace(/\n+$/, "")}\n`; } function selectHunks(file: FileHunks, selector: HunkSelection["hunks"]): FileHunks["hunks"] { diff --git a/packages/coding-agent/test/join-patch.test.ts b/packages/coding-agent/test/join-patch.test.ts new file mode 100644 index 000000000..b3e790ece --- /dev/null +++ b/packages/coding-agent/test/join-patch.test.ts @@ -0,0 +1,40 @@ +import { joinPatch } from "@oh-my-pi/pi-coding-agent/commit/git"; + +describe("joinPatch", () => { + test("preserves space character in empty context line at end of patch", () => { + // This simulates a hunk ending with an empty context line represented as " \n" + const parts = [ + "@@ -1,2 +1,2 @@\n", + "foo\n", + "@@ -10,4 +10,4 @@\n", + "line1\n", + "-old\n", + "+new\n", + " \n", // Empty context line = space + newline + ]; + + const result = joinPatch(parts); + + // The result should end with a space character (the empty context line) + // but NOT start/end with multiple newlines + expect(result.endsWith(" \n")).toBe(true); + expect(result.replace(/[ \t]+$/, "")).toEqual(result); // No trailing spaces should be removed + }); + + test("normalizes multiple trailing newlines in parts", () => { + const parts = ["line1\n", "line2\n", "line3"]; + const result = joinPatch(parts); + + // Should join with single newlines and end with one newline + expect(result.endsWith("\n")).toBe(true); + }); + + test("adds newline to parts that are missing them", () => { + const parts = ["line1", "line2"]; + const result = joinPatch(parts); + + // Should add newlines to both parts + expect(result.includes("line1\n")).toBe(true); + expect(result.endsWith("line2\n")).toBe(true); + }); +}); From 74c4197855a5e3fc273c4959796891a409226e52 Mon Sep 17 00:00:00 2001 From: BayLee4 <63376748+BayLee4@users.noreply.github.com> Date: Wed, 1 Apr 2026 04:51:53 +0200 Subject: [PATCH 05/19] feat(coding-agent): priority to commit model for titles with fallback to smol (#573) Co-authored-by: le-cameleon <200889489+le-cameleon@users.noreply.github.com> --- README.md | 2 +- .../src/commit/model-selection.ts | 20 +----------------- .../coding-agent/src/config/model-resolver.ts | 21 +++++++++++++++++++ .../coding-agent/src/utils/title-generator.ts | 11 ++++------ 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 3ceaabe29..27091ce1e 100644 --- a/README.md +++ b/README.md @@ -313,7 +313,7 @@ Create images directly from the agent: Modern terminal interface with smart session management: -- **Auto session titles**: Sessions automatically titled based on first message using smol model +- **Auto session titles**: Sessions automatically titled based on first message using commit model, fallback to smol - **Welcome screen**: Logo, tips, recent sessions with selection - **Powerline footer**: Model, cwd, git branch/status, token usage, context % - **LSP status**: Shows which language servers are active and ready diff --git a/packages/coding-agent/src/commit/model-selection.ts b/packages/coding-agent/src/commit/model-selection.ts index 54e3d5cb0..2416256b2 100644 --- a/packages/coding-agent/src/commit/model-selection.ts +++ b/packages/coding-agent/src/commit/model-selection.ts @@ -1,7 +1,7 @@ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core"; import type { Api, Model } from "@oh-my-pi/pi-ai"; import { MODEL_ROLE_IDS } from "../config/model-registry"; -import { parseModelPattern, resolveModelRoleValue } from "../config/model-resolver"; +import { parseModelPattern, resolveModelRoleValue, resolveRoleSelection } from "../config/model-resolver"; import type { Settings } from "../config/settings"; import MODEL_PRIO from "../priority.json" with { type: "json" }; @@ -11,24 +11,6 @@ export interface ResolvedCommitModel { thinkingLevel?: ThinkingLevel; } -function resolveRoleSelection( - roles: readonly string[], - settings: Settings, - availableModels: Model[], -): { model: Model; thinkingLevel?: ThinkingLevel } | undefined { - const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() }; - for (const role of roles) { - const resolved = resolveModelRoleValue(settings.getModelRole(role), availableModels, { - settings, - matchPreferences, - }); - if (resolved.model) { - return { model: resolved.model, thinkingLevel: resolved.thinkingLevel }; - } - } - return undefined; -} - export async function resolvePrimaryModel( override: string | undefined, settings: Settings, diff --git a/packages/coding-agent/src/config/model-resolver.ts b/packages/coding-agent/src/config/model-resolver.ts index f4c1c3f7c..c9e7f2ff5 100644 --- a/packages/coding-agent/src/config/model-resolver.ts +++ b/packages/coding-agent/src/config/model-resolver.ts @@ -587,6 +587,27 @@ export function resolveModelOverride( return { explicitThinkingLevel: false }; } +/** + * Resolve a list of role patterns to the first matching model. + */ +export function resolveRoleSelection( + roles: readonly string[], + settings: Settings, + availableModels: Model[], +): { model: Model; thinkingLevel?: ThinkingLevel } | undefined { + const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() }; + for (const role of roles) { + const resolved = resolveModelRoleValue(settings.getModelRole(role), availableModels, { + settings, + matchPreferences, + }); + if (resolved.model) { + return { model: resolved.model, thinkingLevel: resolved.thinkingLevel }; + } + } + return undefined; +} + /** * Resolve model patterns to actual Model objects with optional thinking levels * Format: "pattern:level" where :level is optional diff --git a/packages/coding-agent/src/utils/title-generator.ts b/packages/coding-agent/src/utils/title-generator.ts index 5d7d42a61..4b986640e 100644 --- a/packages/coding-agent/src/utils/title-generator.ts +++ b/packages/coding-agent/src/utils/title-generator.ts @@ -7,7 +7,7 @@ import type { Api, Model } from "@oh-my-pi/pi-ai"; import { completeSimple } from "@oh-my-pi/pi-ai"; import { logger } from "@oh-my-pi/pi-utils"; import type { ModelRegistry } from "../config/model-registry"; -import { resolveModelRoleValue } from "../config/model-resolver"; +import { resolveRoleSelection } from "../config/model-resolver"; import { renderPromptTemplate } from "../config/prompt-templates"; import type { Settings } from "../config/settings"; import titleSystemPrompt from "../prompts/system/title-system.md" with { type: "text" }; @@ -29,12 +29,9 @@ function getTitleModel( if (availableModels.length === 0) return undefined; const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() }; - const configuredSmol = resolveModelRoleValue(settings.getModelRole("smol"), availableModels, { - settings, - matchPreferences, - }); - if (configuredSmol.model) { - return { model: configuredSmol.model, thinkingLevel: configuredSmol.thinkingLevel }; + const titleModel = resolveRoleSelection(["commit", "smol"], settings, availableModels); + if (titleModel) { + return { model: titleModel.model, thinkingLevel: titleModel.thinkingLevel }; } if (currentModel) { From 15f34723c1f45c8e9292a7b7a6eeac74a17e1b7d Mon Sep 17 00:00:00 2001 From: can1357 Date: Wed, 1 Apr 2026 03:17:08 +0200 Subject: [PATCH 06/19] feat(web): removed code_search tool and Exa API integration from web module - Removed code_search tool and all related Exa API integration from web search module. - Deleted code-search.ts implementation including CodeSearchResponse types and executeCodeSearch() function. - Updated getSearchTools() to return only webSearchCustomTool, removing dual-tool support. - Removed code_search tool documentation from README and prompt files. --- README.md | 1 - packages/coding-agent/CHANGELOG.md | 4 + .../src/prompts/tools/code-search.md | 45 ---- .../src/web/search/code-search.ts | 208 ------------------ packages/coding-agent/src/web/search/index.ts | 37 +--- packages/coding-agent/src/web/search/types.ts | 2 - .../tools/web-search-code-context.test.ts | 39 ---- 7 files changed, 5 insertions(+), 331 deletions(-) delete mode 100644 packages/coding-agent/src/prompts/tools/code-search.md delete mode 100644 packages/coding-agent/src/web/search/code-search.ts delete mode 100644 packages/coding-agent/test/tools/web-search-code-context.test.ts diff --git a/README.md b/README.md index 27091ce1e..8a325fdee 100644 --- a/README.md +++ b/README.md @@ -1241,7 +1241,6 @@ Use `--tools ` to restrict available built-in tools. | `fetch` | Fetch and extract URL content | | `web_search` | Multi-provider web search | | `deep_search` | AI-powered deep research with synthesized results (Exa) | -| `code_search` | Search code snippets and technical documentation (Exa) | | `write` | Create/overwrite files | | `generate_image` | Generate or edit images using Gemini image models | diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 0da850e3c..33174f0b6 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog ## [Unreleased] +### Removed + +- Removed `code_search` tool for code snippet and documentation search + ## [13.17.0] - 2026-03-30 diff --git a/packages/coding-agent/src/prompts/tools/code-search.md b/packages/coding-agent/src/prompts/tools/code-search.md deleted file mode 100644 index f412febe3..000000000 --- a/packages/coding-agent/src/prompts/tools/code-search.md +++ /dev/null @@ -1,45 +0,0 @@ -Search code snippets, and technical content. -This tool behaves more like grep than natural-language web search. - - -- Query with exact symbols, identifiers, error strings, CLI flags, filenames, import paths, and short code fragments -- Start with the smallest distinctive token; widen or add one nearby token only if the first query is too broad -- Prefer exact syntax when punctuation carries meaning, such as `Promise.withResolvers`, `useEffect(`, `--watch`, or `"direnv loading"` -- Keep `query` terse; remove filler words, prose, and request framing -- Use `code_context` only for a few disambiguating tokens such as language, library, framework, repo, runtime, or API name -- If a multi-word literal matters exactly, quote the shortest stable phrase first, then refine -- When looking for usage examples of a specific API, search the symbol first; add surrounding call syntax only when needed - - - -- query: Grep-style code search query; use exact tokens, short fragments, or short quoted phrases -- code_context: Optional disambiguation tokens only, not a sentence - - - -Good queries: -- `Promise.withResolvers` -- `DIRENV_LOG_FORMAT` -- `"direnv loading"` -- `useState` with `code_context: react hooks` -- `app.get(` with `code_context: express` -- `ERR_REQUIRE_ESM` with `code_context: node` - -Bad queries: -- `Need the official or source-backed way to silence direnv loading output` -- `How do I use Promise.withResolvers in Bun?` -- `find examples of React state hooks in TypeScript projects` -- `search GitHub for express routing docs` - - - -- Do not use this tool for broad conceptual research, comparisons, or authoritative sourcing; use `web_search`, `web_search_deep`, or `fetch` instead -- Do not put full-sentence instructions into `query` or `code_context` -- Do not pack many weak terms into one query; one strong token plus minimal context usually works better - - - -- `query` should be grep-style code search, not a natural-language request -- `code_context` is optional and should stay short -- If you need explanations, best practices, or comprehensive answers, use broader web search tools instead of this one - \ No newline at end of file diff --git a/packages/coding-agent/src/web/search/code-search.ts b/packages/coding-agent/src/web/search/code-search.ts deleted file mode 100644 index bea12cabe..000000000 --- a/packages/coding-agent/src/web/search/code-search.ts +++ /dev/null @@ -1,208 +0,0 @@ -import type { Component } from "@oh-my-pi/pi-tui"; -import { Text } from "@oh-my-pi/pi-tui"; -import { callExaTool, findApiKey as findExaKey, formatSearchResults, isSearchResponse } from "../../exa/mcp-client"; -import type { CustomToolResult, RenderResultOptions } from "../../extensibility/custom-tools/types"; -import type { Theme } from "../../modes/theme/theme"; -import { - formatCount, - formatExpandHint, - formatMoreItems, - formatStatusIcon, - replaceTabs, - truncateToWidth, -} from "../../tools/render-utils"; -import type { CodeSearchProviderId } from "./types"; - -export interface CodeSearchToolParams { - query: string; - code_context?: string; -} - -export interface CodeSearchSource { - title: string; - url: string; - repository: string; - path: string; - branch: string; - snippet?: string; - totalMatches?: string; -} - -export interface CodeSearchResponse { - provider: CodeSearchProviderId; - query: string; - totalResults?: number; - sources: CodeSearchSource[]; -} - -export interface CodeSearchRenderDetails { - response?: CodeSearchResponse; - error?: string; - provider: CodeSearchProviderId; -} - -function stringifyExaCodeResponse(payload: unknown): string { - if (typeof payload === "string") return payload; - if (typeof payload === "number" || typeof payload === "boolean") return String(payload); - if (payload === null || payload === undefined) return ""; - const serialized = JSON.stringify(payload, null, 2); - return typeof serialized === "string" ? serialized : ""; -} - -function normalizeExaCodeSearchResponse( - params: CodeSearchToolParams, - payload: unknown, - formattedSearchResponse?: string, -): CodeSearchResponse { - const snippet = formattedSearchResponse ?? stringifyExaCodeResponse(payload); - return { - provider: "exa", - query: params.query, - sources: [ - { - title: params.query, - url: "https://exa.ai/", - repository: "exa", - path: "code-search", - branch: "public-mcp", - snippet: snippet.length > 0 ? snippet : undefined, - }, - ], - }; -} - -async function searchCodeWithExa(params: CodeSearchToolParams): Promise { - const exaParams = params.code_context - ? { query: params.query, code_context: params.code_context } - : { query: params.query }; - const response = await callExaTool("get_code_context_exa", exaParams, findExaKey()); - if (isSearchResponse(response)) { - return normalizeExaCodeSearchResponse(params, response, formatSearchResults(response)); - } - - return normalizeExaCodeSearchResponse(params, response); -} - -export function formatCodeSearchForLlm(response: CodeSearchResponse): string { - const parts: string[] = []; - const summaryParts: string[] = [response.provider]; - if (response.totalResults !== undefined) { - summaryParts.push(`${response.totalResults.toLocaleString()} total matches`); - } - parts.push(`Code search via ${summaryParts.join(" · ")}`); - - if (response.sources.length === 0) { - parts.push("No results found."); - return parts.join("\n"); - } - - for (const [index, source] of response.sources.entries()) { - const metadata: string[] = [source.repository, source.path]; - if (source.totalMatches) metadata.push(`${source.totalMatches} matches`); - parts.push(`[${index + 1}] ${metadata.join(" · ")}`); - parts.push(` ${source.url}`); - if (source.snippet) { - for (const line of source.snippet.split("\n").slice(0, 8)) { - parts.push(` ${line}`); - } - } - } - - return parts.join("\n"); -} - -export async function executeCodeSearch( - params: CodeSearchToolParams, -): Promise> { - try { - const response = await searchCodeWithExa(params); - - return { - content: [{ type: "text", text: formatCodeSearchForLlm(response) }], - details: { provider: response.provider, response }, - }; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - return { - content: [{ type: "text", text: `Error: ${message}` }], - details: { provider: "exa", error: message }, - }; - } -} - -export function renderCodeSearchCall( - args: CodeSearchToolParams, - _options: RenderResultOptions, - theme: Theme, -): Component { - let text = `${theme.fg("toolTitle", "Code Search")} ${theme.fg("accent", truncateToWidth(args.query, 80))}`; - if (args.code_context) { - text += ` ${theme.fg("dim", truncateToWidth(args.code_context, 40))}`; - } - return new Text(text, 0, 0); -} - -export function renderCodeSearchResult( - result: { content: Array<{ type: string; text?: string }>; details?: CodeSearchRenderDetails }, - options: RenderResultOptions, - uiTheme: Theme, -): Component { - const details = result.details; - if (details?.error) { - return new Text( - `${formatStatusIcon("error", uiTheme)} ${uiTheme.fg("error", `Error: ${replaceTabs(details.error)}`)}`, - 0, - 0, - ); - } - - const response = details?.response; - if (!response) { - return new Text(`${formatStatusIcon("warning", uiTheme)} ${uiTheme.fg("muted", "No code search results")}`, 0, 0); - } - - const resultCount = response.sources.length; - const meta: string[] = [formatCount("result", resultCount), `provider:${response.provider}`]; - if (response.totalResults !== undefined) { - meta.push(`${response.totalResults.toLocaleString()} total`); - } - const expandHint = formatExpandHint(uiTheme, options.expanded, resultCount > 1); - let text = `${formatStatusIcon(resultCount > 0 ? "success" : "warning", uiTheme)} ${uiTheme.fg("dim", meta.join(uiTheme.sep.dot))}${expandHint}`; - - if (resultCount === 0) { - text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("muted", "No results")}`; - return new Text(text, 0, 0); - } - - const visibleSources = options.expanded ? response.sources : response.sources.slice(0, 1); - for (const [index, source] of visibleSources.entries()) { - const isLast = index === visibleSources.length - 1; - const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch; - const cont = isLast ? " " : uiTheme.tree.vertical; - text += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg("accent", truncateToWidth(replaceTabs(source.title), 100))}`; - text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg("mdLinkUrl", source.url)}`; - - if (source.totalMatches) { - text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg("muted", `Matches: ${source.totalMatches}`)}`; - } - - if (source.snippet) { - const snippetLines = source.snippet.split("\n").slice(0, options.expanded ? 6 : 3); - for (const line of snippetLines) { - text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg( - "toolOutput", - truncateToWidth(replaceTabs(line), 100), - )}`; - } - } - } - - if (!options.expanded && response.sources.length > visibleSources.length) { - text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg( - "muted", - formatMoreItems(response.sources.length - visibleSources.length, "result"), - )}`; - } - - return new Text(text, 0, 0); -} diff --git a/packages/coding-agent/src/web/search/index.ts b/packages/coding-agent/src/web/search/index.ts index 9e5e46a96..ab6de7493 100644 --- a/packages/coding-agent/src/web/search/index.ts +++ b/packages/coding-agent/src/web/search/index.ts @@ -4,7 +4,6 @@ * Single tool supporting Anthropic, Perplexity, Exa, Brave, Jina, Kimi, Gemini, Codex, Tavily, Kagi, Z.AI, and Synthetic * providers with provider-specific parameters exposed conditionally. * - * Code search is also supported via the code_search tool. */ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core"; import { StringEnum } from "@oh-my-pi/pi-ai"; @@ -13,17 +12,9 @@ import { renderPromptTemplate } from "../../config/prompt-templates"; import type { CustomTool, CustomToolContext, RenderResultOptions } from "../../extensibility/custom-tools/types"; import type { Theme } from "../../modes/theme/theme"; import webSearchSystemPrompt from "../../prompts/system/web-search.md" with { type: "text" }; -import codeSearchDescription from "../../prompts/tools/code-search.md" with { type: "text" }; import webSearchDescription from "../../prompts/tools/web-search.md" with { type: "text" }; import type { ToolSession } from "../../tools"; import { formatAge } from "../../tools/render-utils"; -import { - type CodeSearchRenderDetails, - type CodeSearchToolParams, - executeCodeSearch, - renderCodeSearchCall, - renderCodeSearchResult, -} from "./code-search"; import { getSearchProvider, resolveProviderChain, type SearchProvider } from "./provider"; import { renderSearchCall, renderSearchResult, type SearchRenderDetails } from "./render"; import type { SearchProviderId, SearchResponse } from "./types"; @@ -262,34 +253,8 @@ export const webSearchCustomTool: CustomTool = { - name: "code_search", - label: "Code Search", - description: renderPromptTemplate(codeSearchDescription), - parameters: codeSearchParameters, - - async execute(_toolCallId, params, _onUpdate, _ctx, _signal) { - return executeCodeSearch(params); - }, - - renderCall(args: CodeSearchToolParams, options: RenderResultOptions, theme: Theme) { - return renderCodeSearchCall(args, options, theme); - }, - - renderResult(result, options, theme) { - return renderCodeSearchResult(result, options, theme); - }, -}; - export function getSearchTools(): CustomTool[] { - return [webSearchCustomTool, codeSearchTool]; + return [webSearchCustomTool]; } export { getSearchProvider, setPreferredSearchProvider } from "./provider"; diff --git a/packages/coding-agent/src/web/search/types.ts b/packages/coding-agent/src/web/search/types.ts index 845ed5aeb..0a5ce00be 100644 --- a/packages/coding-agent/src/web/search/types.ts +++ b/packages/coding-agent/src/web/search/types.ts @@ -20,8 +20,6 @@ export type SearchProviderId = | "kagi" | "synthetic"; -export type CodeSearchProviderId = "exa"; - export function isSearchProviderId(value: string): value is SearchProviderId { return [ "exa", diff --git a/packages/coding-agent/test/tools/web-search-code-context.test.ts b/packages/coding-agent/test/tools/web-search-code-context.test.ts deleted file mode 100644 index ac5c65481..000000000 --- a/packages/coding-agent/test/tools/web-search-code-context.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { describe, expect, it } from "bun:test"; -import { hookFetch } from "@oh-my-pi/pi-utils"; -import { executeCodeSearch } from "../../src/web/search/code-search"; - -function getFirstTextContent(result: { content: Array<{ type: string; text?: string }> }): string { - const firstContent = result.content[0]; - if (firstContent?.type === "text") return firstContent.text ?? ""; - return ""; -} - -describe("code_search", () => { - it("normalizes Exa response and surfaces it as code search result", async () => { - using _hook = hookFetch( - () => - new Response( - JSON.stringify({ - result: { - content: [ - { - type: "text", - text: "Need the official or source-backed way to silence direnv loading output.", - }, - ], - }, - }), - { status: 200, headers: { "Content-Type": "application/json" } }, - ), - ); - - const result = await executeCodeSearch({ query: "DIRENV_LOG_FORMAT direnv loading silence" }); - const output = getFirstTextContent(result); - expect(output).toContain("Code search via exa"); - expect(output).toContain("Need the official or source-backed way to silence direnv loading output."); - expect(result.details?.provider).toBe("exa"); - expect(result.details?.response?.sources[0]?.snippet).toContain( - "Need the official or source-backed way to silence direnv loading output.", - ); - }); -}); From cbb9f2136352efd5bdc97e46a9059a1d59997447 Mon Sep 17 00:00:00 2001 From: can1357 Date: Wed, 1 Apr 2026 03:17:17 +0200 Subject: [PATCH 07/19] chore(tools): added napi-rs to auto-generated file detection patterns - Added napi-rs to known code generator patterns for auto-generated file detection. --- packages/coding-agent/src/tools/auto-generated-guard.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/coding-agent/src/tools/auto-generated-guard.ts b/packages/coding-agent/src/tools/auto-generated-guard.ts index baba3ffcc..648415ef3 100644 --- a/packages/coding-agent/src/tools/auto-generated-guard.ts +++ b/packages/coding-agent/src/tools/auto-generated-guard.ts @@ -16,7 +16,7 @@ const CHECK_BYTE_COUNT = 1024; const HEADER_LINE_LIMIT = 40; const KNOWN_GENERATOR_PATTERN = - "(?:protoc(?:-gen-[\\w-]+)?|sqlc|buf|swagger(?:-codegen)?|openapi(?:-generator)?|grpc-gateway|mockery|stringer|easyjson|deepcopy-gen|defaulter-gen|conversion-gen|client-gen|lister-gen|informer-gen|kysely-codegen)"; + "(?:protoc(?:-gen-[\\w-]+)?|sqlc|buf|swagger(?:-codegen)?|openapi(?:-generator)?|grpc-gateway|mockery|stringer|easyjson|deepcopy-gen|defaulter-gen|conversion-gen|client-gen|lister-gen|informer-gen|kysely-codegen|napi-rs)"; /** * Strong marker patterns for generated-file headers. From 3b68346123f1fd8fd555cf239afffbaa8e48b860 Mon Sep 17 00:00:00 2001 From: can1357 Date: Wed, 1 Apr 2026 04:39:16 +0200 Subject: [PATCH 08/19] fix(render-utils): used diff-specific colors for diffstat add/remove - Replaced success/error theme colors with toolDiffAdded/toolDiffRemoved for diff stats display. Fixes #589 --- packages/coding-agent/CHANGELOG.md | 6 +++++- packages/coding-agent/src/tools/render-utils.ts | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 33174f0b6..1e25a1519 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -5,6 +5,10 @@ - Removed `code_search` tool for code snippet and documentation search +### Fixed + +- Fixed edit tool diffstats to use diff-specific add/remove theme colors instead of success/error status colors ([#589](https://github.com/can1357/oh-my-pi/issues/589)) + ## [13.17.0] - 2026-03-30 @@ -6455,4 +6459,4 @@ Initial public release. - Git branch display in footer - Message queueing during streaming responses - OAuth integration for Gmail and Google Calendar access -- HTML export with syntax highlighting and collapsible sections \ No newline at end of file +- HTML export with syntax highlighting and collapsible sections diff --git a/packages/coding-agent/src/tools/render-utils.ts b/packages/coding-agent/src/tools/render-utils.ts index a08e4fe08..b8ad5cd87 100644 --- a/packages/coding-agent/src/tools/render-utils.ts +++ b/packages/coding-agent/src/tools/render-utils.ts @@ -382,8 +382,8 @@ export function getDiffStats(diffText: string): DiffStats { export function formatDiffStats(added: number, removed: number, hunks: number, theme: Theme): string { const parts: string[] = []; - if (added > 0) parts.push(theme.fg("success", `+${added}`)); - if (removed > 0) parts.push(theme.fg("error", `-${removed}`)); + if (added > 0) parts.push(theme.fg("toolDiffAdded", `+${added}`)); + if (removed > 0) parts.push(theme.fg("toolDiffRemoved", `-${removed}`)); if (hunks > 0) parts.push(theme.fg("dim", `${hunks} hunk${hunks !== 1 ? "s" : ""}`)); return parts.join(theme.fg("dim", " / ")); } From bcee42ae3b6ad784ad4db00316b0b77603b13d80 Mon Sep 17 00:00:00 2001 From: can1357 Date: Wed, 1 Apr 2026 04:40:03 +0200 Subject: [PATCH 09/19] feat(ai): implemented first-event timeout detection for streaming responses - Added first-event timeout detection for streaming responses to prevent stuck requests and enable faster recovery from stream stalls. - Added PI_STREAM_FIRST_EVENT_TIMEOUT_MS environment variable to configure first-event timeout behavior independently from idle timeouts. - Fixed Anthropic stream timeout errors to be properly retried with first-event recognition, distinguishing first-event from idle timeouts. - Enhanced idle-iterator utility with FirstEventWatchdog mechanism and markFirstStreamEvent wrapper for unified timeout handling across OpenAI, Azure OpenAI, and Anthropic providers. --- packages/ai/CHANGELOG.md | 9 + packages/ai/src/providers/anthropic.ts | 18 +- .../src/providers/azure-openai-responses.ts | 20 ++- .../ai/src/providers/openai-completions.ts | 162 +++++++++--------- packages/ai/src/providers/openai-responses.ts | 20 ++- packages/ai/src/utils/idle-iterator.ts | 102 ++++++++++- packages/ai/test/anthropic-retry.test.ts | 4 + .../ai/test/anthropic-stream-timeout.test.ts | 115 +++++++++++++ 8 files changed, 346 insertions(+), 104 deletions(-) create mode 100644 packages/ai/test/anthropic-stream-timeout.test.ts diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index d9a5aa244..112f51b28 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,6 +1,15 @@ # Changelog ## [Unreleased] +### Added + +- Added first-event timeout detection for streaming responses to abort stuck requests before user-visible content arrives +- Added `PI_STREAM_FIRST_EVENT_TIMEOUT_MS` environment variable to configure first-event timeout (defaults to 15 seconds or idle timeout, whichever is lower) + +### Fixed + +- Fixed Anthropic stream timeout errors to be properly retried by recognizing first-event timeout messages +- Fixed stream stall detection to distinguish between first-event timeouts and idle timeouts, enabling faster recovery for stuck connections ### Added diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index 981d2dc42..f525776a0 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -33,6 +33,7 @@ import type { import { isAnthropicOAuthToken, normalizeToolCallId, resolveCacheRetention } from "../utils"; import { AssistantMessageEventStream } from "../utils/event-stream"; import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector"; +import { getStreamFirstEventTimeoutMs, iterateWithIdleTimeout } from "../utils/idle-iterator"; import { parseStreamingJson } from "../utils/json-parse"; import { buildCopilotDynamicHeaders, @@ -572,9 +573,9 @@ function isTransientStreamParseError(error: unknown): boolean { export function isProviderRetryableError(error: unknown): boolean { if (!(error instanceof Error)) return false; - const msg = error.message; + const msg = error.message.toLowerCase(); return ( - /rate.?limit|too many requests|overloaded|service.?unavailable|internal_error|stream error.*received from peer|1302/i.test( + /rate.?limit|too many requests|overloaded|service.?unavailable|internal_error|stream error.*received from peer|1302|timed?\s*out while waiting for the first event|timeout waiting for first/i.test( msg, ) || isTransientStreamParseError(error) ); @@ -674,13 +675,22 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = ( let providerRetryAttempt = 0; let started = false; do { - const anthropicStream = client.messages.stream({ ...params, stream: true }, { signal: options?.signal }); + const requestAbortController = new AbortController(); + const requestSignal = options?.signal + ? AbortSignal.any([options.signal, requestAbortController.signal]) + : requestAbortController.signal; + const anthropicStream = client.messages.stream({ ...params, stream: true }, { signal: requestSignal }); if (copilotDynamicHeaders && output.usage.premiumRequests === undefined) { output.usage.premiumRequests = copilotDynamicHeaders.premiumRequests; } try { - for await (const event of anthropicStream) { + for await (const event of iterateWithIdleTimeout(anthropicStream, { + firstItemTimeoutMs: getStreamFirstEventTimeoutMs(), + errorMessage: "Anthropic stream stalled while waiting for the next event", + firstItemErrorMessage: "Anthropic stream timed out while waiting for the first event", + onFirstItemTimeout: () => requestAbortController.abort(), + })) { started = true; if (event.type === "message_start") { output.responseId = event.message.id; diff --git a/packages/ai/src/providers/azure-openai-responses.ts b/packages/ai/src/providers/azure-openai-responses.ts index f2a11f880..f836fcfb8 100644 --- a/packages/ai/src/providers/azure-openai-responses.ts +++ b/packages/ai/src/providers/azure-openai-responses.ts @@ -20,7 +20,7 @@ import { } from "../types"; import { AssistantMessageEventStream } from "../utils/event-stream"; import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector"; -import { getOpenAIStreamIdleTimeoutMs, iterateWithIdleTimeout } from "../utils/idle-iterator"; +import { createFirstEventWatchdog, getOpenAIStreamIdleTimeoutMs, getStreamFirstEventTimeoutMs, iterateWithIdleTimeout, markFirstStreamEvent } from "../utils/idle-iterator"; import { mapToOpenAIResponsesToolChoice } from "../utils/tool-choice"; import { supportsDeveloperRole } from "./openai-responses"; import { @@ -120,6 +120,11 @@ export const streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses" const requestSignal = options?.signal ? AbortSignal.any([options.signal, requestAbortController.signal]) : requestAbortController.signal; + const idleTimeoutMs = getOpenAIStreamIdleTimeoutMs(); + const firstEventWatchdog = createFirstEventWatchdog( + getStreamFirstEventTimeoutMs(idleTimeoutMs), + () => requestAbortController.abort(), + ); options?.onPayload?.(params); rawRequestDump = { provider: model.provider, @@ -133,11 +138,14 @@ export const streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses" stream.push({ type: "start", partial: output }); await processResponsesStream( - iterateWithIdleTimeout(openaiStream, { - idleTimeoutMs: getOpenAIStreamIdleTimeoutMs(), - errorMessage: "Azure OpenAI responses stream stalled while waiting for the next event", - onIdle: () => requestAbortController.abort(), - }), + iterateWithIdleTimeout( + markFirstStreamEvent(openaiStream, firstEventWatchdog), + { + idleTimeoutMs, + errorMessage: "Azure OpenAI responses stream stalled while waiting for the next event", + onIdle: () => requestAbortController.abort(), + }, + ), output, stream, model, diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index ab28559d0..c4af9b37a 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -31,7 +31,7 @@ import { } from "../types"; import { AssistantMessageEventStream } from "../utils/event-stream"; import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector"; -import { getOpenAIStreamIdleTimeoutMs, iterateWithIdleTimeout } from "../utils/idle-iterator"; +import { createFirstEventWatchdog, getOpenAIStreamIdleTimeoutMs, getStreamFirstEventTimeoutMs, iterateWithIdleTimeout, markFirstStreamEvent } from "../utils/idle-iterator"; import { parseStreamingJson } from "../utils/json-parse"; import { getKimiCommonHeaders } from "../utils/oauth/kimi"; import { adaptSchemaForStrict, NO_STRICT } from "../utils/schema"; @@ -192,6 +192,11 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = ( const requestSignal = options?.signal ? AbortSignal.any([options.signal, requestAbortController.signal]) : requestAbortController.signal; + const idleTimeoutMs = getOpenAIStreamIdleTimeoutMs(); + const firstEventWatchdog = createFirstEventWatchdog( + getStreamFirstEventTimeoutMs(idleTimeoutMs), + () => requestAbortController.abort(), + ); const { client, copilotPremiumRequests, baseUrl } = await createClient( model, context, @@ -206,96 +211,82 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = ( api: output.api, model: model.id, method: "POST", - url: `${baseUrl ?? "https://api.openai.com/v1"}/chat/completions`, + url: `${baseUrl}/chat/completions`, body: params, }; const openaiStream = await client.chat.completions.create(params, { signal: requestSignal }); if (copilotPremiumRequests !== undefined) output.usage.premiumRequests = copilotPremiumRequests; stream.push({ type: "start", partial: output }); - let currentBlock: TextContent | ThinkingContent | (ToolCall & { partialArgs?: string }) | null = null; - const blocks = output.content; - const blockIndex = () => blocks.length - 1; - const finishCurrentBlock = (block?: typeof currentBlock) => { - if (block) { - if (block.type === "text") { - stream.push({ - type: "text_end", - contentIndex: blockIndex(), - content: block.text, - partial: output, - }); - } else if (block.type === "thinking") { - stream.push({ - type: "thinking_end", - contentIndex: blockIndex(), - content: block.thinking, - partial: output, - }); - } else if (block.type === "toolCall") { - block.arguments = parseStreamingJson(block.partialArgs); - delete block.partialArgs; - stream.push({ - type: "toolcall_end", - contentIndex: blockIndex(), - toolCall: block, - partial: output, - }); - } - } + const parseMiniMaxThinkTags = model.provider === "minimax-code"; + type OpenAIStreamBlock = TextContent | ThinkingContent | (ToolCall & { partialArgs: string }); + let currentBlock: OpenAIStreamBlock | undefined; + const blockIndex = (block: OpenAIStreamBlock | undefined): number => { + if (!block) return Math.max(0, output.content.length - 1); + return output.content.findIndex(contentBlock => contentBlock === block); }; - - const parseMiniMaxThinkTags = model.provider === "minimax-code" || model.provider === "minimax-code-cn"; - let taggedTextBuffer = ""; - let insideTaggedThinking = false; - - const appendTextDelta = (delta: string) => { - if (delta.length === 0) return; + const finishCurrentBlock = (block: OpenAIStreamBlock | undefined): void => { + if (!block) return; + const contentIndex = blockIndex(block); + if (contentIndex < 0) return; + if (block.type === "text") { + stream.push({ type: "text_end", contentIndex, content: block.text, partial: output }); + return; + } + if (block.type === "thinking") { + stream.push({ type: "thinking_end", contentIndex, content: block.thinking, partial: output }); + return; + } + block.arguments = parseStreamingJson(block.partialArgs); + delete (block as { partialArgs?: string }).partialArgs; + stream.push({ type: "toolcall_end", contentIndex, toolCall: block, partial: output }); + }; + const appendText = (message: AssistantMessage, eventStream: AssistantMessageEventStream, text: string): void => { if (!currentBlock || currentBlock.type !== "text") { finishCurrentBlock(currentBlock); currentBlock = { type: "text", text: "" }; - output.content.push(currentBlock); - stream.push({ type: "text_start", contentIndex: blockIndex(), partial: output }); + message.content.push(currentBlock); + eventStream.push({ type: "text_start", contentIndex: blockIndex(currentBlock), partial: message }); } - if (currentBlock.type === "text") { - currentBlock.text += delta; - stream.push({ - type: "text_delta", - contentIndex: blockIndex(), - delta, - partial: output, + currentBlock.text += text; + eventStream.push({ + type: "text_delta", + contentIndex: blockIndex(currentBlock), + delta: text, + partial: message, + }); + }; + const appendThinking = (message: AssistantMessage, eventStream: AssistantMessageEventStream, thinking: string): void => { + if (!currentBlock || currentBlock.type !== "thinking") { + finishCurrentBlock(currentBlock); + currentBlock = { type: "thinking", thinking: "" }; + message.content.push(currentBlock); + eventStream.push({ + type: "thinking_start", + contentIndex: blockIndex(currentBlock), + partial: message, }); } + currentBlock.thinking += thinking; + eventStream.push({ + type: "thinking_delta", + contentIndex: blockIndex(currentBlock), + delta: thinking, + partial: message, + }); }; - const appendThinkingDelta = (delta: string, signature?: string) => { - if (delta.length === 0) return; - if ( - !currentBlock || - currentBlock.type !== "thinking" || - (signature !== undefined && currentBlock.thinkingSignature !== signature) - ) { - finishCurrentBlock(currentBlock); - currentBlock = { - type: "thinking", - thinking: "", - thinkingSignature: signature, - }; - output.content.push(currentBlock); - stream.push({ type: "thinking_start", contentIndex: blockIndex(), partial: output }); - } - if (currentBlock.type === "thinking") { - if (signature !== undefined && !currentBlock.thinkingSignature) { - currentBlock.thinkingSignature = signature; - } - currentBlock.thinking += delta; - stream.push({ - type: "thinking_delta", - contentIndex: blockIndex(), - delta, - partial: output, - }); - } + let taggedTextBuffer = ""; + let insideTaggedThinking = false; + const appendTextDelta = (text: string) => { + if (!text) return; + if (!firstTokenTime) firstTokenTime = Date.now(); + appendText(output, stream, text); + }; + const appendThinkingDelta = (thinking: string) => { + if (!thinking) return; + if (!firstTokenTime) firstTokenTime = Date.now(); + appendThinking(output, stream, thinking); }; const flushTaggedTextBuffer = () => { @@ -332,11 +323,14 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = ( } }; - for await (const chunk of iterateWithIdleTimeout(openaiStream, { - idleTimeoutMs: getOpenAIStreamIdleTimeoutMs(), - errorMessage: "OpenAI completions stream stalled while waiting for the next event", - onIdle: () => requestAbortController.abort(), - })) { + for await (const chunk of iterateWithIdleTimeout( + markFirstStreamEvent(openaiStream, firstEventWatchdog), + { + idleTimeoutMs, + errorMessage: "OpenAI completions stream stalled while waiting for the next event", + onIdle: () => requestAbortController.abort(), + }, + )) { if (!chunk || typeof chunk !== "object") continue; // OpenAI documents ChatCompletionChunk.id as the unique chat completion identifier, @@ -401,7 +395,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = ( if (foundReasoningField) { const delta = (choice.delta as any)[foundReasoningField]; - appendThinkingDelta(delta, foundReasoningField); + appendThinkingDelta(delta); } if (choice?.delta?.tool_calls) { @@ -420,7 +414,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = ( partialArgs: "", }; output.content.push(currentBlock); - stream.push({ type: "toolcall_start", contentIndex: blockIndex(), partial: output }); + stream.push({ type: "toolcall_start", contentIndex: blockIndex(currentBlock), partial: output }); } if (currentBlock.type === "toolCall") { @@ -434,7 +428,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = ( } stream.push({ type: "toolcall_delta", - contentIndex: blockIndex(), + contentIndex: blockIndex(currentBlock), delta, partial: output, }); diff --git a/packages/ai/src/providers/openai-responses.ts b/packages/ai/src/providers/openai-responses.ts index 2c76cc094..480e2a951 100644 --- a/packages/ai/src/providers/openai-responses.ts +++ b/packages/ai/src/providers/openai-responses.ts @@ -30,7 +30,7 @@ import { } from "../utils"; import { AssistantMessageEventStream } from "../utils/event-stream"; import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector"; -import { getOpenAIStreamIdleTimeoutMs, iterateWithIdleTimeout } from "../utils/idle-iterator"; +import { createFirstEventWatchdog, getOpenAIStreamIdleTimeoutMs, getStreamFirstEventTimeoutMs, iterateWithIdleTimeout, markFirstStreamEvent } from "../utils/idle-iterator"; import { adaptSchemaForStrict, NO_STRICT } from "../utils/schema"; import { mapToOpenAIResponsesToolChoice } from "../utils/tool-choice"; import { @@ -172,6 +172,11 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = ( const requestSignal = options?.signal ? AbortSignal.any([options.signal, requestAbortController.signal]) : requestAbortController.signal; + const idleTimeoutMs = getOpenAIStreamIdleTimeoutMs(); + const firstEventWatchdog = createFirstEventWatchdog( + getStreamFirstEventTimeoutMs(idleTimeoutMs), + () => requestAbortController.abort(), + ); options?.onPayload?.(params); rawRequestDump = { provider: model.provider, @@ -187,11 +192,14 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = ( const nativeOutputItems: Array> = []; await processResponsesStream( - iterateWithIdleTimeout(openaiStream, { - idleTimeoutMs: getOpenAIStreamIdleTimeoutMs(), - errorMessage: "OpenAI responses stream stalled while waiting for the next event", - onIdle: () => requestAbortController.abort(), - }), + iterateWithIdleTimeout( + markFirstStreamEvent(openaiStream, firstEventWatchdog), + { + idleTimeoutMs, + errorMessage: "OpenAI responses stream stalled while waiting for the next event", + onIdle: () => requestAbortController.abort(), + }, + ), output, stream, model, diff --git a/packages/ai/src/utils/idle-iterator.ts b/packages/ai/src/utils/idle-iterator.ts index a48e90262..c86bc8394 100644 --- a/packages/ai/src/utils/idle-iterator.ts +++ b/packages/ai/src/utils/idle-iterator.ts @@ -1,6 +1,7 @@ import { $env } from "@oh-my-pi/pi-utils"; const DEFAULT_OPENAI_STREAM_IDLE_TIMEOUT_MS = 45_000; +const DEFAULT_STREAM_FIRST_EVENT_TIMEOUT_MS = 15_000; function normalizeIdleTimeoutMs(value: string | undefined, fallback: number): number | undefined { if (value === undefined) return fallback; @@ -19,20 +20,92 @@ export function getOpenAIStreamIdleTimeoutMs(): number | undefined { return normalizeIdleTimeoutMs($env.PI_OPENAI_STREAM_IDLE_TIMEOUT_MS, DEFAULT_OPENAI_STREAM_IDLE_TIMEOUT_MS); } +/** + * Returns the timeout used while waiting for the first stream event. + * + * Set `PI_STREAM_FIRST_EVENT_TIMEOUT_MS=0` to disable the watchdog. + */ +export function getStreamFirstEventTimeoutMs(idleTimeoutMs?: number): number | undefined { + const fallback = Math.min(DEFAULT_STREAM_FIRST_EVENT_TIMEOUT_MS, idleTimeoutMs ?? DEFAULT_STREAM_FIRST_EVENT_TIMEOUT_MS); + return normalizeIdleTimeoutMs($env.PI_STREAM_FIRST_EVENT_TIMEOUT_MS, fallback); +} + +export interface FirstEventWatchdog { + markFirstEventReceived(): void; + cleanup(): void; +} + +/** + * Starts a watchdog that aborts a request if no first stream event arrives in time. + * Call `markFirstEventReceived()` as soon as the first event is observed. + */ +export function createFirstEventWatchdog( + timeoutMs: number | undefined, + onTimeout: () => void, +): FirstEventWatchdog { + let timer: NodeJS.Timeout | undefined; + if (timeoutMs !== undefined && timeoutMs > 0) { + timer = setTimeout(() => { + timer = undefined; + onTimeout(); + }, timeoutMs); + } + return { + markFirstEventReceived() { + if (!timer) return; + clearTimeout(timer); + timer = undefined; + }, + cleanup() { + if (!timer) return; + clearTimeout(timer); + timer = undefined; + }, + }; +} + +/** + * Wraps an async iterable and clears the watchdog once the first event arrives. + */ +export async function* markFirstStreamEvent( + iterable: AsyncIterable, + watchdog: FirstEventWatchdog, +): AsyncGenerator { + let sawFirstEvent = false; + try { + for await (const item of iterable) { + if (!sawFirstEvent) { + sawFirstEvent = true; + watchdog.markFirstEventReceived(); + } + yield item; + } + } finally { + watchdog.cleanup(); + } +} + export interface IdleTimeoutIteratorOptions { idleTimeoutMs?: number; + firstItemTimeoutMs?: number; errorMessage: string; + firstItemErrorMessage?: string; onIdle?: () => void; + onFirstItemTimeout?: () => void; } /** * Yields items from an async iterable while enforcing a maximum idle gap between items. + * + * The first item may use a shorter timeout so stuck requests can be aborted and retried + * before any user-visible content has streamed. */ export async function* iterateWithIdleTimeout( iterable: AsyncIterable, options: IdleTimeoutIteratorOptions, ): AsyncGenerator { - if (options.idleTimeoutMs === undefined || options.idleTimeoutMs <= 0) { + const firstItemTimeoutMs = options.firstItemTimeoutMs ?? options.idleTimeoutMs; + if ((firstItemTimeoutMs === undefined || firstItemTimeoutMs <= 0) && (options.idleTimeoutMs === undefined || options.idleTimeoutMs <= 0)) { for await (const item of iterable) { yield item; } @@ -40,26 +113,46 @@ export async function* iterateWithIdleTimeout( } const iterator = iterable[Symbol.asyncIterator](); + let sawFirstItem = false; while (true) { const nextResultPromise = iterator.next().then( result => ({ kind: "next" as const, result }), error => ({ kind: "error" as const, error }), ); + const activeTimeoutMs = sawFirstItem ? options.idleTimeoutMs : firstItemTimeoutMs; + + if (activeTimeoutMs === undefined || activeTimeoutMs <= 0) { + const outcome = await nextResultPromise; + if (outcome.kind === "error") { + throw outcome.error; + } + if (outcome.result.done) { + return; + } + sawFirstItem = true; + yield outcome.result.value; + continue; + } + const { promise: timeoutPromise, resolve: resolveTimeout } = Promise.withResolvers<{ kind: "timeout"; }>(); - const timer = setTimeout(() => resolveTimeout({ kind: "timeout" }), options.idleTimeoutMs); + const timer = setTimeout(() => resolveTimeout({ kind: "timeout" }), activeTimeoutMs); try { const outcome = await Promise.race([nextResultPromise, timeoutPromise]); if (outcome.kind === "timeout") { - options.onIdle?.(); + if (sawFirstItem) { + options.onIdle?.(); + } else { + options.onFirstItemTimeout?.(); + } const returnPromise = iterator.return?.(); if (returnPromise) { void returnPromise.catch(() => {}); } - throw new Error(options.errorMessage); + throw new Error(sawFirstItem ? options.errorMessage : (options.firstItemErrorMessage ?? options.errorMessage)); } if (outcome.kind === "error") { throw outcome.error; @@ -67,6 +160,7 @@ export async function* iterateWithIdleTimeout( if (outcome.result.done) { return; } + sawFirstItem = true; yield outcome.result.value; } finally { clearTimeout(timer); diff --git a/packages/ai/test/anthropic-retry.test.ts b/packages/ai/test/anthropic-retry.test.ts index 98d45e4e4..7a7482b8a 100644 --- a/packages/ai/test/anthropic-retry.test.ts +++ b/packages/ai/test/anthropic-retry.test.ts @@ -18,6 +18,10 @@ describe("isProviderRetryableError", () => { ).toBe(true); }); + it("retries first-event timeout errors", () => { + expect(isProviderRetryableError(new Error("Anthropic stream timed out while waiting for the first event"))).toBe(true); + }); + it("does not retry non-transient validation errors", () => { expect(isProviderRetryableError(new Error("Invalid tool schema"))).toBe(false); expect(isProviderRetryableError(new Error("Bad request"))).toBe(false); diff --git a/packages/ai/test/anthropic-stream-timeout.test.ts b/packages/ai/test/anthropic-stream-timeout.test.ts new file mode 100644 index 000000000..177820e70 --- /dev/null +++ b/packages/ai/test/anthropic-stream-timeout.test.ts @@ -0,0 +1,115 @@ +import { afterEach, describe, expect, it, vi } from "bun:test"; +import { Messages } from "@anthropic-ai/sdk/resources/messages/messages"; +import { streamAnthropic } from "../src/providers/anthropic"; +import type { Context, Model } from "../src/types"; + +const originalFirstEventTimeout = Bun.env.PI_STREAM_FIRST_EVENT_TIMEOUT_MS; + +const model: Model<"anthropic-messages"> = { + id: "claude-sonnet-4-5", + name: "Claude Sonnet 4.5", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 200_000, + maxTokens: 8_192, +}; + +const context: Context = { + messages: [{ role: "user", content: "Say hi", timestamp: Date.now() }], +}; + +function createHangingAnthropicStream(signal: AbortSignal | undefined): AsyncIterable> { + return { + async *[Symbol.asyncIterator]() { + const { promise, reject } = Promise.withResolvers(); + const onAbort = () => reject(new Error("request aborted")); + if (signal?.aborted) { + onAbort(); + } else { + signal?.addEventListener("abort", onAbort, { once: true }); + } + try { + await promise; + } catch { + return; + } finally { + signal?.removeEventListener("abort", onAbort); + } + }, + }; +} + +function createSuccessfulAnthropicStream(text: string): AsyncIterable> { + return { + async *[Symbol.asyncIterator]() { + yield { + type: "message_start", + message: { + id: "msg_retry_success", + usage: { + input_tokens: 12, + output_tokens: 0, + cache_read_input_tokens: 0, + cache_creation_input_tokens: 0, + }, + }, + }; + yield { + type: "content_block_start", + index: 0, + content_block: { type: "text", text: "" }, + }; + yield { + type: "content_block_delta", + index: 0, + delta: { type: "text_delta", text }, + }; + yield { type: "content_block_stop", index: 0 }; + yield { + type: "message_delta", + delta: { stop_reason: "end_turn" }, + usage: { + input_tokens: 12, + output_tokens: 4, + cache_read_input_tokens: 0, + cache_creation_input_tokens: 0, + }, + }; + }, + }; +} + +afterEach(() => { + vi.restoreAllMocks(); + if (originalFirstEventTimeout === undefined) { + delete Bun.env.PI_STREAM_FIRST_EVENT_TIMEOUT_MS; + } else { + Bun.env.PI_STREAM_FIRST_EVENT_TIMEOUT_MS = originalFirstEventTimeout; + } +}); + +describe("anthropic first-event timeout retries", () => { + it("retries when the provider never sends the first stream event", async () => { + Bun.env.PI_STREAM_FIRST_EVENT_TIMEOUT_MS = "20"; + let attempt = 0; + + vi.spyOn(Messages.prototype, "stream").mockImplementation((_body, requestOptions) => { + attempt += 1; + const signal = (requestOptions as { signal?: AbortSignal } | undefined)?.signal; + return (attempt === 1 + ? createHangingAnthropicStream(signal) + : createSuccessfulAnthropicStream("retry recovered")) as never; + }); + + const result = await streamAnthropic(model, context, { apiKey: "sk-ant-test" }).result(); + + expect(attempt).toBe(2); + expect(result.stopReason).toBe("stop"); + expect(result.content).toEqual([{ type: "text", text: "retry recovered" }]); + expect(result.responseId).toBe("msg_retry_success"); + }); +}); From 7ebe96590530b516df9a316a6d96978146667c96 Mon Sep 17 00:00:00 2001 From: can1357 Date: Wed, 1 Apr 2026 04:44:24 +0200 Subject: [PATCH 10/19] feat(ai): added thinkingSignature field to preserve reasoning field names across conversation turns - Added `thinkingSignature` field to thinking content blocks to preserve the original reasoning field name (e.g., `reasoning_text`, `reasoning_content`) for accurate follow-up requests. - Enhanced thinking block handling to track and distinguish between different reasoning field types, enabling proper field name preservation across multiple conversation turns. - Updated `appendThinking` function to accept optional signature parameter and conditionally create new thinking blocks when signature changes. - Added test case validating that streamed reasoning field names are preserved for follow-up requests. --- packages/ai/CHANGELOG.md | 6 ++ .../ai/src/providers/openai-completions.ts | 64 +++++++++++++------ .../ai/test/openai-completions-compat.test.ts | 46 ++++++++++++- 3 files changed, 94 insertions(+), 22 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 112f51b28..8c6acf497 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,11 +1,17 @@ # Changelog ## [Unreleased] + ### Added +- Added `thinkingSignature` field to thinking content blocks to preserve the original reasoning field name (e.g., `reasoning_text`, `reasoning_content`) for accurate follow-up requests - Added first-event timeout detection for streaming responses to abort stuck requests before user-visible content arrives - Added `PI_STREAM_FIRST_EVENT_TIMEOUT_MS` environment variable to configure first-event timeout (defaults to 15 seconds or idle timeout, whichever is lower) +### Changed + +- Changed thinking block handling to track and distinguish between different reasoning field types, enabling proper field name preservation across multiple turns + ### Fixed - Fixed Anthropic stream timeout errors to be properly retried by recognizing first-event timeout messages diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index c4af9b37a..ac21f9e1b 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -31,7 +31,13 @@ import { } from "../types"; import { AssistantMessageEventStream } from "../utils/event-stream"; import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector"; -import { createFirstEventWatchdog, getOpenAIStreamIdleTimeoutMs, getStreamFirstEventTimeoutMs, iterateWithIdleTimeout, markFirstStreamEvent } from "../utils/idle-iterator"; +import { + createFirstEventWatchdog, + getOpenAIStreamIdleTimeoutMs, + getStreamFirstEventTimeoutMs, + iterateWithIdleTimeout, + markFirstStreamEvent, +} from "../utils/idle-iterator"; import { parseStreamingJson } from "../utils/json-parse"; import { getKimiCommonHeaders } from "../utils/oauth/kimi"; import { adaptSchemaForStrict, NO_STRICT } from "../utils/schema"; @@ -193,9 +199,8 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = ( ? AbortSignal.any([options.signal, requestAbortController.signal]) : requestAbortController.signal; const idleTimeoutMs = getOpenAIStreamIdleTimeoutMs(); - const firstEventWatchdog = createFirstEventWatchdog( - getStreamFirstEventTimeoutMs(idleTimeoutMs), - () => requestAbortController.abort(), + const firstEventWatchdog = createFirstEventWatchdog(getStreamFirstEventTimeoutMs(idleTimeoutMs), () => + requestAbortController.abort(), ); const { client, copilotPremiumRequests, baseUrl } = await createClient( model, @@ -223,7 +228,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = ( let currentBlock: OpenAIStreamBlock | undefined; const blockIndex = (block: OpenAIStreamBlock | undefined): number => { if (!block) return Math.max(0, output.content.length - 1); - return output.content.findIndex(contentBlock => contentBlock === block); + return output.content.indexOf(block); }; const finishCurrentBlock = (block: OpenAIStreamBlock | undefined): void => { if (!block) return; @@ -241,7 +246,11 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = ( delete (block as { partialArgs?: string }).partialArgs; stream.push({ type: "toolcall_end", contentIndex, toolCall: block, partial: output }); }; - const appendText = (message: AssistantMessage, eventStream: AssistantMessageEventStream, text: string): void => { + const appendText = ( + message: AssistantMessage, + eventStream: AssistantMessageEventStream, + text: string, + ): void => { if (!currentBlock || currentBlock.type !== "text") { finishCurrentBlock(currentBlock); currentBlock = { type: "text", text: "" }; @@ -256,10 +265,19 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = ( partial: message, }); }; - const appendThinking = (message: AssistantMessage, eventStream: AssistantMessageEventStream, thinking: string): void => { - if (!currentBlock || currentBlock.type !== "thinking") { + const appendThinking = ( + message: AssistantMessage, + eventStream: AssistantMessageEventStream, + thinking: string, + signature?: string, + ): void => { + if ( + !currentBlock || + currentBlock.type !== "thinking" || + (signature !== undefined && currentBlock.thinkingSignature !== signature) + ) { finishCurrentBlock(currentBlock); - currentBlock = { type: "thinking", thinking: "" }; + currentBlock = { type: "thinking", thinking: "", thinkingSignature: signature }; message.content.push(currentBlock); eventStream.push({ type: "thinking_start", @@ -267,6 +285,9 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = ( partial: message, }); } + if (signature !== undefined && !currentBlock.thinkingSignature) { + currentBlock.thinkingSignature = signature; + } currentBlock.thinking += thinking; eventStream.push({ type: "thinking_delta", @@ -283,10 +304,10 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = ( if (!firstTokenTime) firstTokenTime = Date.now(); appendText(output, stream, text); }; - const appendThinkingDelta = (thinking: string) => { + const appendThinkingDelta = (thinking: string, signature?: string) => { if (!thinking) return; if (!firstTokenTime) firstTokenTime = Date.now(); - appendThinking(output, stream, thinking); + appendThinking(output, stream, thinking, signature); }; const flushTaggedTextBuffer = () => { @@ -323,14 +344,11 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = ( } }; - for await (const chunk of iterateWithIdleTimeout( - markFirstStreamEvent(openaiStream, firstEventWatchdog), - { - idleTimeoutMs, - errorMessage: "OpenAI completions stream stalled while waiting for the next event", - onIdle: () => requestAbortController.abort(), - }, - )) { + for await (const chunk of iterateWithIdleTimeout(markFirstStreamEvent(openaiStream, firstEventWatchdog), { + idleTimeoutMs, + errorMessage: "OpenAI completions stream stalled while waiting for the next event", + onIdle: () => requestAbortController.abort(), + })) { if (!chunk || typeof chunk !== "object") continue; // OpenAI documents ChatCompletionChunk.id as the unique chat completion identifier, @@ -395,7 +413,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = ( if (foundReasoningField) { const delta = (choice.delta as any)[foundReasoningField]; - appendThinkingDelta(delta); + appendThinkingDelta(delta, foundReasoningField); } if (choice?.delta?.tool_calls) { @@ -414,7 +432,11 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = ( partialArgs: "", }; output.content.push(currentBlock); - stream.push({ type: "toolcall_start", contentIndex: blockIndex(currentBlock), partial: output }); + stream.push({ + type: "toolcall_start", + contentIndex: blockIndex(currentBlock), + partial: output, + }); } if (currentBlock.type === "toolCall") { diff --git a/packages/ai/test/openai-completions-compat.test.ts b/packages/ai/test/openai-completions-compat.test.ts index 55fdf3e00..fa644e7c8 100644 --- a/packages/ai/test/openai-completions-compat.test.ts +++ b/packages/ai/test/openai-completions-compat.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it } from "bun:test"; import { getBundledModel } from "../src/models"; -import { convertMessages, streamOpenAICompletions } from "../src/providers/openai-completions"; +import { convertMessages, detectCompat, streamOpenAICompletions } from "../src/providers/openai-completions"; import type { AssistantMessage, Context, Model, OpenAICompat } from "../src/types"; const originalFetch = global.fetch; @@ -235,4 +235,48 @@ describe("openai-completions compatibility", () => { }), ); }); + + it("preserves the streamed reasoning field name for follow-up requests", async () => { + const model: Model<"openai-completions"> = { + ...getBundledModel("openai", "gpt-4o-mini"), + api: "openai-completions", + }; + global.fetch = createMockFetch([ + { + id: "chatcmpl-reasoning-text", + object: "chat.completion.chunk", + created: 0, + model: model.id, + choices: [ + { + index: 0, + delta: { reasoning_text: "inspect tool output" }, + }, + ], + }, + { + id: "chatcmpl-reasoning-text", + object: "chat.completion.chunk", + created: 0, + model: model.id, + choices: [{ index: 0, delta: {}, finish_reason: "stop" }], + }, + "[DONE]", + ]); + + const result = await streamOpenAICompletions(model, baseContext(), { apiKey: "test-key" }).result(); + expect(result.content).toContainEqual({ + type: "thinking", + thinking: "inspect tool output", + thinkingSignature: "reasoning_text", + }); + + const messages = convertMessages(model, { messages: [result] }, detectCompat(model)); + const assistant = messages.find(message => message.role === "assistant"); + expect(assistant).toBeDefined(); + const assistantObject = toObject(assistant); + expect(assistantObject).toBeDefined(); + expect(assistantObject ? Reflect.get(assistantObject, "reasoning_text") : undefined).toBe("inspect tool output"); + expect(assistantObject ? Reflect.get(assistantObject, "reasoning_content") : undefined).toBeUndefined(); + }); }); From 6645a7a0598a567445ebf25970c46c4e4e83270e Mon Sep 17 00:00:00 2001 From: can1357 Date: Wed, 1 Apr 2026 04:49:16 +0200 Subject: [PATCH 11/19] fix(model-registry): filtered disabled providers from model listing - Applied `disabledProviders` setting to `getAvailable()` and `getDiscoverableProviders()` so `--list-models` and `/model` hide models from disabled providers. - Initialized settings before model refresh in the `--list-models` code path. - Added tests verifying disabled providers are excluded from both methods. Fixes #588 --- packages/coding-agent/CHANGELOG.md | 1 + .../coding-agent/src/config/model-registry.ts | 22 ++++++++++-- packages/coding-agent/src/main.ts | 1 + .../coding-agent/test/model-registry.test.ts | 35 +++++++++++++++++++ 4 files changed, 56 insertions(+), 3 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 1e25a1519..43a82da80 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -7,6 +7,7 @@ ### Fixed +- Fixed `--list-models` and `/model` provider filtering to hide models from disabled providers ([#588](https://github.com/can1357/oh-my-pi/issues/588)) - Fixed edit tool diffstats to use diff-specific add/remove theme colors instead of success/error status colors ([#589](https://github.com/can1357/oh-my-pi/issues/589)) diff --git a/packages/coding-agent/src/config/model-registry.ts b/packages/coding-agent/src/config/model-registry.ts index ff85de248..d53ed3244 100644 --- a/packages/coding-agent/src/config/model-registry.ts +++ b/packages/coding-agent/src/config/model-registry.ts @@ -31,7 +31,7 @@ import { type ConfigError, ConfigFile } from "../config"; import { parseModelString } from "../config/model-resolver"; import { isValidThemeColor, type ThemeColor } from "../modes/theme/theme"; import type { AuthStorage, OAuthCredential } from "../session/auth-storage"; -import type { Settings } from "./settings"; +import { settings, type Settings } from "./settings"; export const kNoAuth = "N/A"; @@ -730,6 +730,14 @@ function normalizeSuppressedSelector(selector: string): string { return `${parsed.provider}/${parsed.id}`; } +function getDisabledProviderIdsFromSettings(): Set { + try { + return new Set(settings.get("disabledProviders")); + } catch { + return new Set(); + } +} + /** * Model registry - loads and manages models, resolves API keys via AuthStorage. */ @@ -1670,11 +1678,19 @@ export class ModelRegistry { * This is a fast check that doesn't refresh OAuth tokens. */ getAvailable(): Model[] { - return this.#models.filter(m => this.#keylessProviders.has(m.provider) || this.authStorage.hasAuth(m.provider)); + const disabledProviders = getDisabledProviderIdsFromSettings(); + return this.#models.filter( + m => + !disabledProviders.has(m.provider) && + (this.#keylessProviders.has(m.provider) || this.authStorage.hasAuth(m.provider)), + ); } getDiscoverableProviders(): string[] { - return this.#discoverableProviders.map(provider => provider.provider); + const disabledProviders = getDisabledProviderIdsFromSettings(); + return this.#discoverableProviders + .filter(provider => !disabledProviders.has(provider.provider)) + .map(provider => provider.provider); } getProviderDiscoveryState(provider: string): ProviderDiscoveryState | undefined { diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index c5ec44e18..f6ad36c5c 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -536,6 +536,7 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise Settings.init({ cwd: getProjectDir() })); await modelRegistry.refresh("online"); const searchPattern = typeof parsedArgs.listModels === "string" ? parsedArgs.listModels : undefined; await listModels(modelRegistry, searchPattern); diff --git a/packages/coding-agent/test/model-registry.test.ts b/packages/coding-agent/test/model-registry.test.ts index 2a2c1e3ec..f18bb52fb 100644 --- a/packages/coding-agent/test/model-registry.test.ts +++ b/packages/coding-agent/test/model-registry.test.ts @@ -4,6 +4,7 @@ import * as os from "node:os"; import * as path from "node:path"; import { Effort, type OpenAICompat, type ThinkingConfig } from "@oh-my-pi/pi-ai"; import { kNoAuth, MODEL_ROLES, ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry"; +import { _resetSettingsForTest, Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; import { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage"; import { hookFetch, Snowflake } from "@oh-my-pi/pi-utils"; @@ -18,6 +19,7 @@ describe("ModelRegistry", () => { }); beforeEach(async () => { + _resetSettingsForTest(); tempDir = path.join(os.tmpdir(), `pi-test-model-registry-${Snowflake.next()}`); fs.mkdirSync(tempDir, { recursive: true }); modelsJsonPath = path.join(tempDir, "models.json"); @@ -25,6 +27,7 @@ describe("ModelRegistry", () => { }); afterEach(() => { + _resetSettingsForTest(); authStorage.close(); if (tempDir && fs.existsSync(tempDir)) { fs.rmSync(tempDir, { recursive: true }); @@ -1034,6 +1037,38 @@ describe("ModelRegistry", () => { expect(model.baseUrl).toBe(initialBaseUrl); }); }); + + describe("disabled provider filtering", () => { + test("getAvailable and getDiscoverableProviders exclude disabled providers from settings", async () => { + writeRawModelsJson({ + ollama: { + baseUrl: "http://127.0.0.1:11434/v1", + api: "openai-completions", + auth: "none", + discovery: { type: "ollama" }, + }, + }); + await authStorage.set("github-copilot", [ + { + type: "oauth", + access: "tid=1;proxy-ep=proxy.individual.githubcopilot.com;exp=9999999999", + refresh: "refresh-individual", + expires: Date.now() + 60_000, + }, + ]); + await Settings.init({ + inMemory: true, + overrides: { + disabledProviders: ["github-copilot", "ollama"], + }, + }); + + const registry = new ModelRegistry(authStorage, modelsJsonPath); + + expect(registry.getAvailable().some(model => model.provider === "github-copilot")).toBe(false); + expect(registry.getDiscoverableProviders()).not.toContain("ollama"); + }); + }); describe("runtime discovery", () => { test("auto-discovers ollama models without provider config", async () => { using _hook = hookFetch(input => { From 77eddf80e924ee515e7ed31e593c66cb94c2b18f Mon Sep 17 00:00:00 2001 From: can1357 Date: Wed, 1 Apr 2026 04:49:41 +0200 Subject: [PATCH 12/19] fix(patch): wrapped long diff lines with continuation gutters - Replaced truncation with line wrapping for diff output in the edit tool renderer. - Added `wrapEditRendererLine` to preserve gutter markers and line numbers on continuation lines. Fixes #578 --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/src/patch/shared.ts | 28 ++++++++++++++++++++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 43a82da80..e3ecae06a 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -7,6 +7,7 @@ ### Fixed +- Fixed edit tool diff rendering to wrap long diff lines with continuation gutters instead of truncating them at terminal width ([#578](https://github.com/can1357/oh-my-pi/issues/578)) - Fixed `--list-models` and `/model` provider filtering to hide models from disabled providers ([#588](https://github.com/can1357/oh-my-pi/issues/588)) - Fixed edit tool diffstats to use diff-specific add/remove theme colors instead of success/error status colors ([#589](https://github.com/can1357/oh-my-pi/issues/589)) diff --git a/packages/coding-agent/src/patch/shared.ts b/packages/coding-agent/src/patch/shared.ts index 7b877e6d9..0178629e8 100644 --- a/packages/coding-agent/src/patch/shared.ts +++ b/packages/coding-agent/src/patch/shared.ts @@ -3,7 +3,7 @@ */ import type { ToolCallContext } from "@oh-my-pi/pi-agent-core"; import type { Component } from "@oh-my-pi/pi-tui"; -import { Text } from "@oh-my-pi/pi-tui"; +import { Text, visibleWidth, wrapTextWithAnsi } from "@oh-my-pi/pi-tui"; import type { RenderResultOptions } from "../extensibility/custom-tools/types"; import type { FileDiagnosticsResult } from "../lsp"; import { renderDiff as renderDiffColored } from "../modes/components/diff"; @@ -222,6 +222,29 @@ function renderDiffSection( return text; } +function wrapEditRendererLine(line: string, width: number): string[] { + if (width <= 0) return [line]; + if (line.length === 0) return [""]; + + const startAnsi = line.match(/^((?:\x1b\[[0-9;]*m)*)/)?.[1] ?? ""; + const bodyWithReset = line.slice(startAnsi.length); + const body = bodyWithReset.endsWith("\x1b[39m") ? bodyWithReset.slice(0, -"\x1b[39m".length) : bodyWithReset; + const diffMatch = /^([+\-\s])(\s*\d+)\|(.*)$/s.exec(body); + + if (!diffMatch) { + return wrapTextWithAnsi(line, width); + } + + const [, marker, lineNum, content] = diffMatch; + const prefix = `${marker}${lineNum}|`; + const prefixWidth = visibleWidth(prefix); + const contentWidth = Math.max(1, width - prefixWidth); + const continuationPrefix = `${" ".repeat(Math.max(0, prefixWidth - 1))}|`; + const wrappedContent = wrapTextWithAnsi(content, contentWidth); + + return wrappedContent.map((segment, index) => `${startAnsi}${index === 0 ? prefix : continuationPrefix}${segment}\x1b[39m`); +} + export const editToolRenderer = { mergeCallAndResult: true, @@ -356,8 +379,7 @@ export const editToolRenderer = { ); } - const lines = - width > 0 ? text.split("\n").map(line => truncateToWidth(line, width, Ellipsis.Omit)) : text.split("\n"); + const lines = width > 0 ? text.split("\n").flatMap(line => wrapEditRendererLine(line, width)) : text.split("\n"); cached = { key, lines }; return lines; }, From 906123c97d013f7fa4fb1568e094a6d37e88f562 Mon Sep 17 00:00:00 2001 From: can1357 Date: Wed, 1 Apr 2026 04:50:07 +0200 Subject: [PATCH 13/19] style: reformat --- .../src/providers/azure-openai-responses.ts | 26 ++++++----- packages/ai/src/providers/openai-responses.ts | 26 ++++++----- packages/ai/src/utils/idle-iterator.ts | 19 +++++--- .../ai/src/utils/oauth/vercel-ai-gateway.ts | 46 +++++++++---------- packages/ai/test/anthropic-retry.test.ts | 4 +- .../ai/test/anthropic-stream-timeout.test.ts | 6 +-- .../coding-agent/src/config/model-registry.ts | 2 +- packages/coding-agent/src/patch/shared.ts | 9 ++-- .../coding-agent/src/utils/title-generator.ts | 1 - 9 files changed, 74 insertions(+), 65 deletions(-) diff --git a/packages/ai/src/providers/azure-openai-responses.ts b/packages/ai/src/providers/azure-openai-responses.ts index f836fcfb8..7d1bfef85 100644 --- a/packages/ai/src/providers/azure-openai-responses.ts +++ b/packages/ai/src/providers/azure-openai-responses.ts @@ -20,7 +20,13 @@ import { } from "../types"; import { AssistantMessageEventStream } from "../utils/event-stream"; import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector"; -import { createFirstEventWatchdog, getOpenAIStreamIdleTimeoutMs, getStreamFirstEventTimeoutMs, iterateWithIdleTimeout, markFirstStreamEvent } from "../utils/idle-iterator"; +import { + createFirstEventWatchdog, + getOpenAIStreamIdleTimeoutMs, + getStreamFirstEventTimeoutMs, + iterateWithIdleTimeout, + markFirstStreamEvent, +} from "../utils/idle-iterator"; import { mapToOpenAIResponsesToolChoice } from "../utils/tool-choice"; import { supportsDeveloperRole } from "./openai-responses"; import { @@ -121,9 +127,8 @@ export const streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses" ? AbortSignal.any([options.signal, requestAbortController.signal]) : requestAbortController.signal; const idleTimeoutMs = getOpenAIStreamIdleTimeoutMs(); - const firstEventWatchdog = createFirstEventWatchdog( - getStreamFirstEventTimeoutMs(idleTimeoutMs), - () => requestAbortController.abort(), + const firstEventWatchdog = createFirstEventWatchdog(getStreamFirstEventTimeoutMs(idleTimeoutMs), () => + requestAbortController.abort(), ); options?.onPayload?.(params); rawRequestDump = { @@ -138,14 +143,11 @@ export const streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses" stream.push({ type: "start", partial: output }); await processResponsesStream( - iterateWithIdleTimeout( - markFirstStreamEvent(openaiStream, firstEventWatchdog), - { - idleTimeoutMs, - errorMessage: "Azure OpenAI responses stream stalled while waiting for the next event", - onIdle: () => requestAbortController.abort(), - }, - ), + iterateWithIdleTimeout(markFirstStreamEvent(openaiStream, firstEventWatchdog), { + idleTimeoutMs, + errorMessage: "Azure OpenAI responses stream stalled while waiting for the next event", + onIdle: () => requestAbortController.abort(), + }), output, stream, model, diff --git a/packages/ai/src/providers/openai-responses.ts b/packages/ai/src/providers/openai-responses.ts index 480e2a951..a3bc8a1a5 100644 --- a/packages/ai/src/providers/openai-responses.ts +++ b/packages/ai/src/providers/openai-responses.ts @@ -30,7 +30,13 @@ import { } from "../utils"; import { AssistantMessageEventStream } from "../utils/event-stream"; import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector"; -import { createFirstEventWatchdog, getOpenAIStreamIdleTimeoutMs, getStreamFirstEventTimeoutMs, iterateWithIdleTimeout, markFirstStreamEvent } from "../utils/idle-iterator"; +import { + createFirstEventWatchdog, + getOpenAIStreamIdleTimeoutMs, + getStreamFirstEventTimeoutMs, + iterateWithIdleTimeout, + markFirstStreamEvent, +} from "../utils/idle-iterator"; import { adaptSchemaForStrict, NO_STRICT } from "../utils/schema"; import { mapToOpenAIResponsesToolChoice } from "../utils/tool-choice"; import { @@ -173,9 +179,8 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = ( ? AbortSignal.any([options.signal, requestAbortController.signal]) : requestAbortController.signal; const idleTimeoutMs = getOpenAIStreamIdleTimeoutMs(); - const firstEventWatchdog = createFirstEventWatchdog( - getStreamFirstEventTimeoutMs(idleTimeoutMs), - () => requestAbortController.abort(), + const firstEventWatchdog = createFirstEventWatchdog(getStreamFirstEventTimeoutMs(idleTimeoutMs), () => + requestAbortController.abort(), ); options?.onPayload?.(params); rawRequestDump = { @@ -192,14 +197,11 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = ( const nativeOutputItems: Array> = []; await processResponsesStream( - iterateWithIdleTimeout( - markFirstStreamEvent(openaiStream, firstEventWatchdog), - { - idleTimeoutMs, - errorMessage: "OpenAI responses stream stalled while waiting for the next event", - onIdle: () => requestAbortController.abort(), - }, - ), + iterateWithIdleTimeout(markFirstStreamEvent(openaiStream, firstEventWatchdog), { + idleTimeoutMs, + errorMessage: "OpenAI responses stream stalled while waiting for the next event", + onIdle: () => requestAbortController.abort(), + }), output, stream, model, diff --git a/packages/ai/src/utils/idle-iterator.ts b/packages/ai/src/utils/idle-iterator.ts index c86bc8394..cd52b4abb 100644 --- a/packages/ai/src/utils/idle-iterator.ts +++ b/packages/ai/src/utils/idle-iterator.ts @@ -26,7 +26,10 @@ export function getOpenAIStreamIdleTimeoutMs(): number | undefined { * Set `PI_STREAM_FIRST_EVENT_TIMEOUT_MS=0` to disable the watchdog. */ export function getStreamFirstEventTimeoutMs(idleTimeoutMs?: number): number | undefined { - const fallback = Math.min(DEFAULT_STREAM_FIRST_EVENT_TIMEOUT_MS, idleTimeoutMs ?? DEFAULT_STREAM_FIRST_EVENT_TIMEOUT_MS); + const fallback = Math.min( + DEFAULT_STREAM_FIRST_EVENT_TIMEOUT_MS, + idleTimeoutMs ?? DEFAULT_STREAM_FIRST_EVENT_TIMEOUT_MS, + ); return normalizeIdleTimeoutMs($env.PI_STREAM_FIRST_EVENT_TIMEOUT_MS, fallback); } @@ -39,10 +42,7 @@ export interface FirstEventWatchdog { * Starts a watchdog that aborts a request if no first stream event arrives in time. * Call `markFirstEventReceived()` as soon as the first event is observed. */ -export function createFirstEventWatchdog( - timeoutMs: number | undefined, - onTimeout: () => void, -): FirstEventWatchdog { +export function createFirstEventWatchdog(timeoutMs: number | undefined, onTimeout: () => void): FirstEventWatchdog { let timer: NodeJS.Timeout | undefined; if (timeoutMs !== undefined && timeoutMs > 0) { timer = setTimeout(() => { @@ -105,7 +105,10 @@ export async function* iterateWithIdleTimeout( options: IdleTimeoutIteratorOptions, ): AsyncGenerator { const firstItemTimeoutMs = options.firstItemTimeoutMs ?? options.idleTimeoutMs; - if ((firstItemTimeoutMs === undefined || firstItemTimeoutMs <= 0) && (options.idleTimeoutMs === undefined || options.idleTimeoutMs <= 0)) { + if ( + (firstItemTimeoutMs === undefined || firstItemTimeoutMs <= 0) && + (options.idleTimeoutMs === undefined || options.idleTimeoutMs <= 0) + ) { for await (const item of iterable) { yield item; } @@ -152,7 +155,9 @@ export async function* iterateWithIdleTimeout( if (returnPromise) { void returnPromise.catch(() => {}); } - throw new Error(sawFirstItem ? options.errorMessage : (options.firstItemErrorMessage ?? options.errorMessage)); + throw new Error( + sawFirstItem ? options.errorMessage : (options.firstItemErrorMessage ?? options.errorMessage), + ); } if (outcome.kind === "error") { throw outcome.error; diff --git a/packages/ai/src/utils/oauth/vercel-ai-gateway.ts b/packages/ai/src/utils/oauth/vercel-ai-gateway.ts index 0ecfe6234..3163c4869 100644 --- a/packages/ai/src/utils/oauth/vercel-ai-gateway.ts +++ b/packages/ai/src/utils/oauth/vercel-ai-gateway.ts @@ -11,8 +11,7 @@ import type { OAuthController } from "./types"; -const AUTH_URL = - "https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai-gateway%2Fapi-keys&title=AI+Gateway+API+Keys"; +const AUTH_URL = "https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai-gateway%2Fapi-keys&title=AI+Gateway+API+Keys"; /** * Login to Vercel AI Gateway. @@ -20,32 +19,29 @@ const AUTH_URL = * Opens browser to Vercel AI Gateway docs and prompts for an API key. * Returns the API key directly (not OAuthCredentials - this isn't OAuth). */ -export async function loginVercelAiGateway( - options: OAuthController, -): Promise { - if (!options.onPrompt) { - throw new Error("Vercel AI Gateway login requires onPrompt callback"); - } +export async function loginVercelAiGateway(options: OAuthController): Promise { + if (!options.onPrompt) { + throw new Error("Vercel AI Gateway login requires onPrompt callback"); + } - options.onAuth?.({ - url: AUTH_URL, - instructions: - "Copy your Vercel AI Gateway API key from the Vercel dashboard", - }); + options.onAuth?.({ + url: AUTH_URL, + instructions: "Copy your Vercel AI Gateway API key from the Vercel dashboard", + }); - const apiKey = await options.onPrompt({ - message: "Paste your Vercel AI Gateway API key", - placeholder: "vck_...", - }); + const apiKey = await options.onPrompt({ + message: "Paste your Vercel AI Gateway API key", + placeholder: "vck_...", + }); - if (options.signal?.aborted) { - throw new Error("Login cancelled"); - } + if (options.signal?.aborted) { + throw new Error("Login cancelled"); + } - const trimmed = apiKey.trim(); - if (!trimmed) { - throw new Error("API key is required"); - } + const trimmed = apiKey.trim(); + if (!trimmed) { + throw new Error("API key is required"); + } - return trimmed; + return trimmed; } diff --git a/packages/ai/test/anthropic-retry.test.ts b/packages/ai/test/anthropic-retry.test.ts index 7a7482b8a..3c3a8e129 100644 --- a/packages/ai/test/anthropic-retry.test.ts +++ b/packages/ai/test/anthropic-retry.test.ts @@ -19,7 +19,9 @@ describe("isProviderRetryableError", () => { }); it("retries first-event timeout errors", () => { - expect(isProviderRetryableError(new Error("Anthropic stream timed out while waiting for the first event"))).toBe(true); + expect(isProviderRetryableError(new Error("Anthropic stream timed out while waiting for the first event"))).toBe( + true, + ); }); it("does not retry non-transient validation errors", () => { diff --git a/packages/ai/test/anthropic-stream-timeout.test.ts b/packages/ai/test/anthropic-stream-timeout.test.ts index 177820e70..c54012c92 100644 --- a/packages/ai/test/anthropic-stream-timeout.test.ts +++ b/packages/ai/test/anthropic-stream-timeout.test.ts @@ -100,9 +100,9 @@ describe("anthropic first-event timeout retries", () => { vi.spyOn(Messages.prototype, "stream").mockImplementation((_body, requestOptions) => { attempt += 1; const signal = (requestOptions as { signal?: AbortSignal } | undefined)?.signal; - return (attempt === 1 - ? createHangingAnthropicStream(signal) - : createSuccessfulAnthropicStream("retry recovered")) as never; + return ( + attempt === 1 ? createHangingAnthropicStream(signal) : createSuccessfulAnthropicStream("retry recovered") + ) as never; }); const result = await streamAnthropic(model, context, { apiKey: "sk-ant-test" }).result(); diff --git a/packages/coding-agent/src/config/model-registry.ts b/packages/coding-agent/src/config/model-registry.ts index d53ed3244..9099c51e2 100644 --- a/packages/coding-agent/src/config/model-registry.ts +++ b/packages/coding-agent/src/config/model-registry.ts @@ -31,7 +31,7 @@ import { type ConfigError, ConfigFile } from "../config"; import { parseModelString } from "../config/model-resolver"; import { isValidThemeColor, type ThemeColor } from "../modes/theme/theme"; import type { AuthStorage, OAuthCredential } from "../session/auth-storage"; -import { settings, type Settings } from "./settings"; +import { type Settings, settings } from "./settings"; export const kNoAuth = "N/A"; diff --git a/packages/coding-agent/src/patch/shared.ts b/packages/coding-agent/src/patch/shared.ts index 0178629e8..1030c8117 100644 --- a/packages/coding-agent/src/patch/shared.ts +++ b/packages/coding-agent/src/patch/shared.ts @@ -21,7 +21,7 @@ import { shortenPath, truncateDiffByHunk, } from "../tools/render-utils"; -import { Ellipsis, Hasher, type RenderCache, renderStatusLine, truncateToWidth } from "../tui"; +import { Hasher, type RenderCache, renderStatusLine, truncateToWidth } from "../tui"; import type { HashlineToolEdit } from "./index"; import type { DiffError, DiffResult, Operation } from "./types"; @@ -242,7 +242,9 @@ function wrapEditRendererLine(line: string, width: number): string[] { const continuationPrefix = `${" ".repeat(Math.max(0, prefixWidth - 1))}|`; const wrappedContent = wrapTextWithAnsi(content, contentWidth); - return wrappedContent.map((segment, index) => `${startAnsi}${index === 0 ? prefix : continuationPrefix}${segment}\x1b[39m`); + return wrappedContent.map( + (segment, index) => `${startAnsi}${index === 0 ? prefix : continuationPrefix}${segment}\x1b[39m`, + ); } export const editToolRenderer = { @@ -379,7 +381,8 @@ export const editToolRenderer = { ); } - const lines = width > 0 ? text.split("\n").flatMap(line => wrapEditRendererLine(line, width)) : text.split("\n"); + const lines = + width > 0 ? text.split("\n").flatMap(line => wrapEditRendererLine(line, width)) : text.split("\n"); cached = { key, lines }; return lines; }, diff --git a/packages/coding-agent/src/utils/title-generator.ts b/packages/coding-agent/src/utils/title-generator.ts index 4b986640e..43743ee64 100644 --- a/packages/coding-agent/src/utils/title-generator.ts +++ b/packages/coding-agent/src/utils/title-generator.ts @@ -28,7 +28,6 @@ function getTitleModel( const availableModels = registry.getAvailable(); if (availableModels.length === 0) return undefined; - const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() }; const titleModel = resolveRoleSelection(["commit", "smol"], settings, availableModels); if (titleModel) { return { model: titleModel.model, thinkingLevel: titleModel.thinkingLevel }; From bccc62eb2e317d463730657912538f648459716f Mon Sep 17 00:00:00 2001 From: can1357 Date: Wed, 1 Apr 2026 05:10:53 +0200 Subject: [PATCH 14/19] fix: CI reasoning replay and test imports --- packages/ai/src/providers/openai-completions.ts | 17 +++++++++++------ packages/coding-agent/test/join-patch.test.ts | 1 + 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index ac21f9e1b..11e356d06 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -927,7 +927,13 @@ export function convertMessages( } if (compat.thinkingFormat === "openai") { - const reasoningField = compat.reasoningContentField ?? "reasoning_content"; + const streamedReasoningField = nonEmptyThinkingBlocks[0]?.thinkingSignature; + const reasoningField = + streamedReasoningField === "reasoning_content" || + streamedReasoningField === "reasoning" || + streamedReasoningField === "reasoning_text" + ? streamedReasoningField + : (compat.reasoningContentField ?? "reasoning_content"); const reasoningContent = (assistantMsg as any)[reasoningField]; if (!reasoningContent) { const reasoning = (assistantMsg as any).reasoning; @@ -983,10 +989,9 @@ export function convertMessages( (assistantMsg as any).reasoning_details = reasoningDetails; } } - // Skip assistant messages that have no content and no tool calls. - // Mistral explicitly requires "either content or tool_calls, but not none". - // Other providers also don't accept empty assistant messages. - // This handles aborted assistant responses that got no content. + // Skip assistant messages that have no content, no tool calls, and no reasoning payload. + // Some OpenAI-compatible backends require replaying reasoning-only assistant turns + // so follow-up requests preserve the provider-specific reasoning field name. const content = assistantMsg.content; const hasContent = content !== null && @@ -995,7 +1000,7 @@ export function convertMessages( if (!hasContent && assistantMsg.tool_calls && compat.requiresAssistantContentForToolCalls) { assistantMsg.content = "."; } - if (!hasContent && !assistantMsg.tool_calls) { + if (!hasContent && !assistantMsg.tool_calls && !hasReasoningField) { continue; } params.push(assistantMsg); diff --git a/packages/coding-agent/test/join-patch.test.ts b/packages/coding-agent/test/join-patch.test.ts index b3e790ece..bcaa4e453 100644 --- a/packages/coding-agent/test/join-patch.test.ts +++ b/packages/coding-agent/test/join-patch.test.ts @@ -1,3 +1,4 @@ +import { describe, expect, test } from "bun:test"; import { joinPatch } from "@oh-my-pi/pi-coding-agent/commit/git"; describe("joinPatch", () => { From 15db1dec5650b58aebb738ac459af1d01625a22a Mon Sep 17 00:00:00 2001 From: Sam Biggins Date: Wed, 1 Apr 2026 05:18:58 +0200 Subject: [PATCH 15/19] fix(ai): update spoofed Gemini CLI User-Agent to v0.35.3 format (#574) * fix(ai): update spoofed Gemini CLI User-Agent to v0.35.3 format Gemini CLI v0.35+ changed its User-Agent format: - Dropped 'google-api-nodejs-client/10.5.0' suffix - Added surface field (third param in parens) - New format: GeminiCLI/VERSION/MODEL (PLATFORM; ARCH; SURFACE) The stale v0.34.0 UA was contributing to google-gemini-cli/gemini-3.1-pro-preview being rejected, alongside Google's new abuse detection for third-party CCA usage (google-gemini/gemini-cli#22970). Verified: model responds successfully with updated UA. * feat(scripts): add spoofed version drift checker Fetches latest stable release from google-gemini/gemini-cli GitHub releases and compares against the hardcoded version in the provider. bun check-spoofed-versions # report drift, exit 1 on mismatch bun check-spoofed-versions --update # apply version bump in-place Extensible for additional spoofed tools (Antigravity, Copilot headers). --- package.json | 3 +- .../ai/src/providers/google-gemini-cli.ts | 8 +- scripts/check-spoofed-versions.ts | 114 ++++++++++++++++++ 3 files changed, 120 insertions(+), 5 deletions(-) create mode 100644 scripts/check-spoofed-versions.ts diff --git a/package.json b/package.json index 100abd40f..d591ded5e 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "generate-models": "bun --cwd=packages/ai scripts/generate-models.ts", "generate-docs-index": "bun --cwd=packages/coding-agent run generate-docs-index", "generate-template": "bun --cwd=packages/coding-agent run generate-template", - "sync-exports": "bun scripts/sync-exports.ts" + "sync-exports": "bun scripts/sync-exports.ts", + "check-spoofed-versions": "bun scripts/check-spoofed-versions.ts" }, "devDependencies": { "@biomejs/biome": "^2.4", diff --git a/packages/ai/src/providers/google-gemini-cli.ts b/packages/ai/src/providers/google-gemini-cli.ts index 043dbe011..eb376dbf4 100644 --- a/packages/ai/src/providers/google-gemini-cli.ts +++ b/packages/ai/src/providers/google-gemini-cli.ts @@ -65,14 +65,14 @@ const ANTIGRAVITY_ENDPOINT_FALLBACKS = [ANTIGRAVITY_DAILY_ENDPOINT, ANTIGRAVITY_ /** * Build a User-Agent string that identifies as Gemini CLI to unlock higher rate limits. - * Uses the same format as the official Gemini CLI: - * GeminiCLI/VERSION/MODEL (PLATFORM; ARCH) google-api-nodejs-client/10.5.0 + * Uses the same format as the official Gemini CLI (v0.35+): + * GeminiCLI/VERSION/MODEL (PLATFORM; ARCH; SURFACE) */ export function getGeminiCliUserAgent(modelId = "gemini-3.1-pro-preview"): string { - const version = process.env.PI_AI_GEMINI_CLI_VERSION || "0.34.0"; + const version = process.env.PI_AI_GEMINI_CLI_VERSION || "0.35.3"; const platform = process.platform === "win32" ? "win32" : process.platform; const arch = process.arch === "x64" ? "x64" : process.arch; - return `GeminiCLI/${version}/${modelId} (${platform}; ${arch}) google-api-nodejs-client/10.5.0`; + return `GeminiCLI/${version}/${modelId} (${platform}; ${arch}; terminal)`; } const ANTIGRAVITY_USER_AGENT = (() => { diff --git a/scripts/check-spoofed-versions.ts b/scripts/check-spoofed-versions.ts new file mode 100644 index 000000000..07f76ff6b --- /dev/null +++ b/scripts/check-spoofed-versions.ts @@ -0,0 +1,114 @@ +#!/usr/bin/env bun + +/** + * Checks spoofed external tool versions against their latest GitHub releases. + * + * We impersonate several external tools (Gemini CLI, Antigravity) via User-Agent + * strings. When these tools release new versions, the upstream service may start + * rejecting or deprioritizing older versions. This script detects drift so we + * can bump before users hit 403s/429s. + * + * Usage: + * bun scripts/check-spoofed-versions.ts # check and report + * bun scripts/check-spoofed-versions.ts --update # update source in-place + */ + +import * as path from "node:path"; + +const PROVIDER_FILE = path.join( + import.meta.dir, + "../packages/ai/src/providers/google-gemini-cli.ts", +); + +interface VersionCheck { + /** Human label for the report. */ + name: string; + /** Regex to extract the current hardcoded version from PROVIDER_FILE. */ + sourcePattern: RegExp; + /** GitHub owner/repo to fetch latest release from. */ + repo: string; + /** Extract semver from the release tag name (e.g. "v0.35.3" -> "0.35.3"). */ + parseTag: (tag: string) => string | null; +} + +/** Fetch latest non-prerelease tag from a GitHub repo. */ +async function fetchLatestGitHubRelease(repo: string, parseTag: (tag: string) => string | null): Promise { + try { + // /releases/latest only returns non-prerelease, non-draft releases + const res = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, { + headers: { Accept: "application/vnd.github+json", "User-Agent": "oh-my-pi/version-check" }, + }); + if (!res.ok) return null; + const data = (await res.json()) as { tag_name?: string }; + return data.tag_name ? parseTag(data.tag_name) : null; + } catch { + return null; + } +} + +const SEMVER_RE = /(\d+\.\d+\.\d+)/; + +const checks: VersionCheck[] = [ + { + name: "Gemini CLI", + sourcePattern: /PI_AI_GEMINI_CLI_VERSION\s*\|\|\s*"(\d+\.\d+\.\d+)"/, + repo: "google-gemini/gemini-cli", + parseTag: (tag) => SEMVER_RE.exec(tag)?.[1] ?? null, + }, +]; + +async function run() { + const doUpdate = process.argv.includes("--update"); + let source = await Bun.file(PROVIDER_FILE).text(); + let anyDrift = false; + let anyUpdate = false; + let anyChecked = false; + + for (const check of checks) { + const match = check.sourcePattern.exec(source); + if (!match?.[1]) { + console.error(`[WARN] Could not extract current ${check.name} version from source`); + continue; + } + + const current = match[1]; + const latest = await fetchLatestGitHubRelease(check.repo, check.parseTag); + + if (!latest) { + console.error(`[FAIL] Could not fetch latest ${check.name} version from ${check.repo}`); + continue; + } + + anyChecked = true; + + if (current === latest) { + console.log(`[OK] ${check.name}: ${current} (up to date)`); + } else { + console.log(`[DRIFT] ${check.name}: ${current} -> ${latest}`); + anyDrift = true; + + if (doUpdate) { + source = source.replace(match[0], match[0].replace(current, latest)); + anyUpdate = true; + console.log(` Updated in source.`); + } + } + } + + if (anyUpdate) { + await Bun.write(PROVIDER_FILE, source); + console.log(`\nWrote updates to ${path.relative(process.cwd(), PROVIDER_FILE)}`); + } + + if (!anyChecked) { + console.error("\nNo version checks succeeded. Cannot verify freshness."); + process.exit(1); + } + + if (anyDrift && !doUpdate) { + console.log("\nRun with --update to apply version bumps."); + process.exit(1); + } +} + +run(); From 9b453ce2e542eac2f83c17bda0197b0eae18f89f Mon Sep 17 00:00:00 2001 From: can1357 Date: Wed, 1 Apr 2026 05:19:32 +0200 Subject: [PATCH 16/19] chore: bump version to 13.17.1 --- Cargo.lock | 62 ++++++++-------- Cargo.toml | 2 +- bun.lock | 102 +++++++++++++------------- packages/agent/package.json | 2 +- packages/ai/CHANGELOG.md | 2 + packages/ai/package.json | 2 +- packages/coding-agent/CHANGELOG.md | 2 + packages/coding-agent/package.json | 2 +- packages/natives/package.json | 2 +- packages/stats/package.json | 2 +- packages/swarm-extension/package.json | 2 +- packages/tui/package.json | 2 +- packages/utils/package.json | 2 +- 13 files changed, 94 insertions(+), 92 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b9fc6408c..f776145fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -291,9 +291,9 @@ dependencies = [ [[package]] name = "blake3" -version = "1.8.3" +version = "1.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" +checksum = "4d2d5991425dfd0785aed03aedcf0b321d61975c9b5b3689c774a2610ae0b51e" dependencies = [ "arrayref", "arrayvec", @@ -676,9 +676,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.17" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" dependencies = [ "libc", ] @@ -1864,9 +1864,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.92" +version = "0.3.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4c90f45aa2e6eacbe8645f77fdea542ac97a494bcd117a67df9ff4d611f995" +checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" dependencies = [ "once_cell", "wasm-bindgen", @@ -2165,9 +2165,9 @@ dependencies = [ [[package]] name = "neo_frizbee" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "638d4811ea05dee397475493065555838c35ed43c2d7d1bdeb5ff674d1a3bd67" +checksum = "34a8a11f48357cecd5ada0751f1490d8da4dd935e595d34cc2727567d5288ead" dependencies = [ "itertools 0.14.0", "raw-cpuid", @@ -2679,7 +2679,7 @@ dependencies = [ [[package]] name = "pi-natives" -version = "13.17.0" +version = "13.17.1" dependencies = [ "arboard", "ast-grep-core", @@ -4402,9 +4402,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.115" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6523d69017b7633e396a89c5efab138161ed5aafcbc8d3e5c5a42ae38f50495a" +checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" dependencies = [ "cfg-if", "once_cell", @@ -4415,9 +4415,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.115" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3a6c758eb2f701ed3d052ff5737f5bfe6614326ea7f3bbac7156192dc32e67" +checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4425,9 +4425,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.115" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "921de2737904886b52bcbb237301552d05969a6f9c40d261eb0533c8b055fedf" +checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" dependencies = [ "bumpalo", "proc-macro2", @@ -4438,18 +4438,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.115" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a93e946af942b58934c604527337bad9ae33ba1d5c6900bbb41c2c07c2364a93" +checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" dependencies = [ "unicode-ident", ] [[package]] name = "wayland-backend" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa75f400b7f719bcd68b3f47cd939ba654cedeef690f486db71331eec4c6a406" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" dependencies = [ "cc", "downcast-rs", @@ -4460,9 +4460,9 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.13" +version = "0.31.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab51d9f7c071abeee76007e2b742499e535148035bb835f97aaed1338cf516c3" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" dependencies = [ "bitflags 2.11.0", "rustix", @@ -4472,9 +4472,9 @@ dependencies = [ [[package]] name = "wayland-protocols" -version = "0.32.11" +version = "0.32.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b23b5df31ceff1328f06ac607591d5ba360cf58f90c8fad4ac8d3a55a3c4aec7" +checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f" dependencies = [ "bitflags 2.11.0", "wayland-backend", @@ -4484,9 +4484,9 @@ dependencies = [ [[package]] name = "wayland-protocols-wlr" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78248e4cc0eff8163370ba5c158630dcae1f3497a586b826eca2ef5f348d6235" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" dependencies = [ "bitflags 2.11.0", "wayland-backend", @@ -4497,9 +4497,9 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.9" +version = "0.31.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c86287151a309799b821ca709b7345a048a2956af05957c89cb824ab919fa4e3" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" dependencies = [ "proc-macro2", "quick-xml", @@ -4508,18 +4508,18 @@ dependencies = [ [[package]] name = "wayland-sys" -version = "0.31.10" +version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374f6b70e8e0d6bf9461a32988fd553b59ff630964924dad6e4a4eb6bd538d17" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" dependencies = [ "pkg-config", ] [[package]] name = "web-sys" -version = "0.3.92" +version = "0.3.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84cde8507f4d7cfcb1185b8cb5890c494ffea65edbe1ba82cfd63661c805ed94" +checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index 6eaa6d640..8f8735645 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ exclude = ["crates/brush-core-vendored", "crates/brush-builtins-vendored"] resolver = "3" [workspace.package] -version = "13.17.0" +version = "13.17.1" edition = "2024" license = "MIT" authors = ["Can Boluk"] diff --git a/bun.lock b/bun.lock index da4340c54..6dd9afcb7 100644 --- a/bun.lock +++ b/bun.lock @@ -15,7 +15,7 @@ }, "packages/agent": { "name": "@oh-my-pi/pi-agent-core", - "version": "13.17.0", + "version": "13.17.1", "dependencies": { "@oh-my-pi/pi-ai": "workspace:*", "@oh-my-pi/pi-utils": "workspace:*", @@ -27,7 +27,7 @@ }, "packages/ai": { "name": "@oh-my-pi/pi-ai", - "version": "13.17.0", + "version": "13.17.1", "bin": { "pi-ai": "./src/cli.ts", }, @@ -51,7 +51,7 @@ }, "packages/coding-agent": { "name": "@oh-my-pi/pi-coding-agent", - "version": "13.17.0", + "version": "13.17.1", "bin": { "omp": "src/cli.ts", }, @@ -80,7 +80,7 @@ }, "packages/natives": { "name": "@oh-my-pi/pi-natives", - "version": "13.17.0", + "version": "13.17.1", "dependencies": { "@oh-my-pi/pi-utils": "workspace:*", }, @@ -114,7 +114,7 @@ }, "packages/stats": { "name": "@oh-my-pi/omp-stats", - "version": "13.17.0", + "version": "13.17.1", "bin": { "omp-stats": "./src/index.ts", }, @@ -139,7 +139,7 @@ }, "packages/swarm-extension": { "name": "@oh-my-pi/swarm-extension", - "version": "13.17.0", + "version": "13.17.1", "bin": { "omp-swarm": "src/cli.ts", }, @@ -152,7 +152,7 @@ }, "packages/tui": { "name": "@oh-my-pi/pi-tui", - "version": "13.17.0", + "version": "13.17.1", "dependencies": { "@oh-my-pi/pi-natives": "workspace:*", "@oh-my-pi/pi-utils": "workspace:*", @@ -165,7 +165,7 @@ }, "packages/utils": { "name": "@oh-my-pi/pi-utils", - "version": "13.17.0", + "version": "13.17.1", "dependencies": { "beautiful-mermaid": "^1.1", "winston": "^3.19", @@ -191,25 +191,25 @@ "@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], - "@aws-sdk/client-bedrock-runtime": ["@aws-sdk/client-bedrock-runtime@3.1019.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.25", "@aws-sdk/credential-provider-node": "^3.972.27", "@aws-sdk/eventstream-handler-node": "^3.972.12", "@aws-sdk/middleware-eventstream": "^3.972.8", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.9", "@aws-sdk/middleware-user-agent": "^3.972.26", "@aws-sdk/middleware-websocket": "^3.972.14", "@aws-sdk/region-config-resolver": "^3.972.10", "@aws-sdk/token-providers": "3.1019.0", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.12", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.12", "@smithy/eventstream-serde-browser": "^4.2.12", "@smithy/eventstream-serde-config-resolver": "^4.3.12", "@smithy/eventstream-serde-node": "^4.2.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.27", "@smithy/middleware-retry": "^4.4.44", "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.43", "@smithy/util-defaults-mode-node": "^4.2.47", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/util-stream": "^4.5.20", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-Wq1uMAZfySYofuwkFMaMM+k7epsGBRcJGE+ZosGB+8jC8Xs1lycbjSEFMt0Mo3z1qhkgEKGCQyjCbPTICMkkVw=="], + "@aws-sdk/client-bedrock-runtime": ["@aws-sdk/client-bedrock-runtime@3.1021.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.26", "@aws-sdk/credential-provider-node": "^3.972.29", "@aws-sdk/eventstream-handler-node": "^3.972.12", "@aws-sdk/middleware-eventstream": "^3.972.8", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.9", "@aws-sdk/middleware-user-agent": "^3.972.28", "@aws-sdk/middleware-websocket": "^3.972.14", "@aws-sdk/region-config-resolver": "^3.972.10", "@aws-sdk/token-providers": "3.1021.0", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.14", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.13", "@smithy/eventstream-serde-browser": "^4.2.12", "@smithy/eventstream-serde-config-resolver": "^4.3.12", "@smithy/eventstream-serde-node": "^4.2.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.28", "@smithy/middleware-retry": "^4.4.46", "@smithy/middleware-serde": "^4.2.16", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.1", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.44", "@smithy/util-defaults-mode-node": "^4.2.48", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.13", "@smithy/util-stream": "^4.5.21", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FqAZq9ewaprev+BAUgUNeQKACVVEbGw8V3SZTKv0hEy5Ed5yhJhNuOL9G0QiUtdG6rX6NujGZjO5xBgKcvMLxQ=="], - "@aws-sdk/core": ["@aws-sdk/core@3.973.25", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws-sdk/xml-builder": "^3.972.16", "@smithy/core": "^3.23.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-TNrx7eq6nKNOO62HWPqoBqPLXEkW6nLZQGwjL6lq1jZtigWYbK1NbCnT7mKDzbLMHZfuOECUt3n6CzxjUW9HWQ=="], + "@aws-sdk/core": ["@aws-sdk/core@3.973.26", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws-sdk/xml-builder": "^3.972.16", "@smithy/core": "^3.23.13", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-A/E6n2W42ruU+sfWk+mMUOyVXbsSgGrY3MJ9/0Az5qUdG67y8I6HYzzoAa+e/lzxxl1uCYmEL6BTMi9ZiZnplQ=="], - "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.23", "", { "dependencies": { "@aws-sdk/core": "^3.973.25", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-EamaclJcCEaPHp6wiVknNMM2RlsPMjAHSsYSFLNENBM8Wz92QPc6cOn3dif6vPDQt0Oo4IEghDy3NMDCzY/IvA=="], + "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.24", "", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-FWg8uFmT6vQM7VuzELzwVo5bzExGaKHdubn0StjgrcU5FvuLExUe+k06kn/40uKv59rYzhez8eFNM4yYE/Yb/w=="], - "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.25", "", { "dependencies": { "@aws-sdk/core": "^3.973.25", "@aws-sdk/types": "^3.973.6", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/node-http-handler": "^4.5.0", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/util-stream": "^4.5.20", "tslib": "^2.6.2" } }, "sha512-qPymamdPcLp6ugoVocG1y5r69ScNiRzb0hogX25/ij+Wz7c7WnsgjLTaz7+eB5BfRxeyUwuw5hgULMuwOGOpcw=="], + "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.26", "", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/types": "^3.973.6", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/node-http-handler": "^4.5.1", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/util-stream": "^4.5.21", "tslib": "^2.6.2" } }, "sha512-CY4ppZ+qHYqcXqBVi//sdHST1QK3KzOEiLtpLsc9W2k2vfZPKExGaQIsOwcyvjpjUEolotitmd3mUNY56IwDEA=="], - "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.26", "", { "dependencies": { "@aws-sdk/core": "^3.973.25", "@aws-sdk/credential-provider-env": "^3.972.23", "@aws-sdk/credential-provider-http": "^3.972.25", "@aws-sdk/credential-provider-login": "^3.972.26", "@aws-sdk/credential-provider-process": "^3.972.23", "@aws-sdk/credential-provider-sso": "^3.972.26", "@aws-sdk/credential-provider-web-identity": "^3.972.26", "@aws-sdk/nested-clients": "^3.996.16", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-xKxEAMuP6GYx2y5GET+d3aGEroax3AgGfwBE65EQAUe090lzyJ/RzxPX9s8v7Z6qAk0XwfQl+LrmH05X7YvTeg=="], + "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.28", "", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/credential-provider-env": "^3.972.24", "@aws-sdk/credential-provider-http": "^3.972.26", "@aws-sdk/credential-provider-login": "^3.972.28", "@aws-sdk/credential-provider-process": "^3.972.24", "@aws-sdk/credential-provider-sso": "^3.972.28", "@aws-sdk/credential-provider-web-identity": "^3.972.28", "@aws-sdk/nested-clients": "^3.996.18", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-wXYvq3+uQcZV7k+bE4yDXCTBdzWTU9x/nMiKBfzInmv6yYK1veMK0AKvRfRBd72nGWYKcL6AxwiPg9z/pYlgpw=="], - "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.26", "", { "dependencies": { "@aws-sdk/core": "^3.973.25", "@aws-sdk/nested-clients": "^3.996.16", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-EFcM8RM3TUxnZOfMJo++3PnyxFu1fL/huzmn3Vh+8IWRgqZawUD3cRwwOr+/4bE9DpyHaLOWFAjY0lfK5X9ZkQ=="], + "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.28", "", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/nested-clients": "^3.996.18", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-ZSTfO6jqUTCysbdBPtEX5OUR//3rbD0lN7jO3sQeS2Gjr/Y+DT6SbIJ0oT2cemNw3UzKu97sNONd1CwNMthuZQ=="], - "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.27", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.23", "@aws-sdk/credential-provider-http": "^3.972.25", "@aws-sdk/credential-provider-ini": "^3.972.26", "@aws-sdk/credential-provider-process": "^3.972.23", "@aws-sdk/credential-provider-sso": "^3.972.26", "@aws-sdk/credential-provider-web-identity": "^3.972.26", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-jXpxSolfFnPVj6GCTtx3xIdWNoDR7hYC/0SbetGZxOC9UnNmipHeX1k6spVstf7eWJrMhXNQEgXC0pD1r5tXIg=="], + "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.29", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.24", "@aws-sdk/credential-provider-http": "^3.972.26", "@aws-sdk/credential-provider-ini": "^3.972.28", "@aws-sdk/credential-provider-process": "^3.972.24", "@aws-sdk/credential-provider-sso": "^3.972.28", "@aws-sdk/credential-provider-web-identity": "^3.972.28", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-clSzDcvndpFJAggLDnDb36sPdlZYyEs5Zm6zgZjjUhwsJgSWiWKwFIXUVBcbruidNyBdbpOv2tNDL9sX8y3/0g=="], - "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.23", "", { "dependencies": { "@aws-sdk/core": "^3.973.25", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-IL/TFW59++b7MpHserjUblGrdP5UXy5Ekqqx1XQkERXBFJcZr74I7VaSrQT5dxdRMU16xGK4L0RQ5fQG1pMgnA=="], + "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.24", "", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Q2k/XLrFXhEztPHqj4SLCNID3hEPdlhh1CDLBpNnM+1L8fq7P+yON9/9M1IGN/dA5W45v44ylERfXtDAlmMNmw=="], - "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.26", "", { "dependencies": { "@aws-sdk/core": "^3.973.25", "@aws-sdk/nested-clients": "^3.996.16", "@aws-sdk/token-providers": "3.1019.0", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-c6ghvRb6gTlMznWhGxn/bpVCcp0HRaz4DobGVD9kI4vwHq186nU2xN/S7QGkm0lo0H2jQU8+dgpUFLxfTcwCOg=="], + "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.28", "", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/nested-clients": "^3.996.18", "@aws-sdk/token-providers": "3.1021.0", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-IoUlmKMLEITFn1SiCTjPfR6KrE799FBo5baWyk/5Ppar2yXZoUdaRqZzJzK6TcJxx450M8m8DbpddRVYlp5R/A=="], - "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.26", "", { "dependencies": { "@aws-sdk/core": "^3.973.25", "@aws-sdk/nested-clients": "^3.996.16", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-cXcS3+XD3iwhoXkM44AmxjmbcKueoLCINr1e+IceMmCySda5ysNIfiGBGe9qn5EMiQ9Jd7pP0AGFtcd6OV3Lvg=="], + "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.28", "", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/nested-clients": "^3.996.18", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-d+6h0SD8GGERzKe27v5rOzNGKOl0D+l0bWJdqrxH8WSQzHzjsQFIAPgIeOTUwBHVsKKwtSxc91K/SWax6XgswQ=="], "@aws-sdk/eventstream-handler-node": ["@aws-sdk/eventstream-handler-node@3.972.12", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/eventstream-codec": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-ruyc/MNR6e+cUrGCth7fLQ12RXBZDy/bV06tgqB9Z5n/0SN/C0m6bsQEV8FF9zPI6VSAOaRd0rNgmpYVnGawrQ=="], @@ -221,15 +221,15 @@ "@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-/Wt5+CT8dpTFQxEJ9iGy/UGrXr7p2wlIOEHvIr/YcHYByzoLjrqkYqXdJjd9UIgWjv7eqV2HnFJen93UTuwfTQ=="], - "@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.26", "", { "dependencies": { "@aws-sdk/core": "^3.973.25", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@smithy/core": "^3.23.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-retry": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-AilFIh4rI/2hKyyGN6XrB0yN96W2o7e7wyrPWCM6QjZM1mcC/pVkW3IWWRvuBWMpVP8Fg+rMpbzeLQ6dTM4gig=="], + "@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.28", "", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@smithy/core": "^3.23.13", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-retry": "^4.2.13", "tslib": "^2.6.2" } }, "sha512-cfWZFlVh7Va9lRay4PN2A9ARFzaBYcA097InT5M2CdRS05ECF5yaz86jET8Wsl2WcyKYEvVr/QNmKtYtafUHtQ=="], "@aws-sdk/middleware-websocket": ["@aws-sdk/middleware-websocket@3.972.14", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-format-url": "^3.972.8", "@smithy/eventstream-codec": "^4.2.12", "@smithy/eventstream-serde-browser": "^4.2.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-qnfDlIHjm6DrTYNvWOUbnZdVKgtoKbO/Qzj+C0Wp5Y7VUrsvBRQtGKxD+hc+mRTS4N0kBJ6iZ3+zxm4N1OSyjg=="], - "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.16", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.25", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.9", "@aws-sdk/middleware-user-agent": "^3.972.26", "@aws-sdk/region-config-resolver": "^3.972.10", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.12", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.27", "@smithy/middleware-retry": "^4.4.44", "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.43", "@smithy/util-defaults-mode-node": "^4.2.47", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-L7Qzoj/qQU1cL5GnYLQP5LbI+wlLCLoINvcykR3htKcQ4tzrPf2DOs72x933BM7oArYj1SKrkb2lGlsJHIic3g=="], + "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.18", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.26", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.9", "@aws-sdk/middleware-user-agent": "^3.972.28", "@aws-sdk/region-config-resolver": "^3.972.10", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.14", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.13", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.28", "@smithy/middleware-retry": "^4.4.46", "@smithy/middleware-serde": "^4.2.16", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.1", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.44", "@smithy/util-defaults-mode-node": "^4.2.48", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.13", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-c7ZSIXrESxHKx2Mcopgd8AlzZgoXMr20fkx5ViPWPOLBvmyhw9VwJx/Govg8Ef/IhEon5R9l53Z8fdYSEmp6VA=="], "@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/config-resolver": "^4.4.13", "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-1dq9ToC6e070QvnVhhbAs3bb5r6cQ10gTVc6cyRV5uvQe7P138TV2uG2i6+Yok4bAkVAcx5AqkTEBUvWEtBlsQ=="], - "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1019.0", "", { "dependencies": { "@aws-sdk/core": "^3.973.25", "@aws-sdk/nested-clients": "^3.996.16", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-OF+2RfRmUKyjzrRWlDcyju3RBsuqcrYDQ8TwrJg8efcOotMzuZN4U9mpVTIdATpmEc4lWNZBMSjPzrGm6JPnAQ=="], + "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1021.0", "", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/nested-clients": "^3.996.18", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-TKY6h9spUk3OLs5v1oAgW9mAeBE3LAGNBwJokLy96wwmd4W2v/tYlXseProyed9ValDj2u1jK/4Rg1T+1NXyJA=="], "@aws-sdk/types": ["@aws-sdk/types@3.973.6", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw=="], @@ -241,7 +241,7 @@ "@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA=="], - "@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.12", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.26", "@aws-sdk/types": "^3.973.6", "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-8phW0TS8ntENJgDcFewYT/Q8dOmarpvSxEjATu2GUBAutiHr++oEGCiBUwxslCMNvwW2cAPZNT53S/ym8zm/gg=="], + "@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.14", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.28", "@aws-sdk/types": "^3.973.6", "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-vNSB/DYaPOyujVZBg/zUznH9QC142MaTHVmaFlF7uzzfg3CgT9f/l4C0Yi+vU/tbBhxVcXVB90Oohk5+o+ZbWw=="], "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.16", "", { "dependencies": { "@smithy/types": "^4.13.1", "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" } }, "sha512-iu2pyvaqmeatIJLURLqx9D+4jKAdTH20ntzB6BFwjyN7V960r4jK32mx0Zf7YbtOYAbmbtQfDNuL60ONinyw7A=="], @@ -267,23 +267,23 @@ "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], - "@biomejs/biome": ["@biomejs/biome@2.4.9", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.9", "@biomejs/cli-darwin-x64": "2.4.9", "@biomejs/cli-linux-arm64": "2.4.9", "@biomejs/cli-linux-arm64-musl": "2.4.9", "@biomejs/cli-linux-x64": "2.4.9", "@biomejs/cli-linux-x64-musl": "2.4.9", "@biomejs/cli-win32-arm64": "2.4.9", "@biomejs/cli-win32-x64": "2.4.9" }, "bin": { "biome": "bin/biome" } }, "sha512-wvZW92FrwitTcacvCBT8xdAbfbxWfDLwjYMmU3djjqQTh7Ni4ZdiWIT/x5VcZ+RQuxiKzIOzi5D+dcyJDFZMsA=="], + "@biomejs/biome": ["@biomejs/biome@2.4.10", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.10", "@biomejs/cli-darwin-x64": "2.4.10", "@biomejs/cli-linux-arm64": "2.4.10", "@biomejs/cli-linux-arm64-musl": "2.4.10", "@biomejs/cli-linux-x64": "2.4.10", "@biomejs/cli-linux-x64-musl": "2.4.10", "@biomejs/cli-win32-arm64": "2.4.10", "@biomejs/cli-win32-x64": "2.4.10" }, "bin": { "biome": "bin/biome" } }, "sha512-xxA3AphFQ1geij4JTHXv4EeSTda1IFn22ye9LdyVPoJU19fNVl0uzfEuhsfQ4Yue/0FaLs2/ccVi4UDiE7R30w=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-d5G8Gf2RpH5pYwiHLPA+UpG3G9TLQu4WM+VK6sfL7K68AmhcEQ9r+nkj/DvR/GYhYox6twsHUtmWWWIKfcfQQA=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vuzzI1cWqDVzOMIkYyHbKqp+AkQq4K7k+UCXWpkYcY/HDn1UxdsbsfgtVpa40shem8Kax4TLDLlx8kMAecgqiw=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-LNCLNgqDMG7BLdc3a8aY/dwKPK7+R8/JXJoXjCvZh2gx8KseqBdFDKbhrr7HCWF8SzNhbTaALhTBoh/I6rf9lA=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-14fzASRo+BPotwp7nWULy2W5xeUyFnTaq1V13Etrrxkrih+ez/2QfgFm5Ehtf5vSjtgx/IJycMMpn5kPd5ZNaA=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-4adnkAUi6K4C/emPRgYznMOcLlUqZdXWM6aIui4VP4LraE764g6Q4YguygnAUoxKjKIXIWPteKMgRbN0wsgwcg=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-7MH1CMW5uuxQ/s7FLST63qF8B3Hgu2HRdZ7tA1X1+mk+St4JOuIrqdhIBnnyqeyWJNI+Bww7Es5QZ0wIc1Cmkw=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-8RCww5xnPn2wpK4L/QDGDOW0dq80uVWfppPxHIUg6mOs9B6gRmqPp32h1Ls3T8GnW8Wo5A8u7vpTwz4fExN+sw=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-WrJY6UuiSD/Dh+nwK2qOTu8kdMDlLV3dLMmychIghHPAysWFq1/DGC1pVZx8POE3ZkzKR3PUUnVrtZfMfaJjyQ=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.9", "", { "os": "linux", "cpu": "x64" }, "sha512-L10na7POF0Ks/cgLFNF1ZvIe+X4onLkTi5oP9hY+Rh60Q+7fWzKDDCeGyiHUFf1nGIa9dQOOUPGe2MyYg8nMSQ=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.10", "", { "os": "linux", "cpu": "x64" }, "sha512-tZLvEEi2u9Xu1zAqRjTcpIDGVtldigVvzug2fTuPG0ME/g8/mXpRPcNgLB22bGn6FvLJpHHnqLnwliOu8xjYrg=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.9", "", { "os": "linux", "cpu": "x64" }, "sha512-5TD+WS9v5vzXKzjetF0hgoaNFHMcpQeBUwKKVi3JbG1e9UCrFuUK3Gt185fyTzvRdwYkJJEMqglRPjmesmVv4A=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.10", "", { "os": "linux", "cpu": "x64" }, "sha512-kDTi3pI6PBN6CiczsWYOyP2zk0IJI08EWEQyDMQWW221rPaaEz6FvjLhnU07KMzLv8q3qSuoB93ua6inSQ55Tw=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-aDZr0RBC3sMGJOU10BvG7eZIlWLK/i51HRIfScE2lVhfts2dQTreowLiJJd+UYg/tHKxS470IbzpuKmd0MiD6g=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-umwQU6qPzH+ISTf/eHyJ/QoQnJs3V9Vpjz2OjZXe9MVBZ7prgGafMy7yYeRGnlmDAn87AKTF3Q6weLoMGpeqdQ=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.9", "", { "os": "win32", "cpu": "x64" }, "sha512-NS4g/2G9SoQ4ktKtz31pvyc/rmgzlcIDCGU/zWbmHJAqx6gcRj2gj5Q/guXhoWTzCUaQZDIqiCQXHS7BcGYc0w=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.10", "", { "os": "win32", "cpu": "x64" }, "sha512-aW/JU5GuyH4uxMrNYpoC2kjaHlyJGLgIa3XkhPEZI0uKhZhJZU8BuEyJmvgzSPQNGozBwWjC972RaNdcJ9KyJg=="], "@bufbuild/protobuf": ["@bufbuild/protobuf@2.11.0", "", {}, "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ=="], @@ -353,11 +353,9 @@ "@sinclair/typebox": ["@sinclair/typebox@0.34.49", "", {}, "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A=="], - "@smithy/abort-controller": ["@smithy/abort-controller@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q=="], - "@smithy/config-resolver": ["@smithy/config-resolver@4.4.13", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-iIzMC5NmOUP6WL6o8iPBjFhUhBZ9pPjpUpQYWMUFQqKyXXzOftbfK8zcQCz/jFV1Psmf05BK5ypx4K2r4Tnwdg=="], - "@smithy/core": ["@smithy/core@3.23.12", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-stream": "^4.5.20", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-o9VycsYNtgC+Dy3I0yrwCqv9CWicDnke0L7EVOrZtJpjb2t0EjaEofmMrYc0T1Kn3yk32zm6cspxF9u9Bj7e5w=="], + "@smithy/core": ["@smithy/core@3.23.13", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-stream": "^4.5.21", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-J+2TT9D6oGsUVXVEMvz8h2EmdVnkBiy2auCie4aSJMvKlzUtO5hqjEzXhoCUkIMo7gAYjbQcN0g/MMSXEhDs1Q=="], "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.12", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg=="], @@ -381,17 +379,17 @@ "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.12", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA=="], - "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.27", "", { "dependencies": { "@smithy/core": "^3.23.12", "@smithy/middleware-serde": "^4.2.15", "@smithy/node-config-provider": "^4.3.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-middleware": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-T3TFfUgXQlpcg+UdzcAISdZpj4Z+XECZ/cefgA6wLBd6V4lRi0svN2hBouN/be9dXQ31X4sLWz3fAQDf+nt6BA=="], + "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.28", "", { "dependencies": { "@smithy/core": "^3.23.13", "@smithy/middleware-serde": "^4.2.16", "@smithy/node-config-provider": "^4.3.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-middleware": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-p1gfYpi91CHcs5cBq982UlGlDrxoYUX6XdHSo91cQ2KFuz6QloHosO7Jc60pJiVmkWrKOV8kFYlGFFbQ2WUKKQ=="], - "@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.44", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/service-error-classification": "^4.2.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-Y1Rav7m5CFRPQyM4CI0koD/bXjyjJu3EQxZZhtLGD88WIrBrQ7kqXM96ncd6rYnojwOo/u9MXu57JrEvu/nLrA=="], + "@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.46", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/service-error-classification": "^4.2.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.13", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-SpvWNNOPOrKQGUqZbEPO+es+FRXMWvIyzUKUOYdDgdlA6BdZj/R58p4umoQ76c2oJC44PiM7mKizyyex1IJzow=="], - "@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.15", "", { "dependencies": { "@smithy/core": "^3.23.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-ExYhcltZSli0pgAKOpQQe1DLFBLryeZ22605y/YS+mQpdNWekum9Ujb/jMKfJKgjtz1AZldtwA/wCYuKJgjjlg=="], + "@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.16", "", { "dependencies": { "@smithy/core": "^3.23.13", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-beqfV+RZ9RSv+sQqor3xroUUYgRFCGRw6niGstPG8zO9LgTl0B0MCucxjmrH/2WwksQN7UUgI7KNANoZv+KALA=="], "@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw=="], "@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.12", "", { "dependencies": { "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw=="], - "@smithy/node-http-handler": ["@smithy/node-http-handler@4.5.0", "", { "dependencies": { "@smithy/abort-controller": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/querystring-builder": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Rnq9vQWiR1+/I6NZZMNzJHV6pZYyEHt2ZnuV3MG8z2NNenC4i/8Kzttz7CjZiHSmsN5frhXhg17z3Zqjjhmz1A=="], + "@smithy/node-http-handler": ["@smithy/node-http-handler@4.5.1", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/querystring-builder": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-ejjxdAXjkPIs9lyYyVutOGNOraqUE9v/NjGMKwwFrfOM354wfSD8lmlj8hVwUzQmlLLF4+udhfCX9Exnbmvfzw=="], "@smithy/property-provider": ["@smithy/property-provider@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A=="], @@ -407,7 +405,7 @@ "@smithy/signature-v4": ["@smithy/signature-v4@5.3.12", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw=="], - "@smithy/smithy-client": ["@smithy/smithy-client@4.12.7", "", { "dependencies": { "@smithy/core": "^3.23.12", "@smithy/middleware-endpoint": "^4.4.27", "@smithy/middleware-stack": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-stream": "^4.5.20", "tslib": "^2.6.2" } }, "sha512-q3gqnwml60G44FECaEEsdQMplYhDMZYCtYhMCzadCnRnnHIobZJjegmdoUo6ieLQlPUzvrMdIJUpx6DoPmzANQ=="], + "@smithy/smithy-client": ["@smithy/smithy-client@4.12.8", "", { "dependencies": { "@smithy/core": "^3.23.13", "@smithy/middleware-endpoint": "^4.4.28", "@smithy/middleware-stack": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-stream": "^4.5.21", "tslib": "^2.6.2" } }, "sha512-aJaAX7vHe5i66smoSSID7t4rKY08PbD8EBU7DOloixvhOozfYWdcSYE4l6/tjkZ0vBZhGjheWzB2mh31sLgCMA=="], "@smithy/types": ["@smithy/types@4.13.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g=="], @@ -423,9 +421,9 @@ "@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ=="], - "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.43", "", { "dependencies": { "@smithy/property-provider": "^4.2.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Qd/0wCKMaXxev/z00TvNzGCH2jlKKKxXP1aDxB6oKwSQthe3Og2dMhSayGCnsma1bK/kQX1+X7SMP99t6FgiiQ=="], + "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.44", "", { "dependencies": { "@smithy/property-provider": "^4.2.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-eZg6XzaCbVr2S5cAErU5eGBDaOVTuTo1I65i4tQcHENRcZ8rMWhQy1DaIYUSLyZjsfXvmCqZrstSMYyGFocvHA=="], - "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.47", "", { "dependencies": { "@smithy/config-resolver": "^4.4.13", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-qSRbYp1EQ7th+sPFuVcVO05AE0QH635hycdEXlpzIahqHHf2Fyd/Zl+8v0XYMJ3cgDVPa0lkMefU7oNUjAP+DQ=="], + "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.48", "", { "dependencies": { "@smithy/config-resolver": "^4.4.13", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-FqOKTlqSaoV3nzO55pMs5NBnZX8EhoI0DGmn9kbYeXWppgHD6dchyuj2HLqp4INJDJbSrj6OFYJkAh/WhSzZPg=="], "@smithy/util-endpoints": ["@smithy/util-endpoints@3.3.3", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig=="], @@ -433,9 +431,9 @@ "@smithy/util-middleware": ["@smithy/util-middleware@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ=="], - "@smithy/util-retry": ["@smithy/util-retry@4.2.12", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-1zopLDUEOwumjcHdJ1mwBHddubYF8GMQvstVCLC54Y46rqoHwlIU+8ZzUeaBcD+WCJHyDGSeZ2ml9YSe9aqcoQ=="], + "@smithy/util-retry": ["@smithy/util-retry@4.2.13", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-qQQsIvL0MGIbUjeSrg0/VlQ3jGNKyM3/2iU3FPNgy01z+Sp4OvcaxbgIoFOTvB61ZoohtutuOvOcgmhbD0katQ=="], - "@smithy/util-stream": ["@smithy/util-stream@4.5.20", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.15", "@smithy/node-http-handler": "^4.5.0", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-4yXLm5n/B5SRBR2p8cZ90Sbv4zL4NKsgxdzCzp/83cXw2KxLEumt5p+GAVyRNZgQOSrzXn9ARpO0lUe8XSlSDw=="], + "@smithy/util-stream": ["@smithy/util-stream@4.5.21", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.15", "@smithy/node-http-handler": "^4.5.1", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-KzSg+7KKywLnkoKejRtIBXDmwBfjGvg1U1i/etkC7XSWUyFCoLno1IohV2c74IzQqdhX5y3uE44r/8/wuK+A7Q=="], "@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw=="], @@ -467,21 +465,21 @@ "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], - "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260330.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260330.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260330.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260330.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260330.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260330.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260330.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260330.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-tyRn6/PAYpkN35bKz2RPfzV/QAACvle9EYlYyywn/p/qPI5yOBLka0zM/2dGKx+o5vHVQ1gr/iVrgW8w7Wqc8Q=="], + "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260331.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260331.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260331.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260331.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260331.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260331.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260331.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260331.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-Gfy2J/LhydkOHOw+ZWRw0M8Xl3O2bzQXLXIYITdMz2N4GpMm8misAvvCzhqMacOGvazKr1FsL9LIIW2kxk6kzw=="], - "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260330.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jX47OX4svh3/CzSDYd81+kELLP5r0+G7lFGiMAVUNpD+ZLmyh/nrkuuI643pGe8D0i9dTfg/aiZQq6d/00Feog=="], + "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260331.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-1PRnBCN2csiCzj76YaSBtP4jPLEGBUmVhXHplC+yHOKaxx9nf3HFiFCg/19raInvN/lJ8+Bp1fZ/qIsWAAHiBw=="], - "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260330.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-JysPTOMnyje+LkdB3LR+Emnl7hJCvMzUVRcOVJyzmxWOMFNCYPWNK3wDdwuoS6jE5Hii9vInt/xtn3pc+kgb+A=="], + "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260331.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-llXnfLGjl+gXsANLD7UI/gSb3lj7aZW13Rf8sVXQnHJ3/dkJRAm/MgLqdjuuyvYq3pFaleiep+zoLd96rLRqUw=="], - "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260330.1", "", { "os": "linux", "cpu": "arm" }, "sha512-rVwd9ZgjR8Le6ItUD/p59LCOYdZbi4DZYavXAMzPPpaJCnmdHwOcJFWc61vricsQljRjVBnunQknHfYABZDq9Q=="], + "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260331.1", "", { "os": "linux", "cpu": "arm" }, "sha512-+8AZzA0BRjMkLDvdQKZOMuheRxNGpSWn7sOtoKqo70R915D0TyEynEXX6B7/aw3+Jfn1H5hLRiBjxoVsmdKENw=="], - "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260330.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-9zlw4ENr6HAUNO+OiWuoQpWUGk4lkqb8NUyXDvTqtlgZGvjyePNVy8OW7KlPuPtCaQS3btV7w4h5X1wlTm2bFA=="], + "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260331.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-sH5gALi89jl5ZjAL/UsLDPsjT/nCLRfHl/pw86ablRX10tYsJhJ/RD6J/cl3g39kJ18tIISSbsuIBn+ncanfSA=="], - "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260330.1", "", { "os": "linux", "cpu": "x64" }, "sha512-7cB4e12e61xaXgJu6N9rMP+6qdkODVsW8zGjVS5a+cobBCsJSdMnlshfitlL9a+BYWe9HseMqPnLcsaE+t22CQ=="], + "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260331.1", "", { "os": "linux", "cpu": "x64" }, "sha512-Yic6MYfX7Uit5jLLENzWFIi6tjp4LTLF37KBiVaHZSvEFyX1kqVwu4j9WNeaz81O6fcB/1dZ1MrILgfcqalNBg=="], - "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260330.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-4wv/FX9F5oAQhzjqwL8KN4O5xgciBy5pT2yLP45xcQahxFUoPnQeF9Pz2x/jwXZSmuh8E1DQWX9evSVGA1MV2A=="], + "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260331.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-vGxK6gtGF97zSx9wOpiVME3h9v0tbZbrHHdKA+fLFNvDV0Df8ud89DEePL7l2yKnVVmf0OnjJy6sYoVyj+LIPA=="], - "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260330.1", "", { "os": "win32", "cpu": "x64" }, "sha512-zKDOG/nq1yTx3cjfc19VFO+TYK2+03Wc17pGdD+X+dWTj15LZ2PKk3s4OtT2b8siTcAUn7zmwLsMCWPYO7xXQw=="], + "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260331.1", "", { "os": "win32", "cpu": "x64" }, "sha512-oJnNiU9UTDPJp6dOmOUW+/Wzt3MQZXIHsDaU4qM0RiAjFE6S+PIX8s5z/ID0orr4MMroUMiLdolL4OVZolNDSw=="], "@typescript/vfs": ["@typescript/vfs@1.6.4", "", { "dependencies": { "debug": "^4.4.3" }, "peerDependencies": { "typescript": "*" } }, "sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ=="], @@ -515,7 +513,7 @@ "bare-path": ["bare-path@3.0.0", "", { "dependencies": { "bare-os": "^3.0.1" } }, "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw=="], - "bare-stream": ["bare-stream@2.11.0", "", { "dependencies": { "streamx": "^2.25.0", "teex": "^1.0.1" }, "peerDependencies": { "bare-abort-controller": "*", "bare-buffer": "*", "bare-events": "*" }, "optionalPeers": ["bare-abort-controller", "bare-buffer", "bare-events"] }, "sha512-Y/+iQ49fL3rIn6w/AVxI/2+BRrpmzJvdWt5Jv8Za6Ngqc6V227c+pYjYYgLdpR3MwQ9ObVXD0ZrqoBztakM0rw=="], + "bare-stream": ["bare-stream@2.12.0", "", { "dependencies": { "streamx": "^2.25.0", "teex": "^1.0.1" }, "peerDependencies": { "bare-abort-controller": "*", "bare-buffer": "*", "bare-events": "*" }, "optionalPeers": ["bare-abort-controller", "bare-buffer", "bare-events"] }, "sha512-w28i8lkBgREV3rPXGbgK+BO66q+ZpKqRWrZLiCdmmUlLPrQ45CzkvRhN+7lnv00Gpi2zy5naRxnUFAxCECDm9g=="], "bare-url": ["bare-url@2.4.0", "", { "dependencies": { "bare-path": "^3.0.0" } }, "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA=="], diff --git a/packages/agent/package.json b/packages/agent/package.json index e22eb7c1a..d18738ea8 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "@oh-my-pi/pi-agent-core", - "version": "13.17.0", + "version": "13.17.1", "description": "General-purpose agent with transport abstraction, state management, and attachment support", "homepage": "https://github.com/can1357/oh-my-pi", "author": "Can Boluk", diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 8c6acf497..843a355a7 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,8 @@ ## [Unreleased] +## [13.17.1] - 2026-04-01 + ### Added - Added `thinkingSignature` field to thinking content blocks to preserve the original reasoning field name (e.g., `reasoning_text`, `reasoning_content`) for accurate follow-up requests diff --git a/packages/ai/package.json b/packages/ai/package.json index 4197acc9a..5965b93c8 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "@oh-my-pi/pi-ai", - "version": "13.17.0", + "version": "13.17.1", "description": "Unified LLM API with automatic model discovery and provider configuration", "homepage": "https://github.com/can1357/oh-my-pi", "author": "Can Boluk", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index e3ecae06a..decb94211 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog ## [Unreleased] + +## [13.17.1] - 2026-04-01 ### Removed - Removed `code_search` tool for code snippet and documentation search diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 91c1d55bf..d3813677a 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "@oh-my-pi/pi-coding-agent", - "version": "13.17.0", + "version": "13.17.1", "description": "Coding agent CLI with read, bash, edit, write tools and session management", "homepage": "https://github.com/can1357/oh-my-pi", "author": "Can Boluk", diff --git a/packages/natives/package.json b/packages/natives/package.json index 2eda873a0..a223806eb 100644 --- a/packages/natives/package.json +++ b/packages/natives/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "@oh-my-pi/pi-natives", - "version": "13.17.0", + "version": "13.17.1", "description": "Native Rust bindings for grep, clipboard, image processing, syntax highlighting, PTY, and shell operations via N-API", "homepage": "https://github.com/can1357/oh-my-pi", "author": "Can Boluk", diff --git a/packages/stats/package.json b/packages/stats/package.json index 7332e3bce..9915c0433 100644 --- a/packages/stats/package.json +++ b/packages/stats/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "@oh-my-pi/omp-stats", - "version": "13.17.0", + "version": "13.17.1", "description": "Local observability dashboard for pi AI usage statistics", "homepage": "https://github.com/can1357/oh-my-pi", "author": "Can Boluk", diff --git a/packages/swarm-extension/package.json b/packages/swarm-extension/package.json index 2d1018791..609510ab1 100644 --- a/packages/swarm-extension/package.json +++ b/packages/swarm-extension/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "@oh-my-pi/swarm-extension", - "version": "13.17.0", + "version": "13.17.1", "description": "Swarm orchestration extension for omp", "homepage": "https://github.com/can1357/oh-my-pi", "author": "Derek Rynd", diff --git a/packages/tui/package.json b/packages/tui/package.json index 270e6068e..0ecb344cf 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "@oh-my-pi/pi-tui", - "version": "13.17.0", + "version": "13.17.1", "description": "Terminal User Interface library with differential rendering for efficient text-based applications", "homepage": "https://github.com/can1357/oh-my-pi", "author": "Can Boluk", diff --git a/packages/utils/package.json b/packages/utils/package.json index e9dcb1a7f..a12eea00b 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "@oh-my-pi/pi-utils", - "version": "13.17.0", + "version": "13.17.1", "description": "Shared utilities for pi packages", "homepage": "https://github.com/can1357/oh-my-pi", "author": "Can Boluk", From 2185f32363c2371894b2409f0f1c878c11e47157 Mon Sep 17 00:00:00 2001 From: can1357 Date: Wed, 1 Apr 2026 06:08:13 +0200 Subject: [PATCH 17/19] fix: publish packages in dependency order --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 23e903bb4..a63c5d9ea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -252,7 +252,7 @@ jobs: env: NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }} run: | - for pkg in packages/*/; do + for pkg in packages/utils packages/ai packages/natives packages/tui packages/stats packages/agent packages/coding-agent; do pkg_name=$(basename "$pkg") # Skip private packages if jq -e '.private == true' "$pkg/package.json" > /dev/null 2>&1; then From 4b1d5ebb0b8b566f2505fdc99fe069af9d7e7864 Mon Sep 17 00:00:00 2001 From: can1357 Date: Wed, 1 Apr 2026 07:41:08 +0200 Subject: [PATCH 18/19] ci: enabled workspace crate caching in Rust cache action - Enabled workspace crate caching in Rust cache action across three CI jobs for faster builds. --- .github/workflows/ci.yml | 5 +++++ rust-toolchain.toml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a63c5d9ea..372f763d7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,6 +63,7 @@ jobs: - uses: Swatinem/rust-cache@v2 with: key: ${{ matrix.target }} + cache-workspace-crates: true - uses: oven-sh/setup-bun@v2 with: bun-version: "1.3" @@ -120,6 +121,8 @@ jobs: with: toolchain: nightly-2026-03-27 - uses: Swatinem/rust-cache@v2 + with: + cache-workspace-crates: true - uses: mlugg/setup-zig@v2 with: version: 0.15.2 @@ -156,6 +159,8 @@ jobs: with: toolchain: nightly-2026-03-27 - uses: Swatinem/rust-cache@v2 + with: + cache-workspace-crates: true - uses: mlugg/setup-zig@v2 with: version: 0.15.2 diff --git a/rust-toolchain.toml b/rust-toolchain.toml index c8bad953b..dd8d2c8b2 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "nightly" +channel = "nightly-2026-03-27" components = ["rustfmt", "clippy", "rust-analyzer"] targets = ["x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"] From 2d5f802daf254c94405a66830f75c1c630178964 Mon Sep 17 00:00:00 2001 From: BayLee4 <63376748+BayLee4@users.noreply.github.com> Date: Wed, 1 Apr 2026 15:26:08 +0200 Subject: [PATCH 19/19] fix(coding-agent): do not enable web_search if explicitly disabled (#593) Co-authored-by: le-cameleon <200889489+le-cameleon@users.noreply.github.com> --- README.md | 1 - packages/coding-agent/src/sdk.ts | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8a325fdee..d70019483 100644 --- a/README.md +++ b/README.md @@ -1240,7 +1240,6 @@ Use `--tools ` to restrict available built-in tools. | `todo_write` | Phased task tracking with progress management | | `fetch` | Fetch and extract URL content | | `web_search` | Multi-provider web search | -| `deep_search` | AI-powered deep research with synthesized results (Exa) | | `write` | Create/overwrite files | | `generate_image` | Generate or edit images using Gemini image models | diff --git a/packages/coding-agent/src/sdk.ts b/packages/coding-agent/src/sdk.ts index 654802369..4c35a1a5d 100644 --- a/packages/coding-agent/src/sdk.ts +++ b/packages/coding-agent/src/sdk.ts @@ -1009,7 +1009,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} } // Add web search tools - customTools.push(...getSearchTools()); + if (options.toolNames?.includes("web_search")) { + customTools.push(...getSearchTools()); + } // Discover and load custom tools from .omp/tools/, .claude/tools/, etc. const builtInToolNames = builtinTools.map(t => t.name);