#!/usr/bin/env node
import {
  schemaToJsonSchema
} from "./chunk-HIU2SH4V.js";
import {
  AuthError,
  ProviderError,
  ProviderNotFoundError,
  RateLimitError
} from "./chunk-TZQHYZKT.js";

// src/providers/claude.ts
import Anthropic from "@anthropic-ai/sdk";

// src/providers/base.ts
var BaseProvider = class {
  /**
   * 将 Message[] 转换为 OpenAI API 格式的消息数组。
   * content 为 string 时直接传递；为 MessageContentPart[] 时保留数组格式（vision 请求）。
   *
   * 自动跳过 role='tool' 和带 toolCalls 的 assistant 消息——
   * 这些是 v0.4.60+ 持久化的工具历史，由 _extraMessages 机制单独注入。
   */
  normalizeMessages(messages) {
    return messages.filter((m) => m.role !== "tool" && !m.toolCalls).map((m) => ({ role: m.role, content: m.content }));
  }
};

// src/providers/claude.ts
var CACHE_MIN_SYSTEM_CHARS = 2e3;
var ClaudeProvider = class extends BaseProvider {
  client;
  info = {
    id: "claude",
    displayName: "Claude (Anthropic)",
    defaultModel: "claude-sonnet-4-5-20250929",
    apiKeyEnvVar: "AICLI_API_KEY_CLAUDE",
    requiresApiKey: true,
    models: [
      {
        id: "claude-opus-4-6",
        displayName: "Claude Opus 4.6",
        contextWindow: 2e5,
        supportsStreaming: true
      },
      {
        id: "claude-sonnet-4-5-20250929",
        displayName: "Claude Sonnet 4.5",
        contextWindow: 2e5,
        supportsStreaming: true
      },
      {
        id: "claude-haiku-4-5-20251001",
        displayName: "Claude Haiku 4.5",
        contextWindow: 2e5,
        supportsStreaming: true
      }
    ]
  };
  async initialize(apiKey, options) {
    const clientOptions = {
      apiKey,
      baseURL: options?.baseUrl
    };
    const proxyUrl = options?.proxy;
    try {
      const { Agent, ProxyAgent, fetch: undiciFetch } = await import("undici");
      const STREAM_BODY_TIMEOUT = 30 * 60 * 1e3;
      const STREAM_HEADERS_TIMEOUT = 5 * 60 * 1e3;
      const dispatcher = proxyUrl ? new ProxyAgent({ uri: proxyUrl, bodyTimeout: STREAM_BODY_TIMEOUT, headersTimeout: STREAM_HEADERS_TIMEOUT }) : new Agent({ bodyTimeout: STREAM_BODY_TIMEOUT, headersTimeout: STREAM_HEADERS_TIMEOUT });
      clientOptions.fetch = ((url, init) => undiciFetch(url, { ...init, dispatcher }));
    } catch {
    }
    this.client = new Anthropic(clientOptions);
  }
  /**
   * 将内部 MessageContentPart[] 格式转换为 Anthropic SDK 期望的 ContentBlockParam[]。
   *
   * 内部格式（OpenAI 兼容）：{ type: 'image_url', image_url: { url: 'data:mime;base64,...' } }
   * Anthropic 格式：{ type: 'image', source: { type: 'base64', media_type: ..., data: ... } }
   *
   * 文本 part 和纯字符串保持不变。
   */
  contentToClaudeParts(content) {
    if (typeof content === "string") return content;
    const blocks = [];
    for (const part of content) {
      if (part.type === "text") {
        blocks.push({ type: "text", text: part.text });
      } else if (part.type === "image_url") {
        const match = part.image_url.url.match(/^data:([^;]+);base64,(.+)$/);
        if (match) {
          blocks.push({
            type: "image",
            source: {
              type: "base64",
              media_type: match[1],
              data: match[2]
            }
          });
        } else {
          process.stderr.write(
            `[warn] Skipped non-base64 image URL (Claude requires base64 data URIs).
`
          );
        }
      }
    }
    return blocks.length > 0 ? blocks : "";
  }
  /**
   * Build a cacheable system prompt payload.
   *
   * stable  — session-invariant content (guidelines, memory, context). Marked with
   *           cache_control: ephemeral when long enough, so Claude reuses the KV cache
   *           across every round in an agentic loop AND across turns within a session.
   * volatile — per-request content (date/time, working directory). Appended as a second
   *            text block WITHOUT cache_control so it never busts the stable cache.
   *
   * Short prompts (below the Anthropic minimum) pass through as a plain string.
   */
  buildSystemParam(stable, volatile) {
    const stableText = stable ?? "";
    const volatileText = volatile ?? "";
    if (!stableText && !volatileText) return void 0;
    if (stableText.length < CACHE_MIN_SYSTEM_CHARS) {
      const combined = [stableText, volatileText].filter(Boolean).join("\n\n---\n\n");
      return combined || void 0;
    }
    const blocks = [
      { type: "text", text: stableText, cache_control: { type: "ephemeral" } }
    ];
    if (volatileText) {
      blocks.push({ type: "text", text: volatileText });
    }
    return blocks;
  }
  /**
   * Mark the last tool definition with `cache_control: ephemeral` so the entire
   * tool block (all 24+ tools) is cached together. Anthropic caches everything
   * up to and including a cache breakpoint, so one marker covers all tools.
   * Returns a new array — does not mutate the input.
   */
  addToolsCacheControl(tools) {
    if (tools.length === 0) return tools;
    const last = tools[tools.length - 1];
    return [
      ...tools.slice(0, -1),
      { ...last, cache_control: { type: "ephemeral" } }
    ];
  }
  /** Extract usage (including cache fields) from an Anthropic response. */
  extractUsage(u) {
    const usage = {
      inputTokens: u.input_tokens,
      outputTokens: u.output_tokens
    };
    if (u.cache_creation_input_tokens != null && u.cache_creation_input_tokens > 0) {
      usage.cacheCreationTokens = u.cache_creation_input_tokens;
    }
    if (u.cache_read_input_tokens != null && u.cache_read_input_tokens > 0) {
      usage.cacheReadTokens = u.cache_read_input_tokens;
    }
    return usage;
  }
  /**
   * 构建 Extended Thinking 参数。
   * - thinking 启用时 temperature 必须为 1 或不设置（Anthropic API 要求）
   * - budget_tokens 最小 1024，默认 10000
   */
  buildThinkingParams(request) {
    if (!request.thinking) {
      return { temperature: request.temperature };
    }
    return {
      thinking: {
        type: "enabled",
        budget_tokens: Math.max(request.thinkingBudget ?? 1e4, 1024)
      },
      // thinking 启用时 temperature 必须不设置（API 强制 temperature=1）
      temperature: void 0
    };
  }
  /**
   * 从 response.content 中提取文本内容。
   * 当 thinking 启用时，将 ThinkingBlock 内容用 <think>...</think> 标签包裹，
   * 复用 renderer.ts 已有的折叠逻辑。
   */
  extractContent(blocks) {
    const thinkingParts = [];
    const textParts = [];
    for (const block of blocks) {
      if (block.type === "thinking") {
        thinkingParts.push(block.thinking);
      } else if (block.type === "text") {
        textParts.push(block.text);
      }
    }
    const mainText = textParts.join("");
    if (thinkingParts.length > 0) {
      const thinkingText = thinkingParts.join("\n");
      return `<think>${thinkingText}</think>${mainText}`;
    }
    return mainText;
  }
  async chat(request) {
    try {
      const messages = this.toClaudeMessagesWithToolHistory(request.messages);
      const { thinking, temperature } = this.buildThinkingParams(request);
      const response = await this.client.messages.create({
        model: request.model,
        messages,
        system: this.buildSystemParam(request.systemPrompt, request.systemPromptVolatile),
        max_tokens: request.maxTokens ?? 8192,
        temperature,
        thinking
      });
      const content = this.extractContent(response.content);
      return {
        content,
        model: response.model,
        usage: this.extractUsage(response.usage)
      };
    } catch (err) {
      throw this.wrapError(err);
    }
  }
  async *chatStream(request) {
    try {
      const messages = this.toClaudeMessagesWithToolHistory(request.messages);
      const { thinking, temperature } = this.buildThinkingParams(request);
      const stream = this.client.messages.stream({
        model: request.model,
        messages,
        system: this.buildSystemParam(request.systemPrompt, request.systemPromptVolatile),
        max_tokens: request.maxTokens ?? 8192,
        temperature,
        thinking
      }, { signal: request.signal });
      let currentBlockType = null;
      for await (const event of stream) {
        if (event.type === "content_block_start") {
          currentBlockType = event.content_block.type;
          if (currentBlockType === "thinking") {
            yield { delta: "<think>", done: false };
          }
        } else if (event.type === "content_block_delta") {
          if (event.delta.type === "text_delta") {
            yield { delta: event.delta.text, done: false };
          } else if (event.delta.type === "thinking_delta") {
            yield { delta: event.delta.thinking, done: false };
          }
        } else if (event.type === "content_block_stop") {
          if (currentBlockType === "thinking") {
            yield { delta: "</think>", done: false };
          }
          currentBlockType = null;
        } else if (event.type === "message_stop") {
          yield { delta: "", done: true };
        }
      }
    } catch (err) {
      throw this.wrapError(err);
    }
  }
  async chatWithTools(request, tools) {
    try {
      const anthropicTools = this.addToolsCacheControl(
        tools.map((t) => ({
          name: t.name,
          description: t.description,
          input_schema: {
            type: "object",
            properties: Object.fromEntries(
              Object.entries(t.parameters).map(([key, schema]) => [
                key,
                schemaToJsonSchema(schema)
              ])
            ),
            required: Object.entries(t.parameters).filter(([, s]) => s.required).map(([k]) => k)
          }
        }))
      );
      const baseMessages = this.toClaudeMessagesWithToolHistory(request.messages);
      const extraMessages = request._extraMessages ?? [];
      const allMessages = [...baseMessages, ...extraMessages];
      const { thinking, temperature } = this.buildThinkingParams(request);
      const response = await this.client.messages.create({
        model: request.model,
        messages: allMessages,
        tools: anthropicTools,
        system: this.buildSystemParam(request.systemPrompt, request.systemPromptVolatile),
        max_tokens: request.maxTokens ?? 8192,
        temperature,
        thinking
      });
      const usage = this.extractUsage(response.usage);
      const toolUseBlocks = response.content.filter(
        (b) => b.type === "tool_use"
      );
      if (toolUseBlocks.length > 0) {
        const toolCalls = toolUseBlocks.map((b) => ({
          id: b.id,
          name: b.name,
          arguments: b.input
        }));
        toolCalls._rawContent = response.content;
        return { toolCalls, usage };
      }
      const content = this.extractContent(response.content);
      return { content, usage };
    } catch (err) {
      throw this.wrapError(err);
    }
  }
  /**
   * 流式工具调用：文本/thinking 实时输出、工具名称/参数逐块发射。
   * 同时收集原始 content blocks 供 buildToolResultMessages 使用。
   */
  async *chatWithToolsStream(request, tools) {
    const anthropicTools = this.addToolsCacheControl(
      tools.map((t) => ({
        name: t.name,
        description: t.description,
        input_schema: {
          type: "object",
          properties: Object.fromEntries(
            Object.entries(t.parameters).map(([key, schema]) => [
              key,
              schemaToJsonSchema(schema)
            ])
          ),
          required: Object.entries(t.parameters).filter(([, s]) => s.required).map(([k]) => k)
        }
      }))
    );
    const baseMessages = this.toClaudeMessagesWithToolHistory(request.messages);
    const extraMessages = request._extraMessages ?? [];
    const allMessages = [...baseMessages, ...extraMessages];
    const { thinking, temperature } = this.buildThinkingParams(request);
    let doneEmitted = false;
    const rawContentBlocks = [];
    try {
      const stream = this.client.messages.stream({
        model: request.model,
        messages: allMessages,
        tools: anthropicTools,
        system: this.buildSystemParam(request.systemPrompt, request.systemPromptVolatile),
        max_tokens: request.maxTokens ?? 8192,
        temperature,
        thinking
      }, { signal: request.signal });
      let currentBlockType = null;
      let currentToolIndex = 0;
      let currentBlockData = {};
      let startUsage = null;
      for await (const event of stream) {
        if (event.type === "message_start") {
          const msgUsage = event.message?.usage;
          if (msgUsage) startUsage = msgUsage;
          continue;
        }
        if (event.type === "content_block_start") {
          const block = event.content_block;
          currentBlockType = block.type;
          currentBlockData = { type: block.type };
          if (block.type === "thinking") {
            yield { type: "thinking_start" };
            currentBlockData = { type: "thinking", thinking: "", signature: "" };
          } else if (block.type === "text") {
            currentBlockData = { type: "text", text: "" };
          } else if (block.type === "tool_use") {
            const tuBlock = block;
            yield {
              type: "tool_call_start",
              index: currentToolIndex,
              id: tuBlock.id,
              name: tuBlock.name
            };
            currentBlockData = { type: "tool_use", id: tuBlock.id, name: tuBlock.name, input: {}, _inputJson: "" };
          } else if (block.type === "redacted_thinking") {
            currentBlockData = { type: "redacted_thinking", data: block.data };
          }
        } else if (event.type === "content_block_delta") {
          if (event.delta.type === "text_delta") {
            yield { type: "text_delta", delta: event.delta.text };
            currentBlockData.text += event.delta.text;
          } else if (event.delta.type === "thinking_delta") {
            const thinkingDelta = event.delta.thinking;
            yield { type: "thinking_delta", delta: thinkingDelta };
            currentBlockData.thinking += thinkingDelta;
          } else if (event.delta.type === "input_json_delta") {
            const jsonDelta = event.delta.partial_json;
            currentBlockData._inputJson += jsonDelta;
            yield {
              type: "tool_call_delta",
              index: currentToolIndex,
              argumentsDelta: jsonDelta
            };
          } else if (event.delta.type === "signature_delta") {
            currentBlockData.signature += event.delta.signature ?? "";
          }
        } else if (event.type === "content_block_stop") {
          if (currentBlockType === "thinking") {
            yield { type: "thinking_end" };
          } else if (currentBlockType === "tool_use") {
            yield { type: "tool_call_end", index: currentToolIndex };
            currentToolIndex++;
          }
          if (currentBlockType === "tool_use" && currentBlockData._inputJson) {
            try {
              currentBlockData.input = JSON.parse(
                currentBlockData._inputJson
              );
            } catch {
            }
            delete currentBlockData._inputJson;
          }
          rawContentBlocks.push(currentBlockData);
          currentBlockType = null;
          currentBlockData = {};
        } else if (event.type === "message_delta") {
          const deltaUsage = event.usage;
          if (deltaUsage) {
            doneEmitted = true;
            yield {
              type: "done",
              usage: this.extractUsage({
                input_tokens: startUsage?.input_tokens ?? deltaUsage.input_tokens ?? 0,
                output_tokens: deltaUsage.output_tokens ?? 0,
                cache_creation_input_tokens: startUsage?.cache_creation_input_tokens,
                cache_read_input_tokens: startUsage?.cache_read_input_tokens
              }),
              rawContent: rawContentBlocks
            };
          }
        }
      }
    } catch (err) {
      if (err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError")) {
        throw err;
      }
      throw this.wrapError(err);
    }
    if (!doneEmitted) {
      yield { type: "done", usage: { inputTokens: 0, outputTokens: 0 }, rawContent: rawContentBlocks };
    }
  }
  /**
   * 将 Message[] 转换为 Anthropic.MessageParam[]，按原始顺序保留历史工具往返。
   *
   * v0.4.100+：之前的实现把 role='tool' 和 assistant.toolCalls 全部剥离后塞进
   * _extraMessages 的尾部，导致跨轮历史的工具调用被插入到当前用户消息之后，
   * 模型会复读上一轮的"完成汇总"。现在按原始顺序内联编码。
   *
   * 注意：历史持久化的 toolCalls 没有原始 thinking blocks（仅当前轮的内存里有），
   * 故只输出 tool_use 块作为 fallback；当前轮通过 buildToolResultMessages() 仍能
   * 携带 thinking 信息。
   */
  toClaudeMessagesWithToolHistory(messages) {
    const out = [];
    let i = 0;
    while (i < messages.length) {
      const m = messages[i];
      if (m.role === "system") {
        i++;
        continue;
      }
      if (m.role === "assistant" && m.toolCalls && m.toolCalls.length > 0) {
        const toolCalls = m.toolCalls;
        const assistantBlocks = [];
        if (typeof m.content === "string" && m.content.trim()) {
          assistantBlocks.push({ type: "text", text: m.content });
        }
        for (const tc of toolCalls) {
          assistantBlocks.push({
            type: "tool_use",
            id: tc.id,
            name: tc.name,
            input: tc.arguments
          });
        }
        out.push({ role: "assistant", content: assistantBlocks });
        const toolResults = [];
        let j = i + 1;
        while (j < messages.length && messages[j].role === "tool") {
          const tm = messages[j];
          if (tm.toolCallId) {
            toolResults.push({
              type: "tool_result",
              tool_use_id: tm.toolCallId,
              content: typeof tm.content === "string" ? tm.content : "",
              is_error: tm.isError ?? false
            });
          }
          j++;
        }
        if (toolResults.length > 0) {
          out.push({ role: "user", content: toolResults });
        }
        i = j;
        continue;
      }
      if (m.role === "tool") {
        i++;
        continue;
      }
      out.push({
        role: m.role,
        content: this.contentToClaudeParts(m.content)
      });
      i++;
    }
    return out;
  }
  buildToolResultMessages(assistantToolCalls, results) {
    const rawContent = assistantToolCalls._rawContent;
    let assistantContent;
    if (rawContent) {
      assistantContent = rawContent.filter((b) => b.type === "thinking" || b.type === "redacted_thinking" || b.type === "tool_use" || b.type === "text").map((b) => {
        if (b.type === "thinking") {
          return { type: "thinking", thinking: b.thinking, signature: b.signature };
        }
        if (b.type === "redacted_thinking") {
          return { type: "redacted_thinking", data: b.data };
        }
        if (b.type === "tool_use") {
          const tu = b;
          return { type: "tool_use", id: tu.id, name: tu.name, input: tu.input };
        }
        return { type: "text", text: b.text };
      });
    } else {
      assistantContent = assistantToolCalls.map((tc) => ({
        type: "tool_use",
        id: tc.id,
        name: tc.name,
        input: tc.arguments
      }));
    }
    const userContent = results.map((r) => ({
      type: "tool_result",
      tool_use_id: r.callId,
      content: r.content,
      is_error: r.isError
    }));
    return [
      { role: "assistant", content: assistantContent },
      { role: "user", content: userContent }
    ];
  }
  async validateApiKey(apiKey) {
    try {
      const testClient = new Anthropic({ apiKey });
      await testClient.models.list();
      return true;
    } catch {
      return false;
    }
  }
  async listModels() {
    return this.info.models;
  }
  wrapError(err) {
    if (err instanceof Anthropic.AuthenticationError) {
      return new AuthError("claude");
    }
    if (err instanceof Anthropic.RateLimitError) {
      return new RateLimitError("claude");
    }
    if (err instanceof Error) {
      return new ProviderError("claude", err.message, err);
    }
    return new ProviderError("claude", String(err));
  }
};

