1490 lines · 54.1 KB · TypeScript
RawDownload
import z from "zod"import fuzzysort from "fuzzysort"import { Config } from "../config/config"import { mapValues, mergeDeep, omit, pickBy, sortBy } from "remeda"import { NoSuchModelError, type Provider as SDK } from "ai"import { Log } from "../util/log"import { BunProc } from "../bun"import { Plugin } from "../plugin"import { ModelsDev } from "./models"import { NamedError } from "@synsci/util/error"import { Auth } from "../auth"import { Env } from "../env"import { Instance } from "../project/instance"import { Flag } from "../flag/flag"import { iife } from "@/util/iife"import { OpenScience } from "../openscience" // Direct imports for bundled providersimport { createAmazonBedrock, type AmazonBedrockProviderSettings } from "@ai-sdk/amazon-bedrock"import { createAnthropic } from "@ai-sdk/anthropic"import { createAzure } from "@ai-sdk/azure"import { createGoogleGenerativeAI } from "@ai-sdk/google"import { createVertex } from "@ai-sdk/google-vertex"import { createVertexAnthropic } from "@ai-sdk/google-vertex/anthropic"import { createOpenAI } from "@ai-sdk/openai"import { createOpenAICompatible } from "@ai-sdk/openai-compatible"import { createOpenRouter, type LanguageModelV2 } from "@openrouter/ai-sdk-provider"import { createOpenaiCompatible as createGitHubCopilotOpenAICompatible } from "./sdk/openai-compatible/src"import { createXai } from "@ai-sdk/xai"import { createMistral } from "@ai-sdk/mistral"import { createGroq } from "@ai-sdk/groq"import { createDeepInfra } from "@ai-sdk/deepinfra"import { createCerebras } from "@ai-sdk/cerebras"import { createCohere } from "@ai-sdk/cohere"import { createGateway } from "@ai-sdk/gateway"import { createTogetherAI } from "@ai-sdk/togetherai"import { createPerplexity } from "@ai-sdk/perplexity"import { createVercel } from "@ai-sdk/vercel"import { createGitLab } from "@gitlab/gitlab-ai-provider"import { ProviderTransform } from "./transform" export namespace Provider {  const log = Log.create({ service: "provider" })   function isGpt5OrLater(modelID: string): boolean {    const match = /^gpt-(\d+)/.exec(modelID)    if (!match) {      return false    }    return Number(match[1]) >= 5  }   function shouldUseCopilotResponsesApi(modelID: string): boolean {    return isGpt5OrLater(modelID) && !modelID.startsWith("gpt-5-mini")  }   const BUNDLED_PROVIDERS: Record<string, (options: any) => SDK> = {    "@ai-sdk/amazon-bedrock": createAmazonBedrock,    "@ai-sdk/anthropic": createAnthropic,    "@ai-sdk/azure": createAzure,    "@ai-sdk/google": createGoogleGenerativeAI,    "@ai-sdk/google-vertex": createVertex,    "@ai-sdk/google-vertex/anthropic": createVertexAnthropic,    "@ai-sdk/openai": createOpenAI,    "@ai-sdk/openai-compatible": createOpenAICompatible,    "@openrouter/ai-sdk-provider": createOpenRouter,    "@ai-sdk/xai": createXai,    "@ai-sdk/mistral": createMistral,    "@ai-sdk/groq": createGroq,    "@ai-sdk/deepinfra": createDeepInfra,    "@ai-sdk/cerebras": createCerebras,    "@ai-sdk/cohere": createCohere,    "@ai-sdk/gateway": createGateway,    "@ai-sdk/togetherai": createTogetherAI,    "@ai-sdk/perplexity": createPerplexity,    "@ai-sdk/vercel": createVercel,    "@gitlab/gitlab-ai-provider": createGitLab,    // @ts-ignore (TODO: kill this code so we dont have to maintain it)    "@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible,  }   function isAtlasApiKey(key: unknown): key is string {    return typeof key === "string" && key.startsWith("thk_")  }   function isAtlasProxyBaseURL(baseURL: unknown): baseURL is string {    return typeof baseURL === "string" && baseURL.includes("/api/llm/proxy/")  }   function requireAtlasProxyForManagedKey(provider: Info, options: Record<string, any>) {    // Key off the EFFECTIVE credential, not raw env. A managed thk_ value can    // sit unused in env while an auth.json key wins resolution; demanding    // proxy routing for it hard-failed every call with advice (`connect    // sync`) that re-delivers the same env and can never fix it.    const effective =      (typeof options["apiKey"] === "string" ? (options["apiKey"] as string) : undefined) ??      provider.key ??      provider.env.map((key) => Env.get(key)).find((value): value is string => !!value)    if (!isAtlasApiKey(effective)) return    if (isAtlasProxyBaseURL(options["baseURL"])) return    throw new Error(      `${provider.id} is using a managed Atlas key without an Atlas proxy URL. ` +        "Run `openscience connect sync` and try again.",    )  }   /** A user-owned (BYOK) key: a real, non-managed credential. Excludes the   *  "public" sentinel used for the zero-cost openscience demo models. */  function isByokKey(key: unknown): key is string {    return typeof key === "string" && key.length > 0 && key !== "public" && !isAtlasApiKey(key)  }   /**   * Blocker (c) — the inverse of requireAtlasProxyForManagedKey.   *   * A prior managed sync may have injected an Atlas proxy `*_BASE_URL` (e.g.   * ANTHROPIC_BASE_URL → app.syntheticsciences.ai). If the resolved key is the   * user's OWN key (BYOK), that key must NEVER be sent to the Atlas proxy — it   * would leak the credential and mis-bill. Pin the base URL back to the   * provider's public endpoint and drop the managed routing.   */  function pinByokToPublicEndpoint(provider: Info, options: Record<string, any>, publicURL: string) {    const effectiveKey =      (typeof options["apiKey"] === "string" ? (options["apiKey"] as string) : undefined) ??      provider.key ??      provider.env.map((key) => Env.get(key)).find((value): value is string => !!value)    // Managed (thk_*) keys must keep their Atlas proxy routing.    if (isAtlasApiKey(effectiveKey)) return    if (!isByokKey(effectiveKey)) return    if (isAtlasProxyBaseURL(options["baseURL"])) {      log.warn("refusing to route BYOK key through Atlas proxy — pinning to public endpoint", {        provider: provider.id,      })      options["baseURL"] = publicURL    }  }   type CustomModelLoader = (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>  type CustomLoader = (provider: Info) => Promise<{    autoload: boolean    getModel?: CustomModelLoader    options?: Record<string, any>  }>   const CUSTOM_LOADERS: Record<string, CustomLoader> = {    async anthropic() {      const baseURL = Env.get("ANTHROPIC_BASE_URL")      return {        autoload: false,        options: {          ...(baseURL ? { baseURL } : {}),          headers: {            "anthropic-beta":              "claude-code-20250219,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",          },        },      }    },    async openscience(input) {      const hasKey = await (async () => {        const env = Env.all()        if (input.env.some((item) => env[item])) return true        if (await Auth.get(input.id)) return true        const config = await Config.get()        if (config.provider?.["synsci"]?.options?.apiKey) return true        return false      })()       if (!hasKey) {        for (const [key, value] of Object.entries(input.models)) {          if (value.cost.input === 0) continue          delete input.models[key]        }      }       return {        autoload: Object.keys(input.models).length > 0,        options: hasKey ? {} : { apiKey: "public" },      }    },    openai: async () => {      const baseURL = Env.get("OPENAI_BASE_URL")      return {        autoload: false,        async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {          return sdk.responses(modelID)        },        options: baseURL ? { baseURL } : {},      }    },    "github-copilot": async () => {      return {        autoload: false,        async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {          if (sdk.responses === undefined && sdk.chat === undefined) return sdk.languageModel(modelID)          return shouldUseCopilotResponsesApi(modelID) ? sdk.responses(modelID) : sdk.chat(modelID)        },        options: {},      }    },    "github-copilot-enterprise": async () => {      return {        autoload: false,        async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {          if (sdk.responses === undefined && sdk.chat === undefined) return sdk.languageModel(modelID)          return shouldUseCopilotResponsesApi(modelID) ? sdk.responses(modelID) : sdk.chat(modelID)        },        options: {},      }    },    azure: async () => {      return {        autoload: false,        async getModel(sdk: any, modelID: string, options?: Record<string, any>) {          if (options?.["useCompletionUrls"]) {            return sdk.chat(modelID)          } else {            return sdk.responses(modelID)          }        },        options: {},      }    },    "azure-cognitive-services": async () => {      const resourceName = Env.get("AZURE_COGNITIVE_SERVICES_RESOURCE_NAME")      return {        autoload: false,        async getModel(sdk: any, modelID: string, options?: Record<string, any>) {          if (options?.["useCompletionUrls"]) {            return sdk.chat(modelID)          } else {            return sdk.responses(modelID)          }        },        options: {          baseURL: resourceName ? `https://${resourceName}.cognitiveservices.azure.com/openai` : undefined,        },      }    },    "amazon-bedrock": async () => {      const config = await Config.get()      const providerConfig = config.provider?.["amazon-bedrock"]       const auth = await Auth.get("amazon-bedrock")       // Region precedence: 1) config file, 2) env var, 3) default      const configRegion = providerConfig?.options?.region      const envRegion = Env.get("AWS_REGION")      const defaultRegion = configRegion ?? envRegion ?? "us-east-1"       // Profile: config file takes precedence over env var      const configProfile = providerConfig?.options?.profile      const envProfile = Env.get("AWS_PROFILE")      const profile = configProfile ?? envProfile       const awsAccessKeyId = Env.get("AWS_ACCESS_KEY_ID")       const awsBearerToken = iife(() => {        const envToken = Env.get("AWS_BEARER_TOKEN_BEDROCK")        if (envToken) return envToken        if (auth?.type === "api") {          Env.set("AWS_BEARER_TOKEN_BEDROCK", auth.key)          return auth.key        }        return undefined      })       const awsWebIdentityTokenFile = Env.get("AWS_WEB_IDENTITY_TOKEN_FILE")       if (!profile && !awsAccessKeyId && !awsBearerToken && !awsWebIdentityTokenFile) return { autoload: false }       const providerOptions: AmazonBedrockProviderSettings = {        region: defaultRegion,      }       // Only use credential chain if no bearer token exists      // Bearer token takes precedence over credential chain (profiles, access keys, IAM roles, web identity tokens)      if (!awsBearerToken) {        const { fromNodeProviderChain } = await import(await BunProc.install("@aws-sdk/credential-providers"))         // Build credential provider options (only pass profile if specified)        const credentialProviderOptions = profile ? { profile } : {}         providerOptions.credentialProvider = fromNodeProviderChain(credentialProviderOptions)      }       // Add custom endpoint if specified (endpoint takes precedence over baseURL)      const endpoint = providerConfig?.options?.endpoint ?? providerConfig?.options?.baseURL      if (endpoint) {        providerOptions.baseURL = endpoint      }       return {        autoload: true,        options: providerOptions,        async getModel(sdk: any, modelID: string, options?: Record<string, any>) {          // Skip region prefixing if model already has a cross-region inference profile prefix          if (modelID.startsWith("global.") || modelID.startsWith("jp.")) {            return sdk.languageModel(modelID)          }           // Region resolution precedence (highest to lowest):          // 1. options.region from openscience.json provider config          // 2. defaultRegion from AWS_REGION environment variable          // 3. Default "us-east-1" (baked into defaultRegion)          const region = options?.region ?? defaultRegion           let regionPrefix = region.split("-")[0]           switch (regionPrefix) {            case "us": {              const modelRequiresPrefix = [                "nova-micro",                "nova-lite",                "nova-pro",                "nova-premier",                "nova-2",                "claude",                "deepseek",              ].some((m) => modelID.includes(m))              const isGovCloud = region.startsWith("us-gov")              if (modelRequiresPrefix && !isGovCloud) {                modelID = `${regionPrefix}.${modelID}`              }              break            }            case "eu": {              const regionRequiresPrefix = [                "eu-west-1",                "eu-west-2",                "eu-west-3",                "eu-north-1",                "eu-central-1",                "eu-south-1",                "eu-south-2",              ].some((r) => region.includes(r))              const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "llama3", "pixtral"].some((m) =>                modelID.includes(m),              )              if (regionRequiresPrefix && modelRequiresPrefix) {                modelID = `${regionPrefix}.${modelID}`              }              break            }            case "ap": {              const isAustraliaRegion = ["ap-southeast-2", "ap-southeast-4"].includes(region)              const isTokyoRegion = region === "ap-northeast-1"              if (                isAustraliaRegion &&                ["anthropic.claude-sonnet-4-6", "anthropic.claude-haiku"].some((m) => modelID.includes(m))              ) {                regionPrefix = "au"                modelID = `${regionPrefix}.${modelID}`              } else if (isTokyoRegion) {                // Tokyo region uses jp. prefix for cross-region inference                const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "nova-pro"].some((m) =>                  modelID.includes(m),                )                if (modelRequiresPrefix) {                  regionPrefix = "jp"                  modelID = `${regionPrefix}.${modelID}`                }              } else {                // Other APAC regions use apac. prefix                const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "nova-pro"].some((m) =>                  modelID.includes(m),                )                if (modelRequiresPrefix) {                  regionPrefix = "apac"                  modelID = `${regionPrefix}.${modelID}`                }              }              break            }          }           return sdk.languageModel(modelID)        },      }    },    openrouter: async () => {      // @openrouter/ai-sdk-provider auto-loads OPENROUTER_API_KEY from      // env but NOT OPENROUTER_BASE_URL (unlike @ai-sdk/openai +      // @ai-sdk/anthropic which both auto-load their *_BASE_URL).      // Forward OPENROUTER_BASE_URL explicitly so the SDK targets the      // Atlas managed proxy when sync sets it. Without this, the SDK      // falls back to https://openrouter.ai/api/v1 and tries to use      // our thk_* token as a direct OR key (OR rejects with      // "Missing Authentication header").      const baseURL = Env.get("OPENROUTER_BASE_URL")      return {        autoload: false,        options: {          ...(baseURL ? { baseURL } : {}),          headers: {            "HTTP-Referer": "https://syntheticsciences.ai/",            "X-Title": "synsci",          },        },      }    },    vercel: async () => {      return {        autoload: false,        options: {          headers: {            "http-referer": "https://syntheticsciences.ai/",            "x-title": "synsci",          },        },      }    },    "google-vertex": async () => {      const project = Env.get("GOOGLE_CLOUD_PROJECT") ?? Env.get("GCP_PROJECT") ?? Env.get("GCLOUD_PROJECT")      const location = Env.get("GOOGLE_CLOUD_LOCATION") ?? Env.get("VERTEX_LOCATION") ?? "us-east5"      const autoload = Boolean(project)      if (!autoload) return { autoload: false }      return {        autoload: true,        options: {          project,          location,        },        async getModel(sdk: any, modelID: string) {          const id = String(modelID).trim()          return sdk.languageModel(id)        },      }    },    "google-vertex-anthropic": async () => {      const project = Env.get("GOOGLE_CLOUD_PROJECT") ?? Env.get("GCP_PROJECT") ?? Env.get("GCLOUD_PROJECT")      const location = Env.get("GOOGLE_CLOUD_LOCATION") ?? Env.get("VERTEX_LOCATION") ?? "global"      const autoload = Boolean(project)      if (!autoload) return { autoload: false }      return {        autoload: true,        options: {          project,          location,        },        async getModel(sdk: any, modelID) {          const id = String(modelID).trim()          return sdk.languageModel(id)        },      }    },    "sap-ai-core": async () => {      const auth = await Auth.get("sap-ai-core")      const envServiceKey = iife(() => {        const envAICoreServiceKey = Env.get("AICORE_SERVICE_KEY")        if (envAICoreServiceKey) return envAICoreServiceKey        if (auth?.type === "api") {          Env.set("AICORE_SERVICE_KEY", auth.key)          return auth.key        }        return undefined      })      const deploymentId = Env.get("AICORE_DEPLOYMENT_ID")      const resourceGroup = Env.get("AICORE_RESOURCE_GROUP")       return {        autoload: !!envServiceKey,        options: envServiceKey ? { deploymentId, resourceGroup } : {},        async getModel(sdk: any, modelID: string) {          return sdk(modelID)        },      }    },    zenmux: async () => {      return {        autoload: false,        options: {          headers: {            "HTTP-Referer": "https://syntheticsciences.ai/",            "X-Title": "synsci",          },        },      }    },    gitlab: async (input) => {      const instanceUrl = Env.get("GITLAB_INSTANCE_URL") || "https://gitlab.com"       const auth = await Auth.get(input.id)      const apiKey = await (async () => {        if (auth?.type === "oauth") return auth.access        if (auth?.type === "api") return auth.key        return Env.get("GITLAB_TOKEN")      })()       const config = await Config.get()      const providerConfig = config.provider?.["gitlab"]       return {        autoload: !!apiKey,        options: {          instanceUrl,          apiKey,          featureFlags: {            duo_agent_platform_agentic_chat: true,            duo_agent_platform: true,

Lines 1–500 of 1490.