mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-12 12:39:52 +08:00
fix(server): use the keys the user sent, and more review fixes
Found by the second PR review: - With AWS_BEARER_TOKEN_BEDROCK set on the server, a request with the user's AWS keys ran on the server's token: the Bedrock SDK prefers it. Checked with Bedrock: invalid user keys used to get an answer. - An OpenAI key with the official URL filled in (the settings form does that) went to the Responses API. Back to main's rule: a configured base URL uses Chat Completions. - A user's Ollama key went to the server's OLLAMA_BASE_URL, for chat and for the model list. Like every other provider, it goes to the user's base URL or Ollama Cloud. - The server's keyless Ollama and EdgeOne were not counted in the quota. - AI_MODEL models on the server's keys ran on any provider with a server key, not only on AI_PROVIDER. - A user's Azure key without a base URL used the server's resource name. - The admin panel's Test button failed whenever access codes were set. - DeepSeek's errors in the stream (plain text) were shown as they were, without a hint and also on the server's keys. Bedrock's throttling in the stream was not recognised as a rate limit. - The EdgeOne function accepted text/plain; x=application/json, which other sites can send without a CORS preflight. - Desktop app: a launch that found the old port taken for a moment (the previous version still quitting after an update) remembered the new port for good. The new port is kept only when Windows reserves the old one. A failed read of the presets file moved it aside as corrupt, and a save could then replace the presets. Switching presets on the same port now reloads the page. The dev launcher no longer misses a preset change made before or during a restart.
This commit is contained in:
+36
-12
@@ -639,6 +639,8 @@ interface Endpoint {
|
||||
fetch?: typeof fetch
|
||||
authToken?: string // Anthropic Bearer auth
|
||||
resourceName?: string // Azure
|
||||
// baseURL comes from the settings or env, not the provider's default
|
||||
configuredBaseURL?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -679,11 +681,10 @@ function createModel(
|
||||
switch (provider) {
|
||||
case "openai": {
|
||||
const openaiProvider = createOpenAI(opts)
|
||||
// A custom base URL is usually a proxy that only has Chat
|
||||
// Completions; the official endpoint uses the Responses API,
|
||||
// which returns reasoning for the o-series and gpt-5 or later
|
||||
return e.baseURL &&
|
||||
e.baseURL !== PROVIDER_INFO.openai.defaultBaseUrl
|
||||
// A configured base URL is usually a proxy that only has Chat
|
||||
// Completions; without one the Responses API is used, which
|
||||
// returns reasoning for the o-series and gpt-5 or later
|
||||
return e.configuredBaseURL
|
||||
? openaiProvider.chat(modelId)
|
||||
: openaiProvider(modelId)
|
||||
}
|
||||
@@ -899,6 +900,9 @@ export function getAIModel(clientOverrides?: ClientOverrides): ModelConfig {
|
||||
...(overrides?.awsSessionToken && {
|
||||
sessionToken: overrides.awsSessionToken,
|
||||
}),
|
||||
// Without an apiKey the SDK reads the server's
|
||||
// AWS_BEARER_TOKEN_BEDROCK, which wins over the keys
|
||||
apiKey: "",
|
||||
})
|
||||
: adminAccessKeyId && adminSecretAccessKey
|
||||
? createAmazonBedrock({
|
||||
@@ -955,7 +959,13 @@ export function getAIModel(clientOverrides?: ClientOverrides): ModelConfig {
|
||||
}
|
||||
|
||||
case "ollama": {
|
||||
const baseURL = overrides?.baseUrl || process.env.OLLAMA_BASE_URL
|
||||
// Like other providers, a user's key never goes to the server's
|
||||
// base URL; without a base URL it is an Ollama Cloud key
|
||||
const baseURL =
|
||||
overrides?.baseUrl ||
|
||||
(overrides?.apiKey
|
||||
? PROVIDER_INFO.ollama.defaultBaseUrl
|
||||
: process.env.OLLAMA_BASE_URL)
|
||||
// SECURITY: When client provides a custom base URL, only use
|
||||
// client-provided API key. Never fall back to server OLLAMA_API_KEY
|
||||
// to prevent leaking server credentials to user-controlled endpoints.
|
||||
@@ -1003,16 +1013,24 @@ export function getAIModel(clientOverrides?: ClientOverrides): ModelConfig {
|
||||
const publicDefault = defaultUrl?.startsWith("https://")
|
||||
? defaultUrl
|
||||
: undefined
|
||||
const baseURL = resolveBaseURL(
|
||||
const configuredBaseURL = resolveBaseURL(
|
||||
overrides?.apiKey,
|
||||
overrides?.baseUrl,
|
||||
resolveBaseUrlEnv(overrides, baseUrlEnv),
|
||||
SDK_KNOWS_ENDPOINT.has(provider) &&
|
||||
!(provider === "openai" && overrides?.apiKey)
|
||||
? undefined
|
||||
: publicDefault,
|
||||
)
|
||||
if (!baseURL && !SDK_KNOWS_ENDPOINT.has(provider)) {
|
||||
const baseURL =
|
||||
configuredBaseURL ||
|
||||
(SDK_KNOWS_ENDPOINT.has(provider) &&
|
||||
!(provider === "openai" && overrides?.apiKey)
|
||||
? undefined
|
||||
: publicDefault)
|
||||
// With a user's Azure key the SDK would read the server's
|
||||
// AZURE_RESOURCE_NAME
|
||||
if (
|
||||
!baseURL &&
|
||||
(!SDK_KNOWS_ENDPOINT.has(provider) ||
|
||||
(provider === "azure" && overrides?.apiKey))
|
||||
) {
|
||||
throw new Error(
|
||||
`${PROVIDER_INFO[provider].label} needs a base URL. Add it in the model settings.`,
|
||||
)
|
||||
@@ -1020,6 +1038,7 @@ export function getAIModel(clientOverrides?: ClientOverrides): ModelConfig {
|
||||
model = createModel(provider, modelId, {
|
||||
apiKey,
|
||||
baseURL,
|
||||
configuredBaseURL: !!configuredBaseURL,
|
||||
fetch: guardedFetch,
|
||||
// Bearer auth for Anthropic when there is no API key
|
||||
authToken:
|
||||
@@ -1038,6 +1057,11 @@ export function getAIModel(clientOverrides?: ClientOverrides): ModelConfig {
|
||||
return { model, providerOptions, modelId, provider }
|
||||
}
|
||||
|
||||
/** The provider of the server's own config: AI_PROVIDER, or the one with a key */
|
||||
export function getServerProvider(): ProviderName | null {
|
||||
return (process.env.AI_PROVIDER as ProviderName) || detectProvider()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
+12
-3
@@ -80,7 +80,8 @@ const GENERAL_TEXTS: Array<[RegExp, LLMErrorCode]> = [
|
||||
/invalid[_ ]api[_ ]key|incorrect api key|unauthorized/i,
|
||||
"invalid_api_key",
|
||||
],
|
||||
[/rate limit|too many requests/i, "rate_limited"],
|
||||
// "too many tokens": Bedrock's throttling
|
||||
[/rate limit|too many requests|too many tokens/i, "rate_limited"],
|
||||
[
|
||||
/Cannot connect to API|ECONNREFUSED|ENOTFOUND|ECONNRESET|ETIMEDOUT|fetch failed/i,
|
||||
"cannot_connect",
|
||||
@@ -115,8 +116,16 @@ function problemDetail(body: string): string | undefined {
|
||||
* it can name the server's account, role or internal hosts.
|
||||
*/
|
||||
export function streamErrorText(error: unknown, hideDetails = false): string {
|
||||
// The SDK passes an invalid tool call's error as a plain string
|
||||
if (typeof error === "string") return error
|
||||
// The SDK passes an invalid tool call's error as a plain string. Other
|
||||
// strings come from providers (DeepSeek's SDK sends stream errors so).
|
||||
if (
|
||||
typeof error === "string" &&
|
||||
/^(Invalid input for tool|Model tried to call unavailable tool)/.test(
|
||||
error,
|
||||
)
|
||||
) {
|
||||
return error
|
||||
}
|
||||
if (isToolCallError(error)) return (error as Error).message
|
||||
const classified = classifyLLMError(error)
|
||||
if (hideDetails) {
|
||||
|
||||
@@ -77,11 +77,12 @@ async function getJson(
|
||||
|
||||
/**
|
||||
* Where to list from without the user's base URL: where chat goes then. For
|
||||
* Ollama 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 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.
|
||||
*/
|
||||
function listFallbackUrl(provider: ProviderName): string {
|
||||
if (provider === "ollama") {
|
||||
function listFallbackUrl(provider: ProviderName, apiKey?: string): string {
|
||||
if (provider === "ollama" && !apiKey) {
|
||||
return process.env.OLLAMA_BASE_URL || "http://127.0.0.1:11434/api"
|
||||
}
|
||||
const url = PROVIDER_INFO[provider].defaultBaseUrl
|
||||
@@ -98,7 +99,7 @@ export async function listProviderModels(
|
||||
{ apiKey, baseUrl }: { apiKey?: string; baseUrl?: string },
|
||||
fetchFn: typeof fetch = fetch,
|
||||
): Promise<ListedModel[]> {
|
||||
const base = normalizeBaseUrl(baseUrl || listFallbackUrl(provider))
|
||||
const base = normalizeBaseUrl(baseUrl || listFallbackUrl(provider, apiKey))
|
||||
const bearer: Record<string, string> = apiKey
|
||||
? { Authorization: `Bearer ${apiKey}` }
|
||||
: {}
|
||||
|
||||
Reference in New Issue
Block a user