// src/providers/gemini.ts
import { GoogleGenerativeAI } from "@google/generative-ai";
var GeminiProvider = class extends BaseProvider {
  client;
  /** 自定义 base URL（来自 config.customBaseUrls.gemini），用于配置代理或镜像站 */
  baseUrl;
  info = {
    id: "gemini",
    displayName: "Gemini (Google)",
    defaultModel: "gemini-2.5-flash",
    apiKeyEnvVar: "AICLI_API_KEY_GEMINI",
    requiresApiKey: true,
    models: [
      {
        id: "gemini-2.5-pro",
        displayName: "Gemini 2.5 Pro",
        contextWindow: 1e6,
        supportsStreaming: true
      },
      {
        id: "gemini-2.5-flash",
        displayName: "Gemini 2.5 Flash",
        contextWindow: 1e6,
        supportsStreaming: true
      },
      {
        id: "gemini-2.0-flash",
        displayName: "Gemini 2.0 Flash",
        contextWindow: 1e6,
        supportsStreaming: true
      },
      {
        id: "gemini-2.0-flash-lite",
        displayName: "Gemini 2.0 Flash Lite",
        contextWindow: 1e6,
        supportsStreaming: true
      },
      {
        id: "gemini-1.5-pro",
        displayName: "Gemini 1.5 Pro",
        contextWindow: 2e6,
        supportsStreaming: true
      },
      {
        id: "gemini-1.5-flash",
        displayName: "Gemini 1.5 Flash",
        contextWindow: 1e6,
        supportsStreaming: true
      }
    ]
  };
  async initialize(apiKey, options) {
    this.baseUrl = options?.["baseUrl"];
    this.client = new GoogleGenerativeAI(apiKey);
  }
  /** 构建 RequestOptions，当配置了自定义 baseUrl 时注入 */
  get reqOpts() {
    return this.baseUrl ? { baseUrl: this.baseUrl } : void 0;
  }
  /**
   * 将内部 MessageContentPart[] 格式转换为 Gemini SDK 期望的 Part[]。
   *
   * 内部格式（OpenAI 兼容）：{ type: 'image_url', image_url: { url: 'data:mime;base64,...' } }
   * Gemini 格式：{ inlineData: { mimeType: 'image/png', data: 'base64...' } }
   *
   * 空字符串保留为 [{ text: '' }] 以避免 Gemini SDK 报空 parts 错误。
   */
  contentToGeminiParts(content) {
    if (typeof content === "string") {
      return [{ text: content }];
    }
    const parts = [];
    for (const p of content) {
      if (p.type === "text") {
        if (p.text.length > 0) parts.push({ text: p.text });
      } else if (p.type === "image_url") {
        const match = p.image_url.url.match(/^data:([^;]+);base64,(.+)$/);
        if (match) {
          parts.push({ inlineData: { mimeType: match[1], data: match[2] } });
        }
      }
    }
    return parts.length > 0 ? parts : [{ text: "" }];
  }
  /**
   * 将 Message[] 转 Gemini Content[]，按原始顺序保留历史工具往返。
   *
   * v0.4.100+：之前过滤掉 role='tool' 和 assistant.toolCalls 后再由 _extraMessages
   * 拼到末尾，导致跨轮历史的工具调用被插到当前用户消息之后，模型会复读上一轮汇总。
   */
  toGeminiHistory(messages) {
    const out = [];
    let i = 0;
    while (i < messages.length) {
      const m = messages[i];
      if (m.role === "system") {
        i++;
        continue;
      }
      if (m.role === "assistant" && m.toolCalls && m.toolCalls.length > 0) {
        const modelParts = [];
        if (typeof m.content === "string" && m.content.trim()) {
          modelParts.push({ text: m.content });
        }
        for (const tc of m.toolCalls) {
          modelParts.push({ functionCall: { name: tc.name, args: tc.arguments } });
        }
        out.push({ role: "model", parts: modelParts });
        const fnParts = [];
        let j = i + 1;
        while (j < messages.length && messages[j].role === "tool") {
          const tm = messages[j];
          fnParts.push({
            functionResponse: {
              name: tm.toolName ?? "unknown",
              response: {
                result: typeof tm.content === "string" ? tm.content : "",
                isError: tm.isError ?? false
              }
            }
          });
          j++;
        }
        if (fnParts.length > 0) {
          out.push({ role: "function", parts: fnParts });
        }
        i = j;
        continue;
      }
      if (m.role === "tool") {
        i++;
        continue;
      }
      out.push({
        role: m.role === "assistant" ? "model" : "user",
        parts: this.contentToGeminiParts(m.content)
      });
      i++;
    }
    return out;
  }
  async chat(request) {
    try {
      if (request.messages.length === 0) {
        return { content: "", model: request.model, usage: void 0 };
      }
      const genModel = this.client.getGenerativeModel({
        model: request.model,
        systemInstruction: [request.systemPrompt, request.systemPromptVolatile].filter(Boolean).join("\n\n---\n\n") || void 0
      }, this.reqOpts);
      const history = this.toGeminiHistory(request.messages.slice(0, -1));
      const lastMsgParts = this.contentToGeminiParts(
        request.messages[request.messages.length - 1].content
      );
      const chat = genModel.startChat({ history });
      const result = await chat.sendMessage(lastMsgParts);
      const meta = result.response.usageMetadata;
      return {
        content: result.response.text(),
        model: request.model,
        usage: meta ? { inputTokens: meta.promptTokenCount ?? 0, outputTokens: meta.candidatesTokenCount ?? 0 } : void 0
      };
    } catch (err) {
      throw this.wrapError(err);
    }
  }
  async *chatStream(request) {
    try {
      if (request.messages.length === 0) {
        yield { delta: "", done: true };
        return;
      }
      const genModel = this.client.getGenerativeModel({
        model: request.model,
        systemInstruction: [request.systemPrompt, request.systemPromptVolatile].filter(Boolean).join("\n\n---\n\n") || void 0
      }, this.reqOpts);
      const history = this.toGeminiHistory(request.messages.slice(0, -1));
      const lastMsgParts = this.contentToGeminiParts(
        request.messages[request.messages.length - 1].content
      );
      const chat = genModel.startChat({ history });
      const result = await chat.sendMessageStream(lastMsgParts);
      for await (const chunk of result.stream) {
        if (request.signal?.aborted) break;
        yield { delta: chunk.text(), done: false };
      }
      const finalResponse = await result.response;
      const meta = finalResponse.usageMetadata;
      if (meta) {
        yield {
          delta: "",
          done: false,
          usage: { inputTokens: meta.promptTokenCount ?? 0, outputTokens: meta.candidatesTokenCount ?? 0 }
        };
      }
      yield { delta: "", done: true };
    } catch (err) {
      throw this.wrapError(err);
    }
  }
  async chatWithTools(request, tools) {
    try {
      if (request.messages.length === 0) {
        return { content: "", usage: void 0 };
      }
      const geminiTools = [{
        functionDeclarations: tools.map((t) => ({
          name: t.name,
          description: t.description,
          parameters: {
            type: "object",
            properties: Object.fromEntries(
              Object.entries(t.parameters).map(([key, schema]) => [
                key,
                schemaToJsonSchema(schema)
              ])
            ),
            required: Object.entries(t.parameters).filter(([, s]) => s.required).map(([k]) => k)
          }
        }))
      }];
      const baseHistory = this.toGeminiHistory(request.messages.slice(0, -1));
      const lastMsgParts = this.contentToGeminiParts(
        request.messages[request.messages.length - 1].content
      );
      const extraHistory = request._extraMessages ?? [];
      let fullHistory;
      let msgToSend;
      if (extraHistory.length === 0) {
        fullHistory = [...baseHistory];
        msgToSend = lastMsgParts;
      } else {
        const lastFnResponse = extraHistory[extraHistory.length - 1];
        fullHistory = [
          ...baseHistory,
          { role: "user", parts: lastMsgParts },
          ...extraHistory.slice(0, -1)
        ];
        msgToSend = lastFnResponse.parts;
      }
      const genModel = this.client.getGenerativeModel({
        model: request.model,
        systemInstruction: [request.systemPrompt, request.systemPromptVolatile].filter(Boolean).join("\n\n---\n\n") || void 0
      }, this.reqOpts);
      const chat = genModel.startChat({ history: fullHistory, tools: geminiTools });
      const result = await chat.sendMessage(msgToSend);
      const response = result.response;
      const meta = response.usageMetadata;
      const usage = meta ? { inputTokens: meta.promptTokenCount ?? 0, outputTokens: meta.candidatesTokenCount ?? 0 } : void 0;
      const parts = response.candidates?.[0]?.content?.parts ?? [];
      const fnCalls = parts.filter((p) => !!p.functionCall).map((p) => p.functionCall);
      if (fnCalls.length > 0) {
        const toolCalls = fnCalls.map((fc, i) => ({
          id: `gemini-fn-${Date.now()}-${i}`,
          name: fc.name,
          arguments: fc.args
        }));
        return { toolCalls, usage };
      }
      return { content: response.text(), usage };
    } catch (err) {
      throw this.wrapError(err);
    }
  }
  buildToolResultMessages(assistantToolCalls, results) {
    const modelParts = assistantToolCalls.map((tc) => ({
      functionCall: { name: tc.name, args: tc.arguments }
    }));
    const fnParts = results.map((r, i) => ({
      functionResponse: {
        name: assistantToolCalls[i].name,
        response: { result: r.content, isError: r.isError }
      }
    }));
    return [
      { role: "model", parts: modelParts },
      { role: "function", parts: fnParts }
    ];
  }
  async validateApiKey(apiKey) {
    try {
      const testClient = new GoogleGenerativeAI(apiKey);
      const model = testClient.getGenerativeModel({ model: "gemini-2.5-flash" }, this.reqOpts);
      await model.generateContent("hi");
      return true;
    } catch {
      return false;
    }
  }
  async listModels() {
    return this.info.models;
  }
  wrapError(err) {
    if (err instanceof Error) {
      const msg = err.message;
      const statusMatch = msg.match(/\b(4\d{2}|5\d{2})\b/);
      const status = statusMatch ? Number(statusMatch[1]) : void 0;
      if (msg.includes("API key") || msg.includes("PERMISSION_DENIED") || status === 401 || status === 403) {
        return new AuthError("gemini");
      }
      if (status === 429 || msg.includes("RESOURCE_EXHAUSTED") || msg.includes("quota")) {
        return new ProviderError(
          "gemini",
          `Rate limit exceeded (429): The current model requires a paid plan or the free quota has been exhausted.
  \u2022 For free usage, switch to gemini-2.5-flash (use /model)
  \u2022 For paid plans, visit https://ai.dev/rate-limit to check your quota`,
          err
        );
      }
      if (msg.includes("fetch failed") || msg.includes("ECONNREFUSED") || msg.includes("ETIMEDOUT") || msg.includes("ENOTFOUND") || msg.includes("socket hang up")) {
        return new ProviderError(
          "gemini",
          `Network connection failed: ${msg}
Node.js does not automatically use system proxies. Try one of the following:
  1. Set environment variable: set HTTPS_PROXY=http://127.0.0.1:<proxy-port>
  2. Configure an accessible Gemini API mirror URL in config.json under customBaseUrls.gemini`,
          err
        );
      }
      return new ProviderError("gemini", msg, err);
    }
    return new ProviderError("gemini", String(err));
  }
};

