mirror of
https://github.com/harivansh-afk/clanker-agent.git
synced 2026-04-15 09:01:13 +00:00
- Copy all pi-mono source into apps/companion-os/ - Update Dockerfile to COPY pre-built binary instead of downloading from GitHub Releases - Update deploy-staging.yml to build pi from source (bun compile) before Docker build - Add apps/companion-os/** to path triggers - No more cross-repo dispatch needed Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
150 lines
3.8 KiB
TypeScript
150 lines
3.8 KiB
TypeScript
import type {
|
|
Api,
|
|
Context,
|
|
Model,
|
|
SimpleStreamOptions,
|
|
} from "@mariozechner/pi-ai";
|
|
import { streamSimple } from "@mariozechner/pi-ai";
|
|
|
|
/**
|
|
* Centralized proxy decision logic.
|
|
*
|
|
* Determines whether to use a CORS proxy for LLM API requests based on:
|
|
* - Provider name
|
|
* - API key pattern (for providers where it matters)
|
|
*/
|
|
|
|
/**
|
|
* Check if a provider/API key combination requires a CORS proxy.
|
|
*
|
|
* @param provider - Provider name (e.g., "anthropic", "openai", "zai")
|
|
* @param apiKey - API key for the provider
|
|
* @returns true if proxy is required, false otherwise
|
|
*/
|
|
export function shouldUseProxyForProvider(
|
|
provider: string,
|
|
apiKey: string,
|
|
): boolean {
|
|
switch (provider.toLowerCase()) {
|
|
case "zai":
|
|
// Z-AI always requires proxy
|
|
return true;
|
|
|
|
case "anthropic":
|
|
// Anthropic OAuth tokens (sk-ant-oat-*) require proxy
|
|
// Regular API keys (sk-ant-api-*) do NOT require proxy
|
|
return apiKey.startsWith("sk-ant-oat");
|
|
|
|
// These providers work without proxy
|
|
case "openai":
|
|
case "google":
|
|
case "groq":
|
|
case "openrouter":
|
|
case "cerebras":
|
|
case "xai":
|
|
case "ollama":
|
|
case "lmstudio":
|
|
return false;
|
|
|
|
// Unknown providers - assume no proxy needed
|
|
// This allows new providers to work by default
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Apply CORS proxy to a model's baseUrl if needed.
|
|
*
|
|
* @param model - The model to potentially proxy
|
|
* @param apiKey - API key for the provider
|
|
* @param proxyUrl - CORS proxy URL (e.g., "https://proxy.mariozechner.at/proxy")
|
|
* @returns Model with modified baseUrl if proxy is needed, otherwise original model
|
|
*/
|
|
export function applyProxyIfNeeded<T extends Api>(
|
|
model: Model<T>,
|
|
apiKey: string,
|
|
proxyUrl?: string,
|
|
): Model<T> {
|
|
// If no proxy URL configured, return original model
|
|
if (!proxyUrl) {
|
|
return model;
|
|
}
|
|
|
|
// If model has no baseUrl, can't proxy it
|
|
if (!model.baseUrl) {
|
|
return model;
|
|
}
|
|
|
|
// Check if this provider/key needs proxy
|
|
if (!shouldUseProxyForProvider(model.provider, apiKey)) {
|
|
return model;
|
|
}
|
|
|
|
// Apply proxy to baseUrl
|
|
return {
|
|
...model,
|
|
baseUrl: `${proxyUrl}/?url=${encodeURIComponent(model.baseUrl)}`,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Check if an error is likely a CORS error.
|
|
*
|
|
* CORS errors in browsers typically manifest as:
|
|
* - TypeError with message "Failed to fetch"
|
|
* - NetworkError
|
|
*
|
|
* @param error - The error to check
|
|
* @returns true if error is likely a CORS error
|
|
*/
|
|
export function isCorsError(error: unknown): boolean {
|
|
if (!(error instanceof Error)) {
|
|
return false;
|
|
}
|
|
|
|
// Check for common CORS error patterns
|
|
const message = error.message.toLowerCase();
|
|
|
|
// "Failed to fetch" is the standard CORS error in most browsers
|
|
if (error.name === "TypeError" && message.includes("failed to fetch")) {
|
|
return true;
|
|
}
|
|
|
|
// Some browsers report "NetworkError"
|
|
if (error.name === "NetworkError") {
|
|
return true;
|
|
}
|
|
|
|
// CORS-specific messages
|
|
if (message.includes("cors") || message.includes("cross-origin")) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Create a streamFn that applies CORS proxy when needed.
|
|
* Reads proxy settings from storage on each call.
|
|
*
|
|
* @param getProxyUrl - Async function to get current proxy URL (or undefined if disabled)
|
|
* @returns A streamFn compatible with Agent's streamFn option
|
|
*/
|
|
export function createStreamFn(getProxyUrl: () => Promise<string | undefined>) {
|
|
return async (
|
|
model: Model<any>,
|
|
context: Context,
|
|
options?: SimpleStreamOptions,
|
|
) => {
|
|
const apiKey = options?.apiKey;
|
|
const proxyUrl = await getProxyUrl();
|
|
|
|
if (!apiKey || !proxyUrl) {
|
|
return streamSimple(model, context, options);
|
|
}
|
|
|
|
const proxiedModel = applyProxyIfNeeded(model, apiKey, proxyUrl);
|
|
return streamSimple(proxiedModel, context, options);
|
|
};
|
|
}
|