fix(coding-agent): resolved Kagi, TTS, and model-discovery bearers through withAuth

This commit is contained in:
can1357
2026-06-12 02:17:06 +02:00
parent 503b2b7156
commit 49cdf56993
5 changed files with 132 additions and 95 deletions
@@ -5,7 +5,7 @@
* `discoverModelsByProviderType` with a `DiscoveryContext`; built-in provider
* discovery lives in pi-catalog's provider-models.
*/
import type { FetchImpl } from "@oh-my-pi/pi-ai";
import { type ApiKey, type FetchImpl, withAuth } from "@oh-my-pi/pi-ai";
import type { Api, Model } from "@oh-my-pi/pi-ai/types";
import { buildModel } from "@oh-my-pi/pi-catalog/build";
import {
@@ -97,10 +97,12 @@ export interface DiscoveryContext {
/** Injected fetch implementation (tests stub this). */
fetch: FetchImpl;
/**
* Resolve a provider's API key for `Authorization: Bearer …`. Returns
* undefined when no key is stored or it is a local/no-auth sentinel.
* Resolve a provider's bearer credential for `Authorization: Bearer …`.
* Returns undefined when no key is stored or it is a local/no-auth
* sentinel; otherwise an {@link ApiKey} whose resolver participates in the
* central force-refresh/rotate auth-retry policy on 401/usage-limit.
*/
getBearerApiKey(provider: string): Promise<string | undefined>;
getBearerApiKeyResolver(provider: string): Promise<ApiKey | undefined>;
}
type OllamaDiscoveredModelMetadata = {
@@ -314,22 +316,26 @@ export async function discoverLlamaCppModels(
const baseUrl = normalizeLlamaCppBaseUrl(providerConfig.baseUrl);
const modelsUrl = `${baseUrl}/models`;
const headers: Record<string, string> = { ...(providerConfig.headers ?? {}) };
const apiKey = await ctx.getBearerApiKey(providerConfig.provider);
if (apiKey) {
headers.Authorization = `Bearer ${apiKey}`;
}
const [response, serverMetadata] = await Promise.all([
ctx.fetch(modelsUrl, {
headers,
signal: AbortSignal.timeout(250),
}),
discoverLlamaCppServerMetadata(ctx, baseUrl, headers),
]);
if (!response.ok) {
throw new Error(`HTTP ${response.status} from ${modelsUrl}`);
}
const baseHeaders: Record<string, string> = { ...(providerConfig.headers ?? {}) };
let headers = baseHeaders;
const attempt = async (h: Record<string, string>) => {
const [response, metadata] = await Promise.all([
ctx.fetch(modelsUrl, {
headers: h,
signal: AbortSignal.timeout(250),
}),
discoverLlamaCppServerMetadata(ctx, baseUrl, h),
]);
if (!response.ok) {
throw new Error(`HTTP ${response.status} from ${modelsUrl}`);
}
headers = h;
return [response, metadata] as const;
};
const apiKey = await ctx.getBearerApiKeyResolver(providerConfig.provider);
const [response, serverMetadata] = apiKey
? await withAuth(apiKey, key => attempt({ ...baseHeaders, Authorization: `Bearer ${key}` }))
: await attempt(baseHeaders);
const payload = (await response.json()) as { data?: Array<{ id: string }> };
const models = payload.data ?? [];
const discovered: Model<Api>[] = [];
@@ -370,19 +376,23 @@ export async function discoverOpenAIModelsList(
const baseUrl = normalizeOpenAIModelsListBaseUrl(providerConfig.baseUrl);
const modelsUrl = `${baseUrl}/models`;
const headers: Record<string, string> = { ...(providerConfig.headers ?? {}) };
const apiKey = await ctx.getBearerApiKey(providerConfig.provider);
if (apiKey) {
headers.Authorization = `Bearer ${apiKey}`;
}
const response = await ctx.fetch(modelsUrl, {
headers,
signal: AbortSignal.timeout(10_000),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status} from ${modelsUrl}`);
}
const baseHeaders: Record<string, string> = { ...(providerConfig.headers ?? {}) };
let headers = baseHeaders;
const attempt = async (h: Record<string, string>) => {
const res = await ctx.fetch(modelsUrl, {
headers: h,
signal: AbortSignal.timeout(10_000),
});
if (!res.ok) {
throw new Error(`HTTP ${res.status} from ${modelsUrl}`);
}
headers = h;
return res;
};
const apiKey = await ctx.getBearerApiKeyResolver(providerConfig.provider);
const response = apiKey
? await withAuth(apiKey, key => attempt({ ...baseHeaders, Authorization: `Bearer ${key}` }))
: await attempt(baseHeaders);
const payload = (await response.json()) as { data?: Array<{ id: string }> };
const models = payload.data ?? [];
const discovered: Model<Api>[] = [];
@@ -435,19 +445,23 @@ export async function discoverProxyModels(
const baseUrl = normalizeOpenAIModelsListBaseUrl(providerConfig.baseUrl);
const modelsUrl = `${baseUrl}/models`;
const headers: Record<string, string> = { ...(providerConfig.headers ?? {}) };
const apiKey = await ctx.getBearerApiKey(providerConfig.provider);
if (apiKey) {
headers.Authorization = `Bearer ${apiKey}`;
}
const response = await ctx.fetch(modelsUrl, {
headers,
signal: AbortSignal.timeout(10_000),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status} from ${modelsUrl}`);
}
const baseHeaders: Record<string, string> = { ...(providerConfig.headers ?? {}) };
let headers = baseHeaders;
const attempt = async (h: Record<string, string>) => {
const res = await ctx.fetch(modelsUrl, {
headers: h,
signal: AbortSignal.timeout(10_000),
});
if (!res.ok) {
throw new Error(`HTTP ${res.status} from ${modelsUrl}`);
}
headers = h;
return res;
};
const apiKey = await ctx.getBearerApiKeyResolver(providerConfig.provider);
const response = apiKey
? await withAuth(apiKey, key => attempt({ ...baseHeaders, Authorization: `Bearer ${key}` }))
: await attempt(baseHeaders);
const payload = (await response.json()) as {
data?: Array<{ id?: string; name?: string; supported_endpoint_types?: string[] }>;
};
@@ -1238,9 +1238,10 @@ export class ModelRegistry {
#discoveryContext(): DiscoveryContext {
return {
fetch: this.#fetch,
getBearerApiKey: async provider => {
getBearerApiKeyResolver: async provider => {
const apiKey = await this.getApiKeyForProvider(provider);
return apiKey && apiKey !== DEFAULT_LOCAL_TOKEN && apiKey !== kNoAuth ? apiKey : undefined;
if (!apiKey || apiKey === DEFAULT_LOCAL_TOKEN || apiKey === kNoAuth) return undefined;
return this.resolver(provider);
},
};
}
+40 -20
View File
@@ -1,6 +1,7 @@
// Ported from NousResearch/hermes-agent (MIT) — tools/tts_tool.py L167-171, L896-959.
import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
import { type ApiKey, ProviderHttpError, withAuth } from "@oh-my-pi/pi-ai";
import * as z from "zod/v4";
import type { CustomTool, CustomToolContext } from "../extensibility/custom-tools/types";
import { ohMyPiXAIUserAgent, resolveXAIHttpCredentials } from "../lib/xai-http";
@@ -96,27 +97,46 @@ export const ttsTool: CustomTool<typeof ttsSchema, TtsToolDetails> = {
const timeoutSignal = AbortSignal.timeout(60_000);
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
const response = await fetch(`${creds.baseURL}/tts`, {
method: "POST",
headers: {
Authorization: `Bearer ${creds.apiKey}`,
"Content-Type": "application/json",
"User-Agent": ohMyPiXAIUserAgent(),
},
body: JSON.stringify(payload),
signal: combinedSignal,
const sessionId = ctx.sessionManager.getSessionId();
const apiKey: ApiKey = ctx.modelRegistry.resolver(creds.provider, {
sessionId,
baseUrl: creds.baseURL,
});
if (!response.ok) {
const detail = await response.text();
return {
isError: true,
content: [
{
type: "text",
text: `xAI TTS failed (${response.status}): ${detail.slice(0, 300)}`,
},
],
};
let response: Response;
try {
response = await withAuth(
apiKey,
async key => {
const resp = await fetch(`${creds.baseURL}/tts`, {
method: "POST",
headers: {
Authorization: `Bearer ${key}`,
"Content-Type": "application/json",
"User-Agent": ohMyPiXAIUserAgent(),
},
body: JSON.stringify(payload),
signal: combinedSignal,
});
if (!resp.ok) {
const detail = await resp.text();
throw new ProviderHttpError(`xAI TTS failed (${resp.status}): ${detail.slice(0, 300)}`, resp.status, {
headers: resp.headers,
});
}
return resp;
},
{ signal: combinedSignal },
);
} catch (error) {
const status = (error as { status?: unknown }).status;
if (error instanceof Error && typeof status === "number") {
return {
isError: true,
content: [{ type: "text", text: error.message }],
};
}
throw error;
}
const bytes = new Uint8Array(await response.arrayBuffer());
await Bun.write(outputPath, bytes);
+26 -27
View File
@@ -6,7 +6,7 @@
* through the shared {@link AuthStorage} broker (Bearer token), and responses
* are categorized result buckets rather than the legacy flat object array.
*/
import type { AuthStorage, FetchImpl } from "@oh-my-pi/pi-ai";
import { type AuthStorage, type FetchImpl, withAuth } from "@oh-my-pi/pi-ai";
import { withHardTimeout } from "./search/providers/utils";
const KAGI_SEARCH_URL = "https://kagi.com/api/v1/search";
@@ -173,14 +173,6 @@ export interface KagiSearchResult {
answer?: string;
}
export async function findKagiApiKey(
authStorage: AuthStorage,
sessionId?: string,
signal?: AbortSignal,
): Promise<string | null> {
return (await authStorage.getApiKey("kagi", sessionId, { signal })) ?? null;
}
/**
* Compute a YYYY-MM-DD date string `recency` units before now, in UTC.
* UTC keeps the recency window deterministic regardless of host timezone and
@@ -247,27 +239,34 @@ export async function searchWithKagi(
options: KagiSearchOptions = {},
authStorage: AuthStorage,
): Promise<KagiSearchResult> {
const apiKey = await findKagiApiKey(authStorage, options.sessionId, options.signal);
if (!apiKey) {
throw new KagiApiError("Kagi credentials not found. Set KAGI_API_KEY or login with 'omp /login kagi'.");
}
const fetchImpl = options.fetch ?? fetch;
const body = JSON.stringify(buildRequestBody(query, options));
const response = await fetchImpl(KAGI_SEARCH_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
Accept: "application/json",
const response = await withAuth(
authStorage.resolver("kagi", { sessionId: options.sessionId }),
async apiKey => {
const res = await fetchImpl(KAGI_SEARCH_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
Accept: "application/json",
},
body,
signal: withHardTimeout(options.signal),
});
if (!res.ok) {
throw parseKagiErrorResponse(res.status, await res.text());
}
return res;
},
body: JSON.stringify(buildRequestBody(query, options)),
signal: withHardTimeout(options.signal),
});
if (!response.ok) {
throw parseKagiErrorResponse(response.status, await response.text());
}
{
signal: options.signal,
missingKeyMessage: "Kagi credentials not found. Set KAGI_API_KEY or login with 'omp /login kagi'.",
},
);
const payload = (await response.json()) as KagiSearchResponse;
if (payload.error && payload.error.length > 0) {
@@ -8,6 +8,9 @@ const fakeAuthStorage = {
async getApiKey() {
return process.env.KAGI_API_KEY ?? undefined;
},
resolver() {
return async () => process.env.KAGI_API_KEY ?? undefined;
},
hasAuth() {
return Boolean(process.env.KAGI_API_KEY);
},