// src/providers/openai-compatible.ts
import OpenAI from "openai";
function toUsage(u) {
  if (!u) return void 0;
  const cached = u.prompt_tokens_details?.cached_tokens ?? 0;
  const usage = {
    inputTokens: Math.max(0, u.prompt_tokens - cached),
    outputTokens: u.completion_tokens
  };
  if (cached > 0) usage.cacheReadTokens = cached;
  return usage;
}
var OpenAICompatibleProvider = class extends BaseProvider {
  client;
  defaultTimeout = 6e4;
  // ms
  /** 子类设为 false 可禁用流式工具调用（虚假声明检测需要完整响应） */
  enableStreamingToolCalls = true;
  async initialize(apiKey, options) {
    if (options?.timeout !== void 0) {
      this.defaultTimeout = options.timeout;
    }
    const clientOptions = {
      apiKey,
      baseURL: options?.baseUrl ?? this.defaultBaseUrl,
      timeout: this.defaultTimeout
    };
    const proxyUrl = options?.proxy;
    try {
      const { Agent, ProxyAgent, fetch: undiciFetch } = await import("undici");
      const STREAM_BODY_TIMEOUT = 30 * 60 * 1e3;
      const STREAM_HEADERS_TIMEOUT = 5 * 60 * 1e3;
      const dispatcher = proxyUrl ? new ProxyAgent({
        uri: proxyUrl,
        bodyTimeout: STREAM_BODY_TIMEOUT,
        headersTimeout: STREAM_HEADERS_TIMEOUT
      }) : new Agent({
        bodyTimeout: STREAM_BODY_TIMEOUT,
        headersTimeout: STREAM_HEADERS_TIMEOUT
      });
      clientOptions.fetch = ((url, init) => undiciFetch(url, { ...init, dispatcher }));
    } catch {
    }
    this.client = new OpenAI(clientOptions);
  }
  /**
   * 将 systemPrompt + messages 合并为 OpenAI messages 数组（system 消息放首位）。
   *
   * v0.4.100+：按原始顺序保留工具消息（assistant.toolCalls 和 role='tool'），
   * 不再剥离到 _extraMessages 末尾——之前的剥离会让历史工具往返被插到当前用户消息之后，
   * 导致模型把"过去的工具调用结果"当作"对当前问题的回应"，DeepSeek V4 Flash 上尤其明显
   * （会复读上一轮的"完成汇总"）。
   *
   * DeepSeek V4 thinking 模式：所有 assistant 消息（含带 toolCalls 的）必须有
   * reasoning_content 字段，缺失则 API 400。
   */
  buildMessages(request) {
    const msgs = [];
    for (const m of request.messages) {
      if (m.role === "tool") {
        if (!m.toolCallId) continue;
        msgs.push({
          role: "tool",
          tool_call_id: m.toolCallId,
          content: typeof m.content === "string" ? m.content : ""
        });
        continue;
      }
      if (m.role === "assistant" && m.toolCalls && m.toolCalls.length > 0) {
        const assistantMsg = {
          role: "assistant",
          content: typeof m.content === "string" && m.content ? m.content : null,
          tool_calls: m.toolCalls.map((tc) => ({
            id: tc.id,
            type: "function",
            function: { name: tc.name, arguments: JSON.stringify(tc.arguments) }
          })),
          reasoning_content: m.reasoningContent ?? ""
        };
        msgs.push(assistantMsg);
        continue;
      }
      const base = { role: m.role, content: m.content };
      if (m.role === "assistant") {
        base.reasoning_content = m.reasoningContent ?? "";
      }
      msgs.push(base);
    }
    const systemContent = [request.systemPrompt, request.systemPromptVolatile].filter(Boolean).join("\n\n---\n\n");
    if (systemContent) {
      return [{ role: "system", content: systemContent }, ...msgs];
    }
    return msgs;
  }
  async chat(request) {
    try {
      const response = await this.client.chat.completions.create({
        model: request.model,
        messages: this.buildMessages(request),
        temperature: request.temperature,
        max_tokens: request.maxTokens,
        stream: false,
        ...request.thinking ? { thinking: { type: "enabled" } } : {}
      }, {
        timeout: request.timeout ?? this.defaultTimeout
      });
      const firstChoice = response.choices?.[0];
      if (!firstChoice) {
        return { content: "", model: response.model, usage: void 0 };
      }
      return {
        content: firstChoice.message.content ?? "",
        model: response.model,
        usage: toUsage(response.usage)
      };
    } catch (err) {
      throw this.wrapError(err);
    }
  }
  async *chatStream(request) {
    try {
      const stream = await this.client.chat.completions.create({
        model: request.model,
        messages: this.buildMessages(request),
        temperature: request.temperature,
        max_tokens: request.maxTokens,
        stream: true,
        // 请求末尾 usage chunk，供 token 统计使用
        stream_options: { include_usage: true },
        ...request.thinking ? { thinking: { type: "enabled" } } : {}
      }, {
        timeout: request.timeout ?? this.defaultTimeout,
        signal: request.signal
      });
      let thinkingStarted = false;
      let reasoningAccumulator = "";
      for await (const chunk of stream) {
        const choice = chunk.choices[0];
        const done = choice?.finish_reason != null;
        if (!choice && chunk.usage) {
          yield {
            delta: "",
            done: true,
            usage: toUsage(chunk.usage),
            ...reasoningAccumulator ? { reasoningContent: reasoningAccumulator } : {}
          };
          continue;
        }
        const reasoningDelta = choice?.delta?.reasoning_content;
        if (reasoningDelta) {
          if (!thinkingStarted) {
            yield { delta: "<think>", done: false };
            thinkingStarted = true;
          }
          reasoningAccumulator += reasoningDelta;
          yield { delta: reasoningDelta, done: false };
          continue;
        }
        const delta = choice?.delta?.content ?? "";
        if (thinkingStarted && delta) {
          thinkingStarted = false;
          yield { delta: "</think>", done: false };
        }
        if (done) {
          yield { delta, done, ...reasoningAccumulator ? { reasoningContent: reasoningAccumulator } : {} };
        } else {
          yield { delta, done };
        }
      }
    } catch (err) {
      throw this.wrapError(err);
    }
  }
  /**
   * 请求 AI 并获取工具调用列表（不执行，只解析）。
   * 返回 { toolCalls, usage? } 时说明 AI 想要调用工具，
   * 返回 { content, usage? } 时说明 AI 给出了最终回答。
   */
  async chatWithTools(request, tools) {
    try {
      const openaiTools = tools.map((t) => ({
        type: "function",
        function: {
          name: t.name,
          description: t.description,
          parameters: {
            type: "object",
            properties: Object.fromEntries(
              Object.entries(t.parameters).map(([key, schema]) => [
                key,
                schemaToJsonSchema(schema)
              ])
            ),
            required: Object.entries(t.parameters).filter(([, s]) => s.required).map(([k]) => k)
          }
        }
      }));
      const baseMessages = this.buildMessages(request);
      const extraMessages = request._extraMessages ?? [];
      const allMessages = [...baseMessages, ...extraMessages];
      const response = await this.client.chat.completions.create({
        model: request.model,
        messages: allMessages,
        tools: openaiTools,
        tool_choice: "auto",
        temperature: request.temperature,
        max_tokens: request.maxTokens,
        stream: false,
        ...request.thinking ? { thinking: { type: "enabled" } } : {}
      }, {
        timeout: request.timeout ?? this.defaultTimeout
      });
      const firstChoice = response.choices?.[0];
      if (!firstChoice) {
        return { content: "", usage: void 0 };
      }
      const message = firstChoice.message;
      const finishReason = firstChoice.finish_reason;
      const usage = toUsage(response.usage);
      const contentStr = typeof message.content === "string" ? message.content : "";
      const hasToolCalls = !!(message.tool_calls && message.tool_calls.length > 0);
      const reasoningContent = message.reasoning_content;
      if (message.tool_calls && message.tool_calls.length > 0) {
        const toolCalls = message.tool_calls.map((tc) => {
          const rawArgs = tc.function.arguments || "{}";
          let parsedArgs;
          try {
            parsedArgs = JSON.parse(rawArgs);
          } catch {
            const truncated = rawArgs.trimEnd();
            const lastComma = truncated.lastIndexOf(",");
            const fixed = lastComma > 0 ? truncated.slice(0, lastComma) + "}" : truncated.slice(0, truncated.indexOf("{") + 1) + "}";
            try {
              parsedArgs = JSON.parse(fixed);
              process.stderr.write(
                `[warn] Tool call JSON was truncated and auto-repaired. Some parameters may be missing.
`
              );
            } catch {
              process.stderr.write(
                `[warn] Tool call JSON could not be parsed, using empty arguments.
`
              );
              parsedArgs = {};
            }
          }
          return {
            id: tc.id,
            name: tc.function.name,
            arguments: parsedArgs
          };
        });
        return { toolCalls, usage, reasoningContent };
      }
      return {
        content: message.content ?? "",
        usage,
        ...reasoningContent ? { reasoningContent } : {},
        ...!hasToolCalls && (finishReason ?? "") ? { finishReason } : {}
      };
    } catch (err) {
      throw this.wrapError(err);
    }
  }
  /**
   * 流式工具调用：文本内容实时输出、工具名称/参数逐块发射。
   * 子类（DeepSeek / Kimi）因虚假声明检测需要完整响应，故不继承此方法。
   */
  async *chatWithToolsStream(request, tools) {
    if (!this.enableStreamingToolCalls) {
      const result = await this.chatWithTools(request, tools);
      if ("toolCalls" in result) {
        for (let i = 0; i < result.toolCalls.length; i++) {
          const tc = result.toolCalls[i];
          yield { type: "tool_call_start", index: i, id: tc.id, name: tc.name };
          yield { type: "tool_call_delta", index: i, argumentsDelta: JSON.stringify(tc.arguments) };
          yield { type: "tool_call_end", index: i };
        }
      } else {
        yield { type: "text_delta", delta: result.content };
      }
      const rc = "reasoningContent" in result ? result.reasoningContent : void 0;
      const fr = "finishReason" in result ? result.finishReason : void 0;
      yield {
        type: "done",
        usage: result.usage,
        ...rc ? { reasoningContent: rc } : {},
        ...fr ? { finishReason: fr } : {}
      };
      return;
    }
    const openaiTools = tools.map((t) => ({
      type: "function",
      function: {
        name: t.name,
        description: t.description,
        parameters: {
          type: "object",
          properties: Object.fromEntries(
            Object.entries(t.parameters).map(([key, schema]) => [
              key,
              schemaToJsonSchema(schema)
            ])
          ),
          required: Object.entries(t.parameters).filter(([, s]) => s.required).map(([k]) => k)
        }
      }
    }));
    const baseMessages = this.buildMessages(request);
    const extraMessages = request._extraMessages ?? [];
    const allMessages = [...baseMessages, ...extraMessages];
    try {
      const stream = await this.client.chat.completions.create({
        model: request.model,
        messages: allMessages,
        tools: openaiTools,
        tool_choice: "auto",
        temperature: request.temperature,
        max_tokens: request.maxTokens,
        stream: true,
        stream_options: { include_usage: true },
        ...request.thinking ? { thinking: { type: "enabled" } } : {}
      }, {
        timeout: request.timeout ?? this.defaultTimeout,
        signal: request.signal
      });
      const toolCallAccumulators = /* @__PURE__ */ new Map();
      let toolCallsEnded = false;
      let thinkingStarted = false;
      let reasoningAccumulator = "";
      let lastFinishReason;
      let doneEmitted = false;
      for await (const chunk of stream) {
        const choice = chunk.choices[0];
        if (choice?.finish_reason) lastFinishReason = choice.finish_reason;
        if (!choice && chunk.usage) {
          if (thinkingStarted) {
            yield { type: "thinking_end" };
            thinkingStarted = false;
          }
          if (!toolCallsEnded && toolCallAccumulators.size > 0) {
            for (const [idx] of toolCallAccumulators) {
              yield { type: "tool_call_end", index: idx };
            }
            toolCallsEnded = true;
          }
          doneEmitted = true;
          yield {
            type: "done",
            usage: toUsage(chunk.usage),
            ...reasoningAccumulator ? { reasoningContent: reasoningAccumulator } : {},
            ...lastFinishReason && lastFinishReason !== "stop" ? { finishReason: lastFinishReason } : {}
          };
          continue;
        }
        if (!choice) continue;
        const delta = choice.delta;
        const reasoningDelta = delta?.reasoning_content;
        if (reasoningDelta) {
          if (!thinkingStarted) {
            yield { type: "thinking_start" };
            thinkingStarted = true;
          }
          reasoningAccumulator += reasoningDelta;
          yield { type: "thinking_delta", delta: reasoningDelta };
          continue;
        }
        if (thinkingStarted && (delta?.content || delta?.tool_calls)) {
          yield { type: "thinking_end" };
          thinkingStarted = false;
        }
        if (delta?.content) {
          yield { type: "text_delta", delta: delta.content };
        }
        if (delta?.tool_calls) {
          for (const tc of delta.tool_calls) {
            const idx = tc.index;
            const existing = toolCallAccumulators.get(idx);
            if (!existing && tc.id && tc.function?.name) {
              const initialArgs = tc.function.arguments ?? "";
              toolCallAccumulators.set(idx, {
                id: tc.id,
                name: tc.function.name,
                arguments: initialArgs
              });
              yield { type: "tool_call_start", index: idx, id: tc.id, name: tc.function.name };
              if (initialArgs) {
                yield { type: "tool_call_delta", index: idx, argumentsDelta: initialArgs };
              }
            } else if (existing && tc.function?.arguments) {
              existing.arguments += tc.function.arguments;
              yield { type: "tool_call_delta", index: idx, argumentsDelta: tc.function.arguments };
            }
          }
        }
        if (choice.finish_reason && !toolCallsEnded && toolCallAccumulators.size > 0) {
          for (const [idx] of toolCallAccumulators) {
            yield { type: "tool_call_end", index: idx };
          }
          toolCallsEnded = true;
        }
      }
      if (!doneEmitted) {
        if (thinkingStarted) {
          yield { type: "thinking_end" };
          thinkingStarted = false;
        }
        if (!toolCallsEnded && toolCallAccumulators.size > 0) {
          for (const [idx] of toolCallAccumulators) {
            yield { type: "tool_call_end", index: idx };
          }
        }
        yield {
          type: "done",
          ...reasoningAccumulator ? { reasoningContent: reasoningAccumulator } : {},
          ...lastFinishReason && lastFinishReason !== "stop" ? { finishReason: lastFinishReason } : {}
        };
      }
    } catch (err) {
      if (err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError")) {
        throw err;
      }
      throw this.wrapError(err);
    }
  }
  /**
   * 将工具结果作为 tool_call 消息追加，供下一轮使用
   */
  buildToolResultMessages(assistantToolCalls, results, reasoningContent) {
    const streamedText = assistantToolCalls._streamedText;
    const assistantMsg = {
      role: "assistant",
      content: streamedText || null,
      tool_calls: assistantToolCalls.map((tc) => ({
        id: tc.id,
        type: "function",
        function: {
          name: tc.name,
          arguments: JSON.stringify(tc.arguments)
        }
      }))
    };
    assistantMsg.reasoning_content = reasoningContent ?? "";
    const resultMsgs = results.map((r) => ({
      role: "tool",
      tool_call_id: r.callId,
      content: r.content
    }));
    return [assistantMsg, ...resultMsgs];
  }
  async validateApiKey(apiKey) {
    try {
      const testClient = new OpenAI({ apiKey, baseURL: this.defaultBaseUrl });
      await testClient.models.list();
      return true;
    } catch {
      return false;
    }
  }
  async listModels() {
    return this.info.models;
  }
  wrapError(err) {
    if (err instanceof OpenAI.AuthenticationError) {
      return new AuthError(this.info.id);
    }
    if (err instanceof OpenAI.RateLimitError) {
      return new RateLimitError(this.info.id);
    }
    if (err instanceof Error) {
      return new ProviderError(this.info.id, err.message, err);
    }
    return new ProviderError(this.info.id, String(err));
  }
};

// src/providers/deepseek.ts
var CODE_BLOCK_PATTERNS = [
  /```(?:sql|bash|shell|powershell|sh)\s*\n/i,
  /```\s*\n\s*(?:SELECT|INSERT|UPDATE|DELETE|psql)\b/i
];
function detectsCodeBlockPseudoCall(content) {
  return CODE_BLOCK_PATTERNS.some((pattern) => pattern.test(content));
}
var DEEPSEEK_CODE_BLOCK_CORRECTION = "You wrote a code block in your response text, but you did NOT actually execute it. Code blocks in text are NOT executed by the system. You MUST use the function calling API to invoke the appropriate tool (e.g., mcp__postgres__query for SQL queries, bash for shell commands). Please call the correct tool NOW to execute the query/command.";
var DEEPSEEK_ANTI_HALLUCINATION = `

[CRITICAL: Anti-Hallucination Enforcement \u2014 DeepSeek Specific]
You have a known tendency to claim files were "saved" or "created" without actually calling write_file. This is UNACCEPTABLE.
Rules you MUST follow:
- NEVER type file content into your response text. ALL file content goes through write_file tool calls ONLY.
- After calling write_file, do NOT describe the file content again in text \u2014 just confirm the tool call result.
- When generating multiple files: call write_file for file 1 \u2192 call write_file for file 2 \u2192 ... \u2192 THEN summarize.
- If you catch yourself writing markdown/code that should be a file, STOP and use write_file instead.
- The system will detect and reject phantom claims. Each failed detection wastes a round. Be honest.`;
var DeepSeekProvider = class extends OpenAICompatibleProvider {
  defaultBaseUrl = "https://api.deepseek.com/v1";
  /** 禁用流式工具调用，确保 chatWithTools 覆写（代码块检测）生效 */
  enableStreamingToolCalls = false;
  info = {
    id: "deepseek",
    displayName: "DeepSeek",
    defaultModel: "deepseek-v4-flash",
    apiKeyEnvVar: "AICLI_API_KEY_DEEPSEEK",
    requiresApiKey: true,
    baseUrl: this.defaultBaseUrl,
    models: [
      // ── V4 family (2026-04-23+)：1M context，支持 Thinking / Non-Thinking 双模式 ──
      {
        id: "deepseek-v4-pro",
        displayName: "DeepSeek V4 Pro (1.6T MoE, 49B active)",
        contextWindow: 1048576,
        supportsStreaming: true,
        supportsThinking: true
      },
      {
        id: "deepseek-v4-flash",
        displayName: "DeepSeek V4 Flash (284B MoE, 13B active)",
        contextWindow: 1048576,
        supportsStreaming: true,
        supportsThinking: true
      }
      // Legacy aliases (deepseek-chat / deepseek-reasoner) removed in v0.4.140 —
      // they retire 2026-07-24 and DeepSeek already routes them to V4 Flash server-side.
      // Pre-existing sessions were migrated to deepseek-v4-flash; pricing.ts keeps the
      // old ids for any historical cost calculations.
    ]
  };
  /**
   * 覆写 chatWithTools — 检测代码块伪工具调用并自动重试。
   *
   * DeepSeek 有时在 system prompt 较长（如技能注入）时，
   * 退化为输出 ```sql/```bash 代码块而不调用工具。
   * 检测到后注入纠正消息强制重试一次。
   */
  async chatWithTools(request, tools) {
    const enhancedRequest = {
      ...request,
      systemPrompt: (request.systemPrompt ?? "") + DEEPSEEK_ANTI_HALLUCINATION
    };
    const result = await super.chatWithTools(enhancedRequest, tools);
    const hasBashTool = tools.some((t) => t.name === "bash");
    const extraMsgs = request._extraMessages ?? [];
    const alreadyUsedTools = extraMsgs.some((m) => m?.role === "tool");
    if (hasBashTool && !alreadyUsedTools && "content" in result && result.content && detectsCodeBlockPseudoCall(result.content)) {
      process.stderr.write(
        `[deepseek] \u26A0 Detected code block pseudo-tool-call (DeepSeek wrote code in text instead of calling a tool). Forcing retry...
`
      );
      const retryRequest = {
        ...request,
        _extraMessages: [
          ...request._extraMessages ?? [],
          // DeepSeek V4 thinking 模式：assistant 消息必须含 reasoning_content（即使为空）
          { role: "assistant", content: result.content, reasoning_content: "" },
          { role: "user", content: DEEPSEEK_CODE_BLOCK_CORRECTION }
        ]
      };
      const retryResult = await super.chatWithTools(retryRequest, tools);
      if (result.usage && "usage" in retryResult && retryResult.usage) {
        retryResult.usage = {
          inputTokens: result.usage.inputTokens + retryResult.usage.inputTokens,
          outputTokens: result.usage.outputTokens + retryResult.usage.outputTokens
        };
      }
      return retryResult;
    }
    return result;
  }
};

