refactor(providers): the quota functions and the model list fallback read the record

Step 4c of the provider refactor plan. usesServerCredentials asks
resolveEndpoint whose key the call uses, so the 'mirrors each branch of
getAIModel' comment and its switch are gone. usesServerEndpoint reads the
credential kind (none, optionalKey) and the sdk (bedrock) instead of three
provider names. listFallbackUrl reads the credential kind and
publicDefaultUrl. Before this commit the differential test agreed with the
pre-refactor copies on all 30720 and 288 input classes; the copies are
deleted now, and the behavior snapshot keeps both functions' answers for
every recorded scenario.
This commit is contained in:
dayuan.jiang
2026-10-12 21:17:51 +09:00
parent 841efcfea2
commit e0a196dded
3 changed files with 26 additions and 315 deletions
+16 -30
View File
@@ -1203,10 +1203,11 @@ export function getServerProvider(): ProviderName | null {
/**
* Whether a call made with the caller's own settings runs on an endpoint of
* the deployment: its EdgeOne function, the server's keyless Ollama, or an
* the deployment: the site's own function (EdgeOne, credential "none"), the
* server's keyless local provider (Ollama, credential "optionalKey"), or an
* address on the server's network (which ignores a dummy key header).
* Bedrock and EdgeOne never use a client base URL. Never in the desktop
* app, where every endpoint is the user's. clientBaseUrl: normalized.
* Bedrock never uses a client base URL. Never in the desktop app, where
* every endpoint is the user's. clientBaseUrl: normalized.
*/
export async function usesServerEndpoint(
provider: ProviderName | null | undefined,
@@ -1214,10 +1215,13 @@ export async function usesServerEndpoint(
apiKey: string | null | undefined,
): Promise<boolean> {
if (process.env.NEXT_AI_DRAWIO_DESKTOP === "1") return false
if (provider === "edgeone") return true
if (provider === "ollama" && !clientBaseUrl && !apiKey) return true
const def = provider ? providerDef(provider) : undefined
if (def?.credential === "none") return true
if (def?.credential === "optionalKey" && !clientBaseUrl && !apiKey) {
return true
}
return (
provider !== "bedrock" &&
def?.sdk !== "bedrock" &&
!!clientBaseUrl &&
(await isPrivateUrl(clientBaseUrl))
)
@@ -1225,8 +1229,8 @@ export async function usesServerEndpoint(
/**
* Whether the call is paid for by the server's own credentials (env keys or
* IAM role) rather than credentials sent with the request. Mirrors which key
* each branch of getAIModel ends up using.
* IAM role) rather than credentials sent with the request: the key
* resolveEndpoint picks for getAIModel is the server's.
*/
export function usesServerCredentials(
provider: ProviderName,
@@ -1236,28 +1240,10 @@ export function usesServerCredentials(
if (process.env.NEXT_AI_DRAWIO_DESKTOP === "1") return false
// Cleaned like getAIModel does: "/" means no base URL
const baseUrl = normalizeBaseUrl(overrides?.baseUrl ?? "")
switch (provider) {
case "bedrock":
return !(
overrides?.apiKey ||
(overrides?.awsAccessKeyId && overrides?.awsSecretAccessKey)
)
case "vertexai":
return !overrides?.vertexApiKey
case "edgeone":
// The platform's own endpoint, no key involved
return false
case "ollama":
// Only a server key costs money; a keyless local server or the
// client's own server does not
return (
!baseUrl &&
!overrides?.apiKey &&
!!(overrides?.apiKeyEnv || process.env.OLLAMA_API_KEY)
)
default:
return !overrides?.apiKey
}
return (
resolveEndpoint(provider, { ...overrides, baseUrl }).keySource ===
"server"
)
}
/**
+10 -18
View File
@@ -1,12 +1,8 @@
import { createGateway } from "ai"
import { getModelInfo } from "@/lib/model-catalog"
import { PROVIDERS, type ProviderName, publicDefaultUrl } from "@/lib/providers"
import { readLimitedBody } from "@/lib/read-limited-body"
import {
normalizeBaseUrl,
ollamaApiUrl,
PROVIDER_INFO,
type ProviderName,
} from "@/lib/types/model-config"
import { normalizeBaseUrl, ollamaApiUrl } from "@/lib/types/model-config"
/** A model a provider offers. tools is false when it cannot call tools. */
export interface ListedModel {
@@ -17,10 +13,7 @@ export interface ListedModel {
const AIHUBMIX_MODELS_ENDPOINT = "https://aihubmix.com/api/v1/models"
export function canListModels(provider: ProviderName): boolean {
return (
Object.hasOwn(PROVIDER_INFO, provider) &&
!!PROVIDER_INFO[provider].modelList
)
return Object.hasOwn(PROVIDERS, provider) && !!PROVIDERS[provider].modelList
}
// Models in OpenAI-style lists that are not for chat
@@ -131,16 +124,15 @@ async function getJson(
/**
* Where to list from without the user's base URL: where chat goes then. For
* Ollama without a key that is the server's Ollama, else the SDK's local
* default; a local default in PROVIDER_INFO (SGLang's) only fills the
* settings form.
* Ollama (an optional key) without a key that is the server's Ollama, else
* the SDK's local default; a local default in the record (SGLang's) only
* fills the settings form.
*/
function listFallbackUrl(provider: ProviderName, apiKey?: string): string {
if (provider === "ollama" && !apiKey) {
if (PROVIDERS[provider].credential === "optionalKey" && !apiKey) {
return process.env.OLLAMA_BASE_URL || "http://127.0.0.1:11434/api"
}
const url = PROVIDER_INFO[provider].defaultBaseUrl
return url?.startsWith("https://") ? url : ""
return publicDefaultUrl(provider) ?? ""
}
/**
@@ -167,7 +159,7 @@ export async function listProviderModels(
baseUrl &&
!/^https:\/\/aihubmix\.com(\/v1)?$/.test(base)
? "openai"
: PROVIDER_INFO[provider].modelList
: PROVIDERS[provider].modelList
switch (style) {
case "anthropic": {
@@ -242,7 +234,7 @@ export async function listProviderModels(
default: {
if (!base) {
throw new ModelListError(
`${PROVIDER_INFO[provider].label} needs a base URL to list its models.`,
`${PROVIDERS[provider].label} needs a base URL to list its models.`,
)
}
const data = await getJson(`${base}/models`, bearer, fetchFn)