UNPKG

langsmith

Version:

Client library to connect to the LangSmith Observability and Evaluation Platform.

254 lines (253 loc) 8.41 kB
import { getEnv, getEnvironmentVariable } from "./env.js"; import * as fsUtils from "./fs.js"; export const DEFAULT_API_URL = "https://api.smith.langchain.com"; const OAUTH_CLIENT_ID = "langsmith-cli"; const TOKEN_REFRESH_LEEWAY_MS = 60_000; const TOKEN_REFRESH_TIMEOUT_MS = 10_000; function isBrowserLikeRuntime() { const env = getEnv(); return env === "browser" || env === "webworker"; } function getProfileConfigPath() { const explicitPath = getEnvironmentVariable("LANGSMITH_CONFIG_FILE"); if (explicitPath) { return explicitPath; } const home = getEnvironmentVariable("HOME") ?? getEnvironmentVariable("USERPROFILE"); if (!home) { return undefined; } return fsUtils.path.join(home, ".langsmith", "config.json"); } function resolveProfileName(config) { const envProfile = getEnvironmentVariable("LANGSMITH_PROFILE"); if (envProfile) { return envProfile; } if (config.current_profile) { return config.current_profile; } if (config.profiles?.default) { return "default"; } return undefined; } function loadProfileState() { if (isBrowserLikeRuntime()) { return undefined; } const configPath = getProfileConfigPath(); if (!configPath || !fsUtils.existsSync(configPath)) { return undefined; } try { const config = JSON.parse(fsUtils.readFileSync(configPath)); const profileName = resolveProfileName(config); const profile = profileName ? config.profiles?.[profileName] : undefined; if (!profileName || !profile) { return undefined; } return { configPath, config, profileName, profile }; } catch { return undefined; } } export function hasValue(value) { return value !== undefined && value !== null && value.trim() !== ""; } function trimConfigValue(value) { return value?.trim().replace(/^["']|["']$/g, ""); } function shouldRefreshProfileToken(profile) { const oauth = profile.oauth; if (!oauth?.refresh_token) { return false; } if (!oauth.access_token) { return true; } if (!oauth.expires_at) { return false; } const expiresAt = Date.parse(oauth.expires_at); if (Number.isNaN(expiresAt)) { return false; } return expiresAt <= Date.now() + TOKEN_REFRESH_LEEWAY_MS; } function normalizeConfigUrl(apiUrl) { let normalized = apiUrl; while (normalized.endsWith("/")) { normalized = normalized.slice(0, -1); } const apiV1Suffix = "/api/v1"; return normalized.endsWith(apiV1Suffix) ? normalized.slice(0, -apiV1Suffix.length) : normalized; } function applyTokenResponse(profile, token) { profile.oauth ??= {}; if (token.access_token) { profile.oauth.access_token = token.access_token; } if (token.refresh_token) { profile.oauth.refresh_token = token.refresh_token; } if (typeof token.expires_in === "number" && token.expires_in > 0) { profile.oauth.expires_at = new Date(Date.now() + token.expires_in * 1000).toISOString(); } } function getAbortReason(signal) { return (signal.reason ?? new Error("The operation was aborted.")); } async function waitForAbortSignal(promise, signal) { if (!signal) { return promise; } if (signal.aborted) { throw getAbortReason(signal); } let cleanup; const abortPromise = new Promise((_, reject) => { const onAbort = () => { reject(getAbortReason(signal)); }; signal.addEventListener("abort", onAbort, { once: true }); cleanup = () => { signal.removeEventListener("abort", onAbort); }; }); try { return await Promise.race([promise, abortPromise]); } finally { cleanup?.(); } } export function loadProfileClientConfig() { const state = loadProfileState(); const profile = state?.profile; if (!state || !profile) { return {}; } const apiKey = trimConfigValue(profile.api_key); const oauthAccessToken = trimConfigValue(profile.oauth?.access_token); const oauthRefreshToken = trimConfigValue(profile.oauth?.refresh_token); return { apiUrl: profile.api_url, apiKey, workspaceId: profile.workspace_id, oauthAccessToken, oauthRefreshToken, profileAuth: apiKey || oauthAccessToken || oauthRefreshToken ? new ProfileAuth(state) : undefined, }; } export class ProfileAuth { constructor(state) { Object.defineProperty(this, "state", { enumerable: true, configurable: true, writable: true, value: state }); Object.defineProperty(this, "refreshPromise", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "managedAuthorizationValue", { enumerable: true, configurable: true, writable: true, value: void 0 }); this.rememberProfileAuthHeader(this.currentAuthHeader()); } currentAuthHeader() { const header = currentAuthHeaderFromProfile(this.state.profile); this.rememberProfileAuthHeader(header); return header; } async getAuthHeader(fetchImplementation, signal) { if (shouldRefreshProfileToken(this.state.profile)) { if (!this.refreshPromise) { this.refreshPromise = this.refreshOAuthToken(fetchImplementation).finally(() => { this.refreshPromise = undefined; }); } await waitForAbortSignal(this.refreshPromise, signal); } const header = authHeaderFromProfile(this.state.profile); this.rememberProfileAuthHeader(header); return header; } isProfileAuthorizationHeader(value) { return value === this.managedAuthorizationValue; } async refreshOAuthToken(fetchImplementation) { const refreshToken = this.state.profile.oauth?.refresh_token; if (!refreshToken) { return; } const refreshApiUrl = trimConfigValue(this.state.profile.api_url) ?? DEFAULT_API_URL; try { const body = new URLSearchParams({ grant_type: "refresh_token", client_id: OAUTH_CLIENT_ID, refresh_token: refreshToken, }); const response = await fetchImplementation(`${normalizeConfigUrl(refreshApiUrl)}/oauth/token`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: body.toString(), signal: AbortSignal.timeout(TOKEN_REFRESH_TIMEOUT_MS), }); if (!response.ok) { return; } const token = (await response.json()); if (!token.access_token) { return; } applyTokenResponse(this.state.profile, token); this.state.config.profiles ??= {}; this.state.config.profiles[this.state.profileName] = this.state.profile; await fsUtils.writeFileAtomic(this.state.configPath, `${JSON.stringify(this.state.config, null, 2)}\n`); } catch { return; } } rememberProfileAuthHeader(header) { this.managedAuthorizationValue = header?.name === "Authorization" ? header.value : undefined; } } function currentAuthHeaderFromProfile(profile) { const oauthAccessToken = trimConfigValue(profile.oauth?.access_token); if (oauthAccessToken) { return { name: "Authorization", value: `Bearer ${oauthAccessToken}` }; } if (trimConfigValue(profile.oauth?.refresh_token)) { return undefined; } return authHeaderFromProfile(profile); } function authHeaderFromProfile(profile) { const oauthAccessToken = trimConfigValue(profile.oauth?.access_token); if (oauthAccessToken) { return { name: "Authorization", value: `Bearer ${oauthAccessToken}` }; } const apiKey = trimConfigValue(profile.api_key); if (apiKey) { return { name: "x-api-key", value: apiKey }; } return undefined; }