// src/providers/zhipu.ts
var ZhipuProvider = class extends OpenAICompatibleProvider {
  defaultBaseUrl = "https://open.bigmodel.cn/api/paas/v4";
  // GLM-5 / GLM-5.1 等深度思考模型生成长内容需要较长时间，默认 5 分钟
  defaultTimeout = 3e5;
  info = {
    id: "zhipu",
    displayName: "Zhipu (GLM)",
    // 默认选 GLM-4.6：中文写作口碑最稳 + 200K 上下文 + 价格只有 5.1 的 ~1/2。
    // 需要 Agent 长跑 / 代码工程时再手动 /model glm-5.1。
    defaultModel: "glm-4.6",
    apiKeyEnvVar: "AICLI_API_KEY_ZHIPU",
    requiresApiKey: true,
    baseUrl: this.defaultBaseUrl,
    models: [
      // ── GLM-5.1 系列（2026-04 旗舰，主打长程 Agent + 代码工程） ──
      {
        id: "glm-5.1",
        displayName: "GLM-5.1 (2026 Flagship, 200K, Agent+Code)",
        contextWindow: 204800,
        supportsStreaming: true,
        supportsThinking: true
      },
      {
        id: "glm-5.1-reasoning",
        displayName: "GLM-5.1 Reasoning (Deep Thinking)",
        contextWindow: 204800,
        supportsStreaming: true,
        supportsThinking: true
      },
      {
        id: "glm-5.1-air",
        displayName: "GLM-5.1 Air (Lightweight 5.1)",
        contextWindow: 204800,
        supportsStreaming: true,
        supportsThinking: true
      },
      // ── GLM-5 系列（2026-02） ──
      {
        id: "glm-5",
        displayName: "GLM-5 (Flagship, Deep Thinking)",
        contextWindow: 131072,
        supportsStreaming: true,
        supportsThinking: true
      },
      // ── GLM-4.6 系列（2025-09，中文写作口碑最佳） ──
      {
        id: "glm-4.6",
        displayName: "GLM-4.6 (200K, \u4E2D\u6587\u5199\u4F5C\u63A8\u8350)",
        contextWindow: 204800,
        supportsStreaming: true
      },
      {
        id: "glm-4.6v",
        displayName: "GLM-4.6V (Vision + Thinking)",
        contextWindow: 131072,
        supportsStreaming: true,
        supportsThinking: true
      },
      // ── GLM-Z1 推理系列 ──
      {
        id: "glm-z1",
        displayName: "GLM-Z1 (Reasoning Flagship)",
        contextWindow: 131072,
        supportsStreaming: true,
        supportsThinking: true
      },
      {
        id: "glm-z1-air",
        displayName: "GLM-Z1 Air (Lightweight Reasoning)",
        contextWindow: 131072,
        supportsStreaming: true,
        supportsThinking: true
      },
      {
        id: "glm-z1-flash",
        displayName: "GLM-Z1 Flash (Free Reasoning)",
        contextWindow: 128e3,
        supportsStreaming: true,
        supportsThinking: true
      },
      // ── GLM-4 系列（稳定，价格低） ──
      {
        id: "glm-4-plus",
        displayName: "GLM-4 Plus",
        contextWindow: 128e3,
        supportsStreaming: true
      },
      {
        id: "glm-4-air",
        displayName: "GLM-4 Air",
        contextWindow: 128e3,
        supportsStreaming: true
      },
      {
        id: "glm-4-flash",
        displayName: "GLM-4 Flash (Free)",
        contextWindow: 128e3,
        supportsStreaming: true
      }
    ]
  };
};

