import {
  getGatewayRuntimeAuthForModel,
  resolveProviderApiKey
} from "./chunk-Q7FJ5ZHM.js";
import {
  loadModelsJsonProviders
} from "./chunk-ODWDQNRE.js";
import {
  buildChatCompletionTokenLimit,
  shouldAssumeOpenAiChatCompletions
} from "./chunk-Y27UJK6V.js";
import {
  extractJsonCandidates
} from "./chunk-UZB5KHKX.js";
import {
  log
} from "./chunk-2ODBA7MQ.js";

// src/fallback-llm.ts
var FallbackLlmClient = class {
  gatewayConfig;
  runtimeContext;
  constructor(gatewayConfig, runtimeContext = {}) {
    this.gatewayConfig = gatewayConfig;
    this.runtimeContext = runtimeContext;
  }
  /**
   * Check if fallback is available (gateway config has at least one model).
   */
  isAvailable(agentId) {
    const models = this.getModelChain(agentId);
    return models.length > 0;
  }
  /**
   * Make a chat completion request using the gateway's default AI chain.
   * Tries primary first, then each fallback in order.
   * When agentId is provided, uses that agent persona's model chain instead of defaults.
   */
  async chatCompletion(messages, options = {}) {
    const models = this.getModelChain(options.agentId);
    if (models.length === 0) {
      log.warn("fallback LLM: no models configured in gateway");
      return null;
    }
    const runChain = async () => {
      for (let i = 0; i < models.length; i++) {
        const model = models[i];
        const isFallback = i > 0;
        try {
          const result = await this.tryModel(model, messages, options);
          if (result) {
            if (isFallback) {
              log.debug(`fallback LLM: succeeded using ${model.modelString} (fallback ${i})`);
            }
            return {
              content: result.content,
              modelUsed: model.modelString,
              usage: result.usage
            };
          }
        } catch (err) {
          const errorMsg = err instanceof Error ? err.message : String(err);
          log.debug(`fallback LLM: ${model.modelString} failed (${errorMsg}), trying next...`);
        }
      }
      log.warn(`fallback LLM: all ${models.length} models in chain failed`);
      return null;
    };
    if (typeof options.timeoutMs === "number") {
      if (options.timeoutMs <= 0) {
        log.warn("fallback LLM: timed out before request started");
        return null;
      }
      let timeoutHandle;
      try {
        return await Promise.race([
          runChain(),
          new Promise((resolve) => {
            timeoutHandle = setTimeout(() => {
              log.warn(`fallback LLM: timed out after ${options.timeoutMs}ms`);
              resolve(null);
            }, options.timeoutMs);
          })
        ]);
      } finally {
        if (timeoutHandle) clearTimeout(timeoutHandle);
      }
    }
    return await runChain();
  }
  /**
   * Make a request with structured output (Zod schema).
   * Returns parsed JSON or null on failure.
   */
  async parseWithSchema(messages, schema, options = {}) {
    const detailed = await this.parseWithSchemaDetailed(messages, schema, options);
    return detailed?.result ?? null;
  }
  /**
   * Like parseWithSchema but also returns the model that was used,
   * so callers can emit accurate trace events.
   */
  async parseWithSchemaDetailed(messages, schema, options = {}) {
    const response = await this.chatCompletion(messages, options);
    if (!response?.content) return null;
    try {
      const candidates = extractJsonCandidates(response.content);
      for (const c of candidates) {
        try {
          const parsed = JSON.parse(c);
          return { result: schema.parse(parsed), modelUsed: response.modelUsed };
        } catch {
        }
      }
      return null;
    } catch (err) {
      log.warn("fallback LLM: failed to parse structured output:", err);
      return null;
    }
  }
  /**
   * Get the full model chain from gateway config.
   * Returns array of models in order: [primary, fallback1, fallback2, ...]
   *
   * When agentId is provided, looks up the matching entry in agents.list[]
   * and uses that persona's model chain. Falls back to agents.defaults.model
   * if agentId is not found or not provided.
   */
  getModelChain(agentId) {
    const chain = [];
    const providers = this.gatewayConfig?.models?.providers ?? {};
    let modelConfig;
    if (agentId) {
      const persona = this.gatewayConfig?.agents?.list?.find(
        (a) => a.id === agentId
      );
      if (persona?.model) {
        modelConfig = persona.model;
        log.debug(`fallback LLM: using agent persona "${agentId}" model chain`);
      } else {
        log.warn(
          `fallback LLM: agent persona "${agentId}" not found or has no model config, falling back to defaults`
        );
      }
    }
    if (!modelConfig) {
      modelConfig = this.gatewayConfig?.agents?.defaults?.model;
    }
    const modelStrings = [];
    if (modelConfig?.primary) {
      modelStrings.push(modelConfig.primary);
    }
    if (Array.isArray(modelConfig?.fallbacks)) {
      for (const fb of modelConfig.fallbacks) {
        if (typeof fb === "string" && !modelStrings.includes(fb)) {
          modelStrings.push(fb);
        }
      }
    }
    for (const modelString of modelStrings) {
      const modelRef = this.parseModelString(modelString, providers);
      if (modelRef) {
        chain.push(modelRef);
      }
    }
    return chain;
  }
  /**
   * Parse a "provider/model" string and look up its config.
   */
  parseModelString(modelString, providers) {
    const parts = modelString.split("/");
    if (parts.length < 2) {
      log.warn(`fallback LLM: invalid model format: ${modelString}`);
      return null;
    }
    const providerId = parts[0];
    const modelId = parts.slice(1).join("/");
    const providerConfig = providers[providerId] ?? this.resolveFromModelsJson(providerId);
    if (!providerConfig) {
      log.warn(`fallback LLM: provider not found: ${providerId}`);
      return null;
    }
    return { providerId, modelId, providerConfig, modelString };
  }
  /**
   * Look up a provider from the gateway's materialized models.json, which
   * contains all providers including built-in ones (openai-codex, google-vertex,
   * etc.) that aren't in the user's openclaw.json but are registered by
   * gateway plugins. Returns null if the provider isn't found there either.
   */
  resolveFromModelsJson(providerId) {
    const allProviders = loadModelsJsonProviders();
    const config = allProviders[providerId];
    if (config) {
      log.debug(`fallback LLM: resolved provider "${providerId}" from models.json (api: ${config.api ?? "default"})`);
      return config;
    }
    return null;
  }
  /**
   * Try to call a single model.
   *
   * Uses the gateway's native getRuntimeAuthForModel when available — this
   * handles all provider-specific auth transforms (OAuth token exchange,
   * base URL overrides for codex/copilot/etc.) through the same codepath
   * the gateway itself uses. Falls back to resolveProviderApiKey for
   * simpler providers or when the runtime module isn't loaded.
   */
  async tryModel(model, messages, options) {
    const runtimeAuth = await this.resolveRuntimeAuth(model);
    const effectiveBaseUrl = runtimeAuth?.baseUrl ?? model.providerConfig.baseUrl;
    const resolvedApiKey = runtimeAuth?.apiKey ?? await this.resolveFallbackApiKey(model);
    const rawKey = model.providerConfig.apiKey;
    const needsResolution = rawKey === "secretref-managed" || typeof rawKey === "object" && rawKey !== null;
    if (needsResolution && !resolvedApiKey) {
      throw new Error(`API key for provider "${model.providerId}" could not be resolved from secret ref`);
    }
    const effectiveConfig = {
      ...model.providerConfig,
      baseUrl: effectiveBaseUrl,
      ...resolvedApiKey ? { apiKey: resolvedApiKey } : {}
    };
    if (model.providerConfig.api === "anthropic-messages") {
      return await this.callAnthropic(effectiveConfig, model.modelId, messages, options);
    }
    if (model.providerConfig.api === "openai-responses" || model.providerConfig.api === "openai-codex-responses" || model.providerConfig.api === "azure-openai-responses") {
      return await this.callOpenAIResponses(
        effectiveConfig,
        model.modelId,
        messages,
        options
      );
    }
    return await this.callOpenAI(
      effectiveConfig,
      model.modelId,
      messages,
      options,
      shouldAssumeOpenAiChatCompletions(effectiveConfig.baseUrl)
    );
  }
  /**
   * Resolve request-ready auth through the gateway's native runtime, which
   * handles provider-specific transforms (OAuth token exchange for codex/copilot,
   * base URL rewrite, etc.). Returns null if the runtime isn't available.
   */
  async resolveRuntimeAuth(model) {
    try {
      const getRuntimeAuth = await getGatewayRuntimeAuthForModel();
      if (!getRuntimeAuth) return null;
      const result = await getRuntimeAuth({
        model: {
          provider: model.providerId,
          id: model.modelId,
          api: model.providerConfig.api,
          baseUrl: model.providerConfig.baseUrl
        },
        cfg: this.gatewayConfig,
        workspaceDir: this.runtimeContext.workspaceDir
      });
      if (result?.apiKey || result?.baseUrl) {
        log.debug(
          `fallback LLM: resolved runtime auth for "${model.modelString}" (source: ${result.source ?? "unknown"}, mode: ${result.mode ?? "unknown"})`
        );
        return { apiKey: result.apiKey, baseUrl: result.baseUrl };
      }
    } catch (err) {
      log.debug(
        `fallback LLM: gateway runtime auth failed for "${model.modelString}": ${err instanceof Error ? err.message : String(err)}`
      );
    }
    return null;
  }
  /**
   * Resolve API key through the existing provider-level resolution (env vars,
   * secret refs, etc.). Used as fallback when gateway runtime auth isn't available.
   */
  async resolveFallbackApiKey(model) {
    return resolveProviderApiKey(
      model.providerId,
      model.providerConfig.apiKey,
      this.gatewayConfig,
      this.runtimeContext.agentDir
    );
  }
  /**
   * Call OpenAI-compatible API.
   */
  async callOpenAI(config, modelId, messages, options, assumeOpenAI) {
    const base = config.baseUrl.replace(/\/$/, "");
    const url = base.endsWith("/v1") ? `${base}/chat/completions` : `${base}/v1/chat/completions`;
    const headers = {
      "Content-Type": "application/json",
      ...config.headers
    };
    if (config.apiKey && typeof config.apiKey === "string") {
      if (config.authHeader !== false) {
        headers["Authorization"] = `Bearer ${config.apiKey}`;
      }
    }
    const body = {
      model: modelId,
      messages,
      temperature: options.temperature ?? 0.3,
      ...buildChatCompletionTokenLimit(modelId, options.maxTokens ?? 4096, {
        assumeOpenAI
      })
    };
    const response = await fetch(url, {
      method: "POST",
      headers,
      body: JSON.stringify(body)
    });
    if (!response.ok) {
      const error = await response.text();
      throw new Error(`OpenAI API error: ${response.status} ${error}`);
    }
    const data = await response.json();
    const content = data.choices?.[0]?.message?.content;
    if (!content) {
      throw new Error("Empty response from OpenAI API");
    }
    return {
      content,
      usage: data.usage ? {
        inputTokens: data.usage.prompt_tokens,
        outputTokens: data.usage.completion_tokens,
        totalTokens: data.usage.total_tokens
      } : void 0
    };
  }
  /**
   * Call an OpenAI-compatible Responses API.
   */
  async callOpenAIResponses(config, modelId, messages, options) {
    const base = config.baseUrl.replace(/\/$/, "");
    const url = base.endsWith("/v1") ? `${base}/responses` : `${base}/v1/responses`;
    const headers = {
      "Content-Type": "application/json",
      ...config.headers
    };
    if (config.apiKey && typeof config.apiKey === "string" && config.authHeader !== false) {
      headers["Authorization"] = `Bearer ${config.apiKey}`;
    }
    const instructions = messages.filter((message) => message.role === "system").map((message) => message.content).join("\n\n").trim();
    const input = messages.filter((message) => message.role !== "system").map((message) => ({
      role: message.role,
      content: [{
        type: message.role === "assistant" ? "output_text" : "input_text",
        text: message.content
      }]
    }));
    const body = {
      model: modelId,
      input,
      max_output_tokens: Math.max(0, Math.floor(options.maxTokens ?? 4096)),
      temperature: options.temperature ?? 0.3
    };
    if (instructions.length > 0) {
      body.instructions = instructions;
    }
    const response = await fetch(url, {
      method: "POST",
      headers,
      body: JSON.stringify(body)
    });
    if (!response.ok) {
      const error = await response.text();
      throw new Error(`OpenAI Responses API error: ${response.status} ${error}`);
    }
    const data = await response.json();
    const outputText = extractResponsesOutputText(data);
    if (!outputText) {
      throw new Error("Empty response from OpenAI Responses API");
    }
    return {
      content: outputText,
      usage: data.usage ? {
        inputTokens: data.usage.input_tokens,
        outputTokens: data.usage.output_tokens,
        totalTokens: data.usage.total_tokens
      } : void 0
    };
  }
  /**
   * Call Anthropic Messages API.
   */
  async callAnthropic(config, modelId, messages, options) {
    const base = config.baseUrl.replace(/\/$/, "");
    const url = base.endsWith("/v1") ? `${base}/messages` : `${base}/v1/messages`;
    const headers = {
      "Content-Type": "application/json",
      "anthropic-version": "2023-06-01",
      ...config.headers
    };
    if (config.apiKey && typeof config.apiKey === "string") {
      headers["x-api-key"] = config.apiKey;
    }
    const systemMessage = messages.find((m) => m.role === "system")?.content;
    const nonSystemMessages = messages.filter((m) => m.role !== "system");
    const anthropicMessages = nonSystemMessages.map((m) => ({
      role: m.role,
      content: m.content
    }));
    const body = {
      model: modelId,
      messages: anthropicMessages,
      max_tokens: options.maxTokens ?? 4096,
      temperature: options.temperature ?? 0.3
    };
    if (systemMessage) {
      body.system = systemMessage;
    }
    const response = await fetch(url, {
      method: "POST",
      headers,
      body: JSON.stringify(body)
    });
    if (!response.ok) {
      const error = await response.text();
      throw new Error(`Anthropic API error: ${response.status} ${error}`);
    }
    const data = await response.json();
    const content = data.content?.[0]?.text;
    if (!content) {
      throw new Error("Empty response from Anthropic API");
    }
    return {
      content,
      usage: data.usage ? {
        inputTokens: data.usage.input_tokens,
        outputTokens: data.usage.output_tokens,
        totalTokens: (data.usage.input_tokens ?? 0) + (data.usage.output_tokens ?? 0)
      } : void 0
    };
  }
};
function extractResponsesOutputText(data) {
  if (typeof data.output_text === "string" && data.output_text.trim().length > 0) {
    return data.output_text;
  }
  const chunks = [];
  for (const item of data.output ?? []) {
    if (typeof item.text === "string" && item.text.trim().length > 0) {
      chunks.push(item.text);
    }
    for (const part of item.content ?? []) {
      if ((part.type === "output_text" || part.type === "text") && typeof part.text === "string" && part.text.trim().length > 0) {
        chunks.push(part.text);
      }
    }
  }
  const joined = chunks.join("\n").trim();
  return joined.length > 0 ? joined : null;
}

export {
  FallbackLlmClient
};
//# sourceMappingURL=chunk-CRU27Q4J.js.map