// src/tools/hallucination.ts
var HALLUCINATION_PATTERNS = [
  /文件路径[：:]\s*`?[^\s`]+\.\w{1,5}/,
  // 文件路径: `path/to/file.ext`（要求文件扩展名）
  /已生成[：:!！]/,
  // 已生成完成！
  /已保存到?\s*[`'"]/,
  // 已保存到 `path`（要求后跟路径引号）
  /已写入[：:!！]/,
  // 已写入！
  /已创建[：:!！]/,
  // 已创建！
  /File\s+(?:written|saved|created)\s+(?:to|as|at)/i,
  // File written to / saved as（要求介词）
  /生成完成[！!]/,
  // 生成完成！
  /✅\s*(?:文件|已[生保写创]|第)\S*\.\w{1,5}/,
  // ✅ 文件已保存 path.ext（要求文件扩展名）
  /文件已[成功]?创建/,
  // 文件已成功创建 / 文件已创建
  /教案已[成功]?[生创保写]/,
  // 教案已成功生成 / 教案已保存
  /已成功[保写创生]入?[：:!！\s`'"]/,
  // 已成功保存 / 已成功写入 / 已成功创建
  /保存[到至]了?\s*[`'"]/,
  // 保存到了 `path` / 保存至 'path'
  /内容如下[：:]/,
  // 内容如下：（后跟大段文件内容）
  /以下是.*(?:教案|文件|内容)[：:]/
  // 以下是xx教案内容：（Kimi 常见模式）
];
function detectsHallucinatedFileOp(content) {
  return HALLUCINATION_PATTERNS.some((pattern) => pattern.test(content));
}
function hadPreviousWriteToolCalls(extraMessages) {
  const msgs = extraMessages;
  return msgs.some((msg) => {
    if (msg.role === "assistant" && Array.isArray(msg.tool_calls)) {
      return msg.tool_calls.some((tc) => {
        const fn = tc.function;
        const name = fn?.name ?? "";
        return name === "write_file" || name === "edit_file";
      });
    }
    if (msg.role === "assistant" && Array.isArray(msg.content)) {
      return msg.content.some((block) => {
        if (block.type !== "tool_use") return false;
        const name = block.name ?? "";
        return name === "write_file" || name === "edit_file";
      });
    }
    if (msg.role === "model" && Array.isArray(msg.parts)) {
      return msg.parts.some((part) => {
        const fc = part.functionCall;
        const name = fc?.name ?? "";
        return name === "write_file" || name === "edit_file";
      });
    }
    return false;
  });
}
var TOOL_CALL_REMINDER = `

[\u26A0\uFE0F Mandatory Tool Call Policy]
When you need to create, write, or modify files, you MUST use the function calling API to invoke write_file or edit_file.
NEVER claim "file saved", "file created", "written to", etc. in your response text without actually calling the tool.
Describing file content in text without calling the tool = the file does not exist = task failure.
If multiple files need to be generated, you MUST call write_file separately for each file \u2014 do not skip any.
Do NOT output fake "completion summaries" unless you have actually completed all file writes via tool_calls.

CRITICAL \u2014 Batch file generation rules:
1. You MUST call write_file once per file. There are NO shortcuts.
2. After writing file N, immediately proceed to call write_file for file N+1. Do NOT stop to summarize.
3. If you find yourself typing file content into your response text instead of into a write_file call, STOP and use the tool.
4. Only produce a text summary AFTER all write_file calls have been made and returned success.
5. The system compares every "file saved" claim against actual tool calls. Phantom claims trigger an automatic retry \u2014 do not waste rounds.`;
function buildWriteRoundReminder(writtenCount) {
  return `

[Write Progress Reminder]
You have successfully called write_file ${writtenCount} time(s) so far in this turn. If there are more files to write, call write_file NOW for the next file. Do NOT produce a text summary until ALL files have been written via tool calls.`;
}
var HALLUCINATION_CORRECTION_MESSAGE = "You did NOT actually call the write_file tool \u2014 the file was NOT created! Please immediately use the write_file tool via the function calling API to perform the actual file write. Do NOT describe file content in text \u2014 you MUST invoke write_file through the tool_calls mechanism.";
function extractClaimedFilePaths(content) {
  const paths = /* @__PURE__ */ new Set();
  const add = (p) => {
    const trimmed = p.trim().replace(/[，,。、；;:：]+$/, "");
    if (trimmed && /\.\w{1,6}$/.test(trimmed)) paths.add(trimmed);
  };
  let m;
  const actionLineRe = /(?:已[生保写创]|saved|written|created|完成.*(?:写入|保存|创建|生成)|输出|file\s+(?:saved|written|created))/i;
  const backtickRe = /`([^`\n]+?\.\w{1,6})`/g;
  while ((m = backtickRe.exec(content)) !== null) {
    let pos = m.index;
    let linesBack = 0;
    while (linesBack < 9 && pos > 0) {
      pos--;
      if (content[pos] === "\n") linesBack++;
    }
    const windowStart = pos === 0 ? 0 : pos + 1;
    const lineEndIdx = content.indexOf("\n", m.index + m[0].length);
    const window = content.slice(windowStart, lineEndIdx === -1 ? void 0 : lineEndIdx);
    if (actionLineRe.test(window)) add(m[1]);
  }
  const zhRe = /(?:已保存(?:到)?|已写入(?:到)?|已创建|已生成|文件路径[：:]|保存为|写入到)\s*[`'”””]?([^\s`'”””，,。\n]+?\.\w{1,6})/g;
  while ((m = zhRe.exec(content)) !== null) add(m[1]);
  const enRe = /(?:saved|written|created)\s+(?:to|as|at)\s+[`'”]?([^\s`'”\n,]+?\.\w{1,6})/gi;
  while ((m = enRe.exec(content)) !== null) add(m[1]);
  const checkRe = /✅[^\n`]*?[`'”]?([^\s`'”\n,，。]+?\.\w{1,6})/g;
  while ((m = checkRe.exec(content)) !== null) {
    let pos = m.index;
    let linesBack = 0;
    while (linesBack < 9 && pos > 0) {
      pos--;
      if (content[pos] === "\n") linesBack++;
    }
    const windowStart = pos === 0 ? 0 : pos + 1;
    const lineEndIdx = content.indexOf("\n", m.index + m[0].length);
    const window = content.slice(windowStart, lineEndIdx === -1 ? void 0 : lineEndIdx);
    if (actionLineRe.test(window)) add(m[1]);
  }
  return Array.from(paths);
}
function extractWrittenFilePaths(extraMessages) {
  const paths = /* @__PURE__ */ new Set();
  const msgs = extraMessages;
  const addFromArgs = (raw) => {
    if (typeof raw === "string") {
      try {
        const parsed = JSON.parse(raw);
        if (typeof parsed.path === "string") paths.add(parsed.path);
      } catch {
      }
    } else if (raw && typeof raw === "object") {
      const p = raw.path;
      if (typeof p === "string") paths.add(p);
    }
  };
  for (const msg of msgs) {
    if (msg.role === "assistant" && Array.isArray(msg.tool_calls)) {
      for (const tc of msg.tool_calls) {
        const fn = tc.function;
        const name = fn?.name ?? "";
        if (name === "write_file" || name === "edit_file") {
          addFromArgs(fn?.arguments);
        }
      }
    }
    if (msg.role === "assistant" && Array.isArray(msg.content)) {
      for (const block of msg.content) {
        if (block.type !== "tool_use") continue;
        const name = block.name ?? "";
        if (name === "write_file" || name === "edit_file") {
          addFromArgs(block.input);
        }
      }
    }
    if (msg.role === "model" && Array.isArray(msg.parts)) {
      for (const part of msg.parts) {
        const fc = part.functionCall;
        if (!fc) continue;
        const name = fc.name ?? "";
        if (name === "write_file" || name === "edit_file") {
          addFromArgs(fc.args);
        }
      }
    }
  }
  return Array.from(paths);
}
function extractBashCommands(extraMessages) {
  const cmds = [];
  const msgs = extraMessages;
  const addCmd = (raw) => {
    if (typeof raw === "string") {
      try {
        const parsed = JSON.parse(raw);
        if (typeof parsed.command === "string") cmds.push(parsed.command);
      } catch {
      }
    } else if (raw && typeof raw === "object") {
      const c = raw.command;
      if (typeof c === "string") cmds.push(c);
    }
  };
  for (const msg of msgs) {
    if (msg.role === "assistant" && Array.isArray(msg.tool_calls)) {
      for (const tc of msg.tool_calls) {
        const fn = tc.function;
        if (fn?.name === "bash") addCmd(fn?.arguments);
      }
    }
    if (msg.role === "assistant" && Array.isArray(msg.content)) {
      for (const block of msg.content) {
        if (block.type === "tool_use" && block.name === "bash") addCmd(block.input);
      }
    }
    if (msg.role === "model" && Array.isArray(msg.parts)) {
      for (const part of msg.parts) {
        const fc = part.functionCall;
        if (fc && fc.name === "bash") addCmd(fc.args);
      }
    }
  }
  return cmds;
}
function findPhantomClaims(content, extraMessages) {
  const claimed = extractClaimedFilePaths(content);
  if (claimed.length === 0) return [];
  const normalize = (p) => p.replace(/\\/g, "/").toLowerCase().replace(/^\.\//, "");
  const basename = (p) => {
    const parts = normalize(p).split("/");
    return parts[parts.length - 1] ?? "";
  };
  const written = extractWrittenFilePaths(extraMessages).map(normalize);
  const writtenBases = new Set(written.map(basename));
  const writtenFull = new Set(written);
  const bashText = extractBashCommands(extraMessages).map((c) => c.replace(/\\/g, "/").toLowerCase()).join("\n");
  return claimed.filter((raw) => {
    const norm = normalize(raw);
    if (writtenFull.has(norm)) return false;
    for (const w of writtenFull) {
      if (w.endsWith("/" + norm) || norm.endsWith("/" + w)) return false;
    }
    if (writtenBases.has(basename(norm))) return false;
    const base = basename(norm);
    if (base && bashText.includes(base)) return false;
    return true;
  });
}
function buildPhantomCorrectionMessage(phantoms) {
  const list = phantoms.map((p) => `  - ${p}`).join("\n");
  return "You claimed to have written the following file(s), but no matching write_file tool call was actually made in this turn:\n" + list + '\n\nEach of these files does NOT exist on disk. You MUST now invoke write_file (via the function calling API) for every missing file listed above. Do NOT output another "completion summary" until the tool calls have actually been made.';
}
var DSML_PIPE_CLASS = "[|\\uFF5C\\u2502\\u2503\\u01C0]";
var PSEUDO_TOOL_CALL_PATTERNS = [
  // <tool_call name="..."> ... </tool_call>  (DeepSeek V4 thinking, GLM)
  /<tool_call\s+name\s*=\s*["'][\w._-]+["']/,
  // <function_calls> ... </function_calls>  (Anthropic-style as text)
  /<\/?function_calls\s*>/,
  // <invoke name="..." />  (Anthropic XML tool-call, which is real for
  // Claude API but is text/garbage for any other provider's plain stream)
  /<invoke\s+name\s*=\s*["'][\w._-]+["']/,
  // <tool_use> ... <tool_use_id>  (Claude flavor leaked into text)
  /<tool_use(?:_id)?\b/,
  // ```tool_call\n...\n``` markdown fences (Kimi/Zhipu fallback)
  /```\s*tool_call\b/i,
  // Bare JSON tool-call block: lines starting with `{"name":"...","arguments":`
  /^\s*\{\s*"name"\s*:\s*"[\w._-]+"\s*,\s*"arguments"\s*:/m,
  // v0.4.112: <think> ... </think> reasoning blocks. The REPL renderer
  // suppresses these from terminal output, but tee mode writes the raw
  // delta to disk → reasoning leaks into the saved file. We saw a 600-line
  // 审计报告.md whose first 57 lines were the model's planning monologue.
  /<think\b[^>]*>/i,
  // v0.4.112: leading ```markdown / ```md fence wrapping the entire document.
  // DeepSeek V4 Pro Thinking sometimes "politely" wraps its document output
  // in a markdown fence. The fence ends up literally in the saved file.
  /^\s*```\s*(?:markdown|md|gfm)\b/im,
  // v0.4.173: DeepSeek V4 DSML pseudo-tool-call markup leaked as text. DeepSeek
  // emits a fake tool call using its native special-token markup
  //   <｜｜DSML｜｜tool_calls> <｜｜DSML｜｜invoke name="write"> <｜｜DSML｜｜parameter …>
  // where the "pipe" is U+FF5C FULLWIDTH VERTICAL LINE (the same token family as
  // <｜User｜>/<｜Assistant｜>). We saw an exam paper saved via save_last_response
  // whose tee stream was preamble + this DSML wrapper + the real document body.
  // The earlier <invoke …> pattern uses ASCII < > and does NOT match these.
  new RegExp(`<\\/?\\s*${DSML_PIPE_CLASS}+\\s*DSML\\s*${DSML_PIPE_CLASS}+`, "i")
];
function detectPseudoToolCalls(content) {
  if (!content || content.length === 0) return null;
  for (const re of PSEUDO_TOOL_CALL_PATTERNS) {
    if (re.test(content)) return re.source;
  }
  return null;
}
function stripPseudoToolCalls(content) {
  if (!content) return content;
  let out = content;
  const dsmlBody = extractDsmlContent(out);
  if (dsmlBody !== null) {
    out = dsmlBody;
  } else {
    out = stripDsmlTags(out);
  }
  out = out.replace(/<tool_call\b[^>]*>[\s\S]*?<\/tool_call>/gi, "");
  out = out.replace(/<tool_call\b[^>]*\/>/gi, "");
  out = out.replace(/<function_calls\b[^>]*>[\s\S]*?<\/function_calls>/gi, "");
  out = out.replace(/<invoke\b[^>]*>[\s\S]*?<\/invoke>/gi, "");
  out = out.replace(/<invoke\b[^>]*\/>/gi, "");
  out = out.replace(/<tool_use(?:_id)?\b[^>]*>[\s\S]*?<\/tool_use(?:_id)?>/gi, "");
  out = out.replace(/```\s*tool_call\b[\s\S]*?```/gi, "");
  out = out.replace(/<think\b[^>]*>[\s\S]*?<\/think>/gi, "");
  out = out.replace(/<think\b[^>]*>[\s\S]*?(?=^#{1,3}\s+\S|\n\s*\n)/im, "");
  out = out.replace(/^\s*\{\s*"name"\s*:\s*"[\w._-]+"\s*,\s*"arguments"\s*:[\s\S]*?\}\s*$/gm, "");
  out = unwrapDocumentFence(out);
  out = peelMetaNarration(out);
  out = out.replace(/\n{3,}/g, "\n\n").trim();
  return out;
}
function extractDsmlContent(content) {
  if (!content) return null;
  const P = DSML_PIPE_CLASS;
  const re = new RegExp(
    `<\\s*${P}+\\s*DSML\\s*${P}+\\s*parameter\\b[^>]*\\bname\\s*=\\s*["']content["'][^>]*>([\\s\\S]*?)<\\s*/\\s*${P}+\\s*DSML\\s*${P}+\\s*parameter\\s*>`,
    "i"
  );
  const m = content.match(re);
  if (m && typeof m[1] === "string") {
    const body = m[1].trim();
    return body.length > 0 ? body : null;
  }
  return null;
}
function stripDsmlTags(content) {
  const P = DSML_PIPE_CLASS;
  let out = content;
  out = out.replace(
    new RegExp(
      `<\\s*${P}+\\s*DSML\\s*${P}+\\s*tool_calls\\b[\\s\\S]*?<\\s*/\\s*${P}+\\s*DSML\\s*${P}+\\s*tool_calls\\s*>`,
      "gi"
    ),
    ""
  );
  out = out.replace(new RegExp(`<\\s*/?\\s*${P}+\\s*DSML\\s*${P}+[^>]*>`, "gi"), "");
  return out;
}
function unwrapDocumentFence(content) {
  const trimmed = content.trim();
  const open = trimmed.match(/^```\s*(markdown|md|gfm)?\s*\n/i);
  if (!open) return content;
  const afterOpen = trimmed.slice(open[0].length);
  const closeMatch = afterOpen.match(/\n```\s*$/);
  if (!closeMatch) return content;
  const inner = afterOpen.slice(0, afterOpen.length - closeMatch[0].length);
  if (inner.length < 200) return content;
  return inner;
}
function peelMetaNarration(content) {
  let out = content;
  const firstHeadingMatch = out.match(/^#{1,3}\s+\S.*$/m);
  if (firstHeadingMatch && firstHeadingMatch.index !== void 0) {
    const before = out.slice(0, firstHeadingMatch.index);
    const hasIntroMarker = /(?:以下(?:即为|是|就是)|这是|Here\s+is|Below\s+is|完整的?(?:审计报告|内容|文档)|审计报告(?:如下|的完整内容))/i.test(before);
    if (before.length > 0 && before.length < 800 && hasIntroMarker) {
      out = out.slice(firstHeadingMatch.index);
    }
    if (out.startsWith("---\n")) {
      const headingAfterRule = out.slice(4).match(/^#{1,3}\s+\S/m);
      if (headingAfterRule && headingAfterRule.index !== void 0 && headingAfterRule.index < 100) {
        out = out.slice(4 + headingAfterRule.index);
      }
    }
  }
  const codaMatch = out.match(/\n[^\n]*?(?:以上(?:即为|就是|内容|为完整的?)|Above\s+is\s+the|本报告已经|该报告(?:已经|包含)|报告(?:已|至此)结束)[^\n]*$/i);
  if (codaMatch && codaMatch.index !== void 0 && codaMatch.index > out.length / 2) {
    out = out.slice(0, codaMatch.index);
  }
  return out.trim();
}
var META_NARRATION_HARD_MARKERS = [
  /\[⚠️\s*CONTENT GENERATION MODE\]/,
  /CONTENT_ONLY_STREAM_REMINDER\b/,
  /<system-reminder>/i
];
var META_NARRATION_HEURISTICS = [
  /\bthe user (?:is asking me|wants me|is requesting|expects me)\b/i,
  /\blet me (?:re-?read|re-?consider|reconsider|think about|carefully (?:re-?read|consider))\b/i,
  /\bI'?m (?:in (?:a )?content-only|in CONTENT-ONLY|currently in)\b/i,
  /\bI think (?:there might be|I should|I cannot|the (?:user|best)|maybe)\b/i,
  /\bWait,?\s+let me\b/i,
  /\bActually,?\s+I\b/i,
  /\bI need to be honest with the user\b/i,
  /\bI(?:'m| am) in a special mode\b/i,
  /\bGiven that I cannot\b/i
];
function detectMetaNarration(content) {
  if (!content) return null;
  const head = content.slice(0, 2e3);
  for (const re of META_NARRATION_HARD_MARKERS) {
    if (re.test(head)) return re.source;
  }
  if (/^#{1,3}\s+\S/m.test(head)) return null;
  let hits = 0;
  let firstMatch = "";
  for (const re of META_NARRATION_HEURISTICS) {
    if (re.test(head)) {
      hits++;
      if (!firstMatch) firstMatch = re.source;
      if (hits >= 2) return `meta-narration:${firstMatch}`;
    }
  }
  return null;
}
function looksLikeDocumentBody(content) {
  if (!content || content.length < 200) return false;
  if (/^#{1,6}\s+\S/m.test(content)) return true;
  const paragraphs = content.split(/\n\s*\n/).filter((p) => p.trim().length > 30);
  if (paragraphs.length >= 3) return true;
  return false;
}
function stripToolCallReminder(systemPrompt) {
  if (!systemPrompt) return systemPrompt;
  const idx = systemPrompt.indexOf("[\u26A0\uFE0F Mandatory Tool Call Policy]");
  if (idx === -1) return systemPrompt;
  return systemPrompt.slice(0, idx).trimEnd();
}
var TEE_FINAL_USER_NUDGE = `\u26A0\uFE0F STOP using tools NOW. The save_last_response tee stream is open and capturing every token of THIS response. Output ONLY the requested document body, in markdown. The very first character of your response must be the document's top-level heading (e.g. "# \u5BA1\u8BA1\u62A5\u544A" / "# Audit Report"). Do NOT print <tool_call>, </tool_call>, <function_calls>, <invoke>, <tool_use>, <think>, or any other tool-call markup. Do NOT narrate that you will produce the document \u2014 just produce it. Do NOT pretend to call tools \u2014 there are none in this stream.`;
var CONTENT_ONLY_STREAM_REMINDER = `

[\u26A0\uFE0F CONTENT GENERATION MODE]
You are now in a CONTENT-ONLY streaming pass. The file at the configured path will receive every token of THIS response.
- Do NOT emit <tool_call>, </tool_call>, <function_calls>, <invoke>, <tool_use>, or any tool-call XML/JSON markup.
- Do NOT print "I will now call ...", "let me read ...", "<think>" reasoning blocks (the surrounding REPL handles those separately \u2014 they should not enter the saved file).
- Do NOT pretend to call tools. There are NO tools available in this stream \u2014 only your text output is captured.
- Produce ONLY the requested document body. Markdown is fine. Code blocks are fine. Tool-call markup is NOT.
- If you accidentally start a <tool_call>, STOP and produce the document body instead.

The file is closed and named when this stream ends. If your output contains pseudo-tool-call markup, the save will be REJECTED and you will be asked to retry.`;

// src/providers/kimi.ts
var KIMI_XML_REMINDER = `

[IMPORTANT: Tool Call Format Rules - Kimi Specific]
When calling any tool (write_file, bash, read_file, etc.), you MUST use the API's structured function calling interface exclusively.
NEVER output XML-formatted pseudo tool call tags (such as <write_file>, <bash>, <read_file>, etc.) in your reply text.
XML tags in text will NOT be executed by the system, resulting in files not being written, commands not being run, and complete task failure.

[CRITICAL: Anti-Hallucination Enforcement]
You have a known tendency to claim files were "saved" or "created" without actually calling write_file. This is UNACCEPTABLE.
Rules you MUST follow:
- NEVER type file content into your response text. ALL file content goes through write_file tool calls ONLY.
- After calling write_file, do NOT describe the file content again in text \u2014 just confirm the tool call result.
- When generating multiple files: call write_file for file 1 \u2192 call write_file for file 2 \u2192 ... \u2192 THEN summarize.
- If you catch yourself writing markdown/code that should be a file, STOP and use write_file instead.
- The system will detect and reject phantom claims. Each failed detection wastes a round. Be honest.`;
var KimiProvider = class _KimiProvider extends OpenAICompatibleProvider {
  defaultBaseUrl = "https://api.moonshot.ai/v1";
  // 禁用流式工具调用：Kimi 的 XML 伪调用检测（方案 A）需要完整响应
  enableStreamingToolCalls = false;
  /**
   * Kimi K2.x 系列（非 moonshot-v1）只允许 temperature=1。
   * 其他值会收到 400 "invalid temperature: only 1 is allowed for this model"。
   */
  static TEMP_1_MODELS = /* @__PURE__ */ new Set([
    "kimi-k2.6",
    "kimi-k2.5",
    "kimi-k2-thinking",
    "kimi-k2-thinking-turbo",
    "kimi-k2-0905-preview"
  ]);
  /** 对需要固定 temperature=1 的模型自动修正请求。 */
  fixTemp(req) {
    if (_KimiProvider.TEMP_1_MODELS.has(req.model) && req.temperature !== 1) {
      return { ...req, temperature: 1 };
    }
    return req;
  }
  async chat(request) {
    return super.chat(this.fixTemp(request));
  }
  async *chatStream(request) {
    yield* super.chatStream(this.fixTemp(request));
  }
  info = {
    id: "kimi",
    displayName: "Kimi (Moonshot AI)",
    defaultModel: "kimi-k2.6",
    apiKeyEnvVar: "AICLI_API_KEY_KIMI",
    requiresApiKey: true,
    baseUrl: this.defaultBaseUrl,
    models: [
      // Kimi K2.6 / K2.5 系列（最新一代，推荐）
      {
        id: "kimi-k2.6",
        displayName: "Kimi K2.6",
        contextWindow: 262144,
        supportsStreaming: true
      },
      {
        id: "kimi-k2.5",
        displayName: "Kimi K2.5",
        contextWindow: 262144,
        supportsStreaming: true
      },
      // Kimi K2 Thinking 系列（支持多步推理 + 工具调用）
      {
        id: "kimi-k2-thinking",
        displayName: "Kimi K2 Thinking",
        contextWindow: 262144,
        supportsStreaming: true
      },
      {
        id: "kimi-k2-thinking-turbo",
        displayName: "Kimi K2 Thinking Turbo",
        contextWindow: 262144,
        supportsStreaming: true
      },
      // Kimi K2 Preview 系列
      {
        id: "kimi-k2-0905-preview",
        displayName: "Kimi K2 0905 Preview",
        contextWindow: 262144,
        supportsStreaming: true
      },
      // moonshot-v1 系列（老一代，稳定）
      {
        id: "moonshot-v1-8k",
        displayName: "Moonshot V1 8K",
        contextWindow: 8192,
        supportsStreaming: true
      },
      {
        id: "moonshot-v1-32k",
        displayName: "Moonshot V1 32K",
        contextWindow: 32768,
        supportsStreaming: true
      },
      {
        id: "moonshot-v1-128k",
        displayName: "Moonshot V1 128K",
        contextWindow: 131072,
        supportsStreaming: true
      },
      // moonshot-v1 Vision 系列（支持图片输入）
      {
        id: "moonshot-v1-8k-vision-preview",
        displayName: "Moonshot V1 8K Vision",
        contextWindow: 8192,
        supportsStreaming: true
      },
      {
        id: "moonshot-v1-32k-vision-preview",
        displayName: "Moonshot V1 32K Vision",
        contextWindow: 32768,
        supportsStreaming: true
      },
      {
        id: "moonshot-v1-128k-vision-preview",
        displayName: "Moonshot V1 128K Vision",
        contextWindow: 131072,
        supportsStreaming: true
      }
    ]
  };
  /**
   * 覆写 chatWithTools — 仅保留 Kimi 专属的 Plan A（XML 伪调用解析）。
   * Plan B（system prompt 规范提示）和 Plan C（虚假声明检测 + 重试）
   * 已由 REPL 层 handleChatWithTools() 统一处理。
   *
   * Plan A（Kimi 专属兜底）：若 Kimi 在文本中输出了 XML 格式工具调用，
   *   自动检测并解析，转换为真实 ToolCall 对象送入执行器。
   */
  async chatWithTools(request, tools) {
    const enhancedRequest = this.fixTemp({
      ...request,
      systemPrompt: (request.systemPrompt ?? "") + KIMI_XML_REMINDER
    });
    const result = await super.chatWithTools(enhancedRequest, tools);
    if ("content" in result && result.content) {
      if (detectsHallucinatedFileOp(result.content)) {
        const xmlToolCalls = this.parseXmlToolCalls(result.content, tools);
        if (xmlToolCalls.length > 0) {
          process.stderr.write(
            `[kimi] \u26A0 Detected ${xmlToolCalls.length} XML pseudo tool call(s) (Kimi did not use the API mechanism). Automatically converted to real tool calls for execution.
`
          );
          return { toolCalls: xmlToolCalls, usage: result.usage };
        }
      }
    }
    return result;
  }
  /**
   * 方案 A 核心逻辑：从 Kimi 的文本响应中提取 XML 格式伪工具调用。
   *
   * Kimi 输出的 XML 格式为：
   *   <tool_name>
   *   <param1>value1</param1>
   *   <param2>value2</param2>
   *   </tool_name>
   *
   * 使用 indexOf 而非正则，避免大文件内容（14KB+ Markdown）时的性能问题。
   * 参数值的提取依据工具 schema 中已知的参数名，安全可靠。
   */
  parseXmlToolCalls(content, tools) {
    if (!content.includes("</")) return [];
    const toolCalls = [];
    let callIndex = 0;
    for (const tool of tools) {
      const startTag = `<${tool.name}>`;
      const endTag = `</${tool.name}>`;
      const paramNames = Object.keys(tool.parameters);
      let searchFrom = 0;
      while (true) {
        const blockStart = content.indexOf(startTag, searchFrom);
        if (blockStart === -1) break;
        const blockEnd = content.indexOf(endTag, blockStart + startTag.length);
        if (blockEnd === -1) break;
        const innerContent = content.slice(blockStart + startTag.length, blockEnd);
        const args = {};
        for (const paramName of paramNames) {
          const openTag = `<${paramName}>`;
          const closeTag = `</${paramName}>`;
          const pStart = innerContent.indexOf(openTag);
          if (pStart !== -1) {
            const pEnd = innerContent.indexOf(closeTag, pStart + openTag.length);
            if (pEnd !== -1) {
              args[paramName] = innerContent.slice(pStart + openTag.length, pEnd);
            }
          }
        }
        if (Object.keys(args).length > 0) {
          toolCalls.push({
            // id 格式：xml-fallback-{时间戳}-{序号}，便于调试日志追踪
            id: `xml-fallback-${Date.now()}-${callIndex++}`,
            name: tool.name,
            arguments: args
          });
        }
        searchFrom = blockEnd + endTag.length;
      }
    }
    return toolCalls;
  }
};

// src/providers/minimax.ts
var MiniMaxProvider = class extends OpenAICompatibleProvider {
  defaultBaseUrl = "https://api.minimaxi.com/v1";
  info = {
    id: "minimax",
    displayName: "MiniMax (\u6D77\u87BA)",
    defaultModel: "MiniMax-M3",
    apiKeyEnvVar: "AICLI_API_KEY_MINIMAX",
    requiresApiKey: true,
    baseUrl: this.defaultBaseUrl,
    models: [
      {
        id: "MiniMax-M3",
        displayName: "MiniMax M3\uFF08\u65D7\u8230\uFF0C1M \u4E0A\u4E0B\u6587\uFF09",
        contextWindow: 1e6,
        supportsStreaming: true
      },
      {
        id: "MiniMax-M2.7",
        displayName: "MiniMax M2.7",
        contextWindow: 1e6,
        supportsStreaming: true
      },
      {
        id: "MiniMax-M2.7-highspeed",
        displayName: "MiniMax M2.7 Highspeed",
        contextWindow: 1e6,
        supportsStreaming: true
      },
      {
        id: "MiniMax-M2.5",
        displayName: "MiniMax M2.5",
        contextWindow: 1e6,
        supportsStreaming: true
      },
      {
        id: "MiniMax-M2.5-highspeed",
        displayName: "MiniMax M2.5 Highspeed",
        contextWindow: 1e6,
        supportsStreaming: true
      },
      {
        id: "MiniMax-M2.1",
        displayName: "MiniMax M2.1",
        contextWindow: 1e6,
        supportsStreaming: true
      },
      {
        id: "MiniMax-M2.1-highspeed",
        displayName: "MiniMax M2.1 Highspeed",
        contextWindow: 1e6,
        supportsStreaming: true
      },
      {
        id: "MiniMax-M2",
        displayName: "MiniMax M2",
        contextWindow: 1e6,
        supportsStreaming: true
      }
    ]
  };
};

// src/providers/openai.ts
var OpenAIProvider = class extends OpenAICompatibleProvider {
  defaultBaseUrl = "https://api.openai.com/v1";
  info = {
    id: "openai",
    displayName: "OpenAI",
    defaultModel: "gpt-4o",
    apiKeyEnvVar: "AICLI_API_KEY_OPENAI",
    requiresApiKey: true,
    baseUrl: this.defaultBaseUrl,
    models: [
      // GPT-5.4 系列（最新旗舰，1.05M 上下文）
      {
        id: "gpt-5.4",
        displayName: "GPT-5.4",
        contextWindow: 105e4,
        supportsStreaming: true
      },
      {
        id: "gpt-5.4-pro",
        displayName: "GPT-5.4 Pro",
        contextWindow: 105e4,
        supportsStreaming: true
      },
      // GPT-5 系列（400K 上下文）
      {
        id: "gpt-5",
        displayName: "GPT-5",
        contextWindow: 4e5,
        supportsStreaming: true
      },
      {
        id: "gpt-5-mini",
        displayName: "GPT-5 Mini",
        contextWindow: 4e5,
        supportsStreaming: true
      },
      {
        id: "gpt-5-nano",
        displayName: "GPT-5 Nano",
        contextWindow: 4e5,
        supportsStreaming: true
      },
      // GPT-4.1 系列（1M 上下文）
      {
        id: "gpt-4.1",
        displayName: "GPT-4.1",
        contextWindow: 1048576,
        supportsStreaming: true
      },
      {
        id: "gpt-4.1-mini",
        displayName: "GPT-4.1 Mini",
        contextWindow: 1048576,
        supportsStreaming: true
      },
      {
        id: "gpt-4.1-nano",
        displayName: "GPT-4.1 Nano",
        contextWindow: 1048576,
        supportsStreaming: true
      },
      // GPT-4o 系列（128K 上下文）
      {
        id: "gpt-4o",
        displayName: "GPT-4o",
        contextWindow: 128e3,
        supportsStreaming: true
      },
      {
        id: "gpt-4o-mini",
        displayName: "GPT-4o Mini",
        contextWindow: 128e3,
        supportsStreaming: true
      },
      // 推理模型（200K 上下文）
      {
        id: "o3",
        displayName: "o3",
        contextWindow: 2e5,
        supportsStreaming: true
      },
      {
        id: "o3-mini",
        displayName: "o3 Mini",
        contextWindow: 2e5,
        supportsStreaming: true
      },
      {
        id: "o4-mini",
        displayName: "o4 Mini",
        contextWindow: 2e5,
        supportsStreaming: true
      }
    ]
  };
};

// src/providers/openrouter.ts
var OpenRouterProvider = class extends OpenAICompatibleProvider {
  defaultBaseUrl = "https://openrouter.ai/api/v1";
  info = {
    id: "openrouter",
    displayName: "OpenRouter",
    defaultModel: "anthropic/claude-sonnet-4",
    apiKeyEnvVar: "AICLI_API_KEY_OPENROUTER",
    requiresApiKey: true,
    baseUrl: this.defaultBaseUrl,
    models: [
      // Anthropic Claude 系列
      {
        id: "anthropic/claude-opus-4",
        displayName: "Claude Opus 4",
        contextWindow: 2e5,
        supportsStreaming: true
      },
      {
        id: "anthropic/claude-sonnet-4",
        displayName: "Claude Sonnet 4",
        contextWindow: 2e5,
        supportsStreaming: true
      },
      {
        id: "anthropic/claude-haiku-4",
        displayName: "Claude Haiku 4",
        contextWindow: 2e5,
        supportsStreaming: true
      },
      // OpenAI 系列
      {
        id: "openai/gpt-5.4",
        displayName: "GPT-5.4",
        contextWindow: 105e4,
        supportsStreaming: true
      },
      {
        id: "openai/gpt-4o",
        displayName: "GPT-4o",
        contextWindow: 128e3,
        supportsStreaming: true
      },
      {
        id: "openai/gpt-4o-mini",
        displayName: "GPT-4o Mini",
        contextWindow: 128e3,
        supportsStreaming: true
      },
      // Google Gemini 系列
      {
        id: "google/gemini-2.5-pro",
        displayName: "Gemini 2.5 Pro",
        contextWindow: 1048576,
        supportsStreaming: true
      },
      {
        id: "google/gemini-2.5-flash",
        displayName: "Gemini 2.5 Flash",
        contextWindow: 1048576,
        supportsStreaming: true
      },
      // Meta Llama 系列
      {
        id: "meta-llama/llama-4-maverick",
        displayName: "Llama 4 Maverick",
        contextWindow: 1048576,
        supportsStreaming: true
      },
      {
        id: "meta-llama/llama-4-scout",
        displayName: "Llama 4 Scout",
        contextWindow: 512e3,
        supportsStreaming: true
      },
      // DeepSeek 系列
      {
        id: "deepseek/deepseek-chat-v3-0324",
        displayName: "DeepSeek V3 0324",
        contextWindow: 131072,
        supportsStreaming: true
      },
      {
        id: "deepseek/deepseek-r1",
        displayName: "DeepSeek R1",
        contextWindow: 163840,
        supportsStreaming: true
      },
      // Qwen 系列
      {
        id: "qwen/qwen3-coder",
        displayName: "Qwen3 Coder",
        contextWindow: 262144,
        supportsStreaming: true
      },
      {
        id: "qwen/qwen-2.5-72b-instruct",
        displayName: "Qwen 2.5 72B",
        contextWindow: 131072,
        supportsStreaming: true
      },
      // Mistral 系列
      {
        id: "mistralai/mistral-large",
        displayName: "Mistral Large",
        contextWindow: 131072,
        supportsStreaming: true
      },
      {
        id: "mistralai/mistral-small-3.1-24b-instruct",
        displayName: "Mistral Small 3.1 24B",
        contextWindow: 128e3,
        supportsStreaming: true
      }
    ]
  };
};

// src/providers/ollama.ts
var OllamaProvider = class extends OpenAICompatibleProvider {
  // 用 127.0.0.1 而非 localhost：Node.js 18+ 在 Windows 上优先解析 localhost 为 IPv6 (::1)，
  // 但 Ollama 默认只监听 IPv4，导致连接失败。
  defaultBaseUrl = "http://127.0.0.1:11434/v1";
  defaultTimeout = 12e4;
  // 本地推理可能较慢，默认 2 分钟
  /** 动态模型列表 */
  dynamicModels = [];
  info = {
    id: "ollama",
    displayName: "Ollama (Local)",
    defaultModel: "",
    apiKeyEnvVar: "",
    requiresApiKey: false,
    baseUrl: this.defaultBaseUrl,
    models: []
  };
  async initialize(apiKey, options) {
    const baseUrl = options?.baseUrl ?? this.defaultBaseUrl;
    await super.initialize(apiKey || "ollama", { ...options, baseUrl });
    await this.tryConnect();
  }
  /**
   * 尝试通过 OpenAI client 的 models.list() 刷新 Ollama 模型列表。
   * 成功返回 true，失败返回 false（不抛异常）。
   */
  async tryConnect() {
    try {
      const response = await this.client.models.list();
      const modelIds = response.data.map((m) => m.id);
      if (modelIds.length === 0) {
        return false;
      }
      this.dynamicModels = modelIds.map((id) => ({
        id,
        displayName: id,
        contextWindow: this.estimateContextWindow(id),
        supportsStreaming: true,
        supportsThinking: false
      }));
      const preferred = ["llama3.1", "llama3", "qwen2.5", "qwen3", "qwen2", "deepseek-r1", "mistral", "gemma2", "gemma3"];
      const defaultModel = this.dynamicModels.find(
        (m) => preferred.some((p) => m.id.startsWith(p))
      )?.id ?? this.dynamicModels[0].id;
      Object.assign(this.info, {
        defaultModel,
        models: this.dynamicModels
      });
      return true;
    } catch (err) {
      process.stderr.write(`[ollama] connect failed: ${err instanceof Error ? err.message : String(err)}
`);
      return false;
    }
  }
  /** 根据模型名估算上下文窗口 */
  estimateContextWindow(modelName) {
    const name = modelName.toLowerCase();
    if (name.includes("llama3") || name.includes("llama-3")) return 131072;
    if (name.includes("qwen2.5") || name.includes("qwen3")) return 131072;
    if (name.includes("qwen2") || name.includes("qwen-2")) return 32768;
    if (name.includes("deepseek")) return 65536;
    if (name.includes("gemma2") || name.includes("gemma-2") || name.includes("gemma3")) return 8192;
    if (name.includes("mistral")) return 32768;
    if (name.includes("phi")) return 16384;
    if (name.includes("codellama") || name.includes("code-llama")) return 16384;
    return 8192;
  }
  /** 实时刷新模型列表 */
  async listModels() {
    await this.tryConnect();
    return this.dynamicModels.length > 0 ? this.dynamicModels : this.info.models;
  }
  /** Ollama 无需验证 API Key，只检查服务是否可达 */
  async validateApiKey() {
    return this.tryConnect();
  }
};

// src/providers/custom.ts
var CustomProvider = class extends OpenAICompatibleProvider {
  defaultBaseUrl;
  info;
  constructor(cfg) {
    super();
    this.defaultBaseUrl = cfg.baseUrl;
    this.info = {
      id: cfg.id,
      displayName: cfg.displayName,
      defaultModel: cfg.defaultModel,
      // 环境变量格式：AICLI_API_KEY_<ID大写>，如 AICLI_API_KEY_SILICONFLOW
      apiKeyEnvVar: `AICLI_API_KEY_${cfg.id.toUpperCase()}`,
      // 本地服务（如 Ollama）不需要 API Key，设为 false
      requiresApiKey: false,
      baseUrl: cfg.baseUrl,
      models: cfg.models.length > 0 ? cfg.models.map((m) => ({
        id: m.id,
        displayName: m.displayName ?? m.id,
        contextWindow: m.contextWindow ?? 4096,
        supportsStreaming: true
      })) : [
        // 如果未配置 models 列表，以 defaultModel 作为唯一条目
        {
          id: cfg.defaultModel,
          displayName: cfg.defaultModel,
          contextWindow: 4096,
          supportsStreaming: true
        }
      ]
    };
  }
};

// src/providers/registry.ts
var BUILT_IN_PROVIDERS = [
  ClaudeProvider,
  GeminiProvider,
  DeepSeekProvider,
  ZhipuProvider,
  KimiProvider,
  MiniMaxProvider,
  OpenAIProvider,
  OpenRouterProvider,
  OllamaProvider
];
var ProviderRegistry = class {
  providers = /* @__PURE__ */ new Map();
  // 记录自定义 provider 的状态（用于 listAll 展示）
  customConfigs = [];
  async initialize(getApiKey, getOptions, customProviderConfigs) {
    for (const ProviderClass of BUILT_IN_PROVIDERS) {
      const provider = new ProviderClass();
      const apiKey = getApiKey(provider.info.id);
      if (apiKey || !provider.info.requiresApiKey) {
        const options = getOptions?.(provider.info.id) ?? {};
        try {
          await provider.initialize(apiKey ?? "", options);
          this.providers.set(provider.info.id, provider);
        } catch (err) {
          if (provider.info.requiresApiKey) throw err;
        }
      }
    }
    this.customConfigs = customProviderConfigs ?? [];
    for (const cfg of this.customConfigs) {
      const provider = new CustomProvider(cfg);
      const apiKey = getApiKey(cfg.id) ?? cfg.apiKey ?? "no-key";
      const extraOptions = getOptions?.(cfg.id) ?? {};
      await provider.initialize(apiKey, {
        baseUrl: cfg.baseUrl,
        timeout: cfg.timeout,
        ...extraOptions
      });
      this.providers.set(cfg.id, provider);
    }
  }
  /**
   * Register a provider instance under the given id (overwrites any existing).
   * Primary uses: integration tests injecting MockProvider, and future hot-swap
   * scenarios. The provider is assumed already initialized.
   */
  register(id, provider) {
    this.providers.set(id, provider);
  }
  get(id) {
    const provider = this.providers.get(id);
    if (!provider) {
      throw new ProviderNotFoundError(id);
    }
    return provider;
  }
  has(id) {
    return this.providers.has(id);
  }
  listAvailable() {
    return [...this.providers.values()];
  }
  listAll() {
    const builtIn = BUILT_IN_PROVIDERS.map((P) => {
      const p = new P();
      return {
        id: p.info.id,
        displayName: p.info.displayName,
        configured: this.providers.has(p.info.id),
        defaultModel: p.info.defaultModel,
        isCustom: false
      };
    });
    const custom = this.customConfigs.map((cfg) => ({
      id: cfg.id,
      displayName: cfg.displayName,
      configured: this.providers.has(cfg.id),
      defaultModel: cfg.defaultModel,
      isCustom: true
    }));
    return [...builtIn, ...custom];
  }
  getFirstAvailable() {
    return this.providers.values().next().value;
  }
};

export {
  detectsHallucinatedFileOp,
  hadPreviousWriteToolCalls,
  TOOL_CALL_REMINDER,
  buildWriteRoundReminder,
  HALLUCINATION_CORRECTION_MESSAGE,
  extractWrittenFilePaths,
  extractBashCommands,
  findPhantomClaims,
  buildPhantomCorrectionMessage,
  detectPseudoToolCalls,
  stripPseudoToolCalls,
  detectMetaNarration,
  looksLikeDocumentBody,
  stripToolCallReminder,
  TEE_FINAL_USER_NUDGE,
  CONTENT_ONLY_STREAM_REMINDER,
  ProviderRegistry
};
