From edeb344ad992bc9ccc2e573003b7dfa53efc108e Mon Sep 17 00:00:00 2001 From: cthomas Date: Tue, 27 Jan 2026 15:02:01 -0800 Subject: [PATCH] fix: always populate user-agent (#708) --- src/agent/http-headers.ts | 24 +++++++++++++++++++ src/cli/App.tsx | 17 ++++--------- src/cli/helpers/mcpOauth.ts | 8 ++----- src/index.ts | 4 ++-- src/providers/byok-providers.ts | 7 ++---- src/providers/minimax-provider.ts | 7 ++---- src/providers/openai-codex-provider.ts | 7 ++---- src/providers/zai-provider.ts | 7 ++---- .../scripts/restore-memory.ts | 8 +++---- src/telemetry/index.ts | 5 ++-- 10 files changed, 45 insertions(+), 49 deletions(-) create mode 100644 src/agent/http-headers.ts diff --git a/src/agent/http-headers.ts b/src/agent/http-headers.ts new file mode 100644 index 0000000..04bf829 --- /dev/null +++ b/src/agent/http-headers.ts @@ -0,0 +1,24 @@ +import packageJson from "../../package.json"; + +/** + * Get standard headers for manual HTTP calls to Letta API. + * Use this for any direct fetch() calls (not SDK calls). + */ +export function getLettaCodeHeaders(apiKey?: string): Record { + return { + "Content-Type": "application/json", + "User-Agent": `letta-code/${packageJson.version}`, + "X-Letta-Source": "letta-code", + ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}), + }; +} + +/** + * Get headers for MCP OAuth connections (includes Accept header for SSE). + */ +export function getMcpOAuthHeaders(apiKey: string): Record { + return { + ...getLettaCodeHeaders(apiKey), + Accept: "text/event-stream", + }; +} diff --git a/src/cli/App.tsx b/src/cli/App.tsx index 2a79f57..c904a04 100644 --- a/src/cli/App.tsx +++ b/src/cli/App.tsx @@ -41,6 +41,7 @@ import { getResumeData } from "../agent/check-approval"; import { getClient } from "../agent/client"; import { getCurrentAgentId, setCurrentAgentId } from "../agent/context"; import { type AgentProvenance, createAgent } from "../agent/create"; +import { getLettaCodeHeaders } from "../agent/http-headers"; import { ISOLATED_BLOCK_LABELS } from "../agent/memory"; import { detachMemoryFilesystemBlock, @@ -1099,11 +1100,7 @@ export default function App({ const apiKey = process.env.LETTA_API_KEY || settings.env?.LETTA_API_KEY; const response = await fetch(`${baseURL}/v1/metadata/balance`, { - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${apiKey}`, - "X-Letta-Source": "letta-code", - }, + headers: getLettaCodeHeaders(apiKey), }); if (response.ok) { @@ -5113,11 +5110,7 @@ export default function App({ const balanceResponse = await fetch( `${baseURL}/v1/metadata/balance`, { - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${apiKey}`, - "X-Letta-Source": "letta-code", - }, + headers: getLettaCodeHeaders(apiKey), }, ); @@ -8754,9 +8747,7 @@ ${SYSTEM_REMINDER_CLOSE} { method: "POST", headers: { - "Content-Type": "application/json", - ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}), - "X-Letta-Source": "letta-code", + ...getLettaCodeHeaders(apiKey), "X-Letta-Code-Device-ID": settingsManager.getOrCreateDeviceId(), }, body: JSON.stringify({ diff --git a/src/cli/helpers/mcpOauth.ts b/src/cli/helpers/mcpOauth.ts index 2a43e29..49a829d 100644 --- a/src/cli/helpers/mcpOauth.ts +++ b/src/cli/helpers/mcpOauth.ts @@ -4,6 +4,7 @@ */ import { getServerUrl } from "../../agent/client"; +import { getMcpOAuthHeaders } from "../../agent/http-headers"; import { settingsManager } from "../../settings-manager"; // Match backend's OauthStreamEvent enum @@ -76,12 +77,7 @@ export async function connectMcpServer( const response = await fetch(`${baseUrl}/v1/tools/mcp/servers/connect`, { method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "text/event-stream", - Authorization: `Bearer ${apiKey}`, - "X-Letta-Source": "letta-code", - }, + headers: getMcpOAuthHeaders(apiKey), body: JSON.stringify(config), signal: abortSignal, }); diff --git a/src/index.ts b/src/index.ts index 7fad22b..9093ce8 100755 --- a/src/index.ts +++ b/src/index.ts @@ -11,7 +11,7 @@ import { setConversationId as setContextConversationId, } from "./agent/context"; import type { AgentProvenance } from "./agent/create"; - +import { getLettaCodeHeaders } from "./agent/http-headers"; import { ensureSkillsBlocks, ISOLATED_BLOCK_LABELS } from "./agent/memory"; import { LETTA_CLOUD_API_URL } from "./auth/oauth"; import { ConversationSelector } from "./cli/components/ConversationSelector"; @@ -1478,7 +1478,7 @@ async function main(): Promise { const apiKey = process.env.LETTA_API_KEY || settings.env?.LETTA_API_KEY; const response = await fetch(`${baseURL}/v1/metadata/balance`, { - headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {}, + headers: getLettaCodeHeaders(apiKey), }); if (response.ok) { const data = (await response.json()) as { diff --git a/src/providers/byok-providers.ts b/src/providers/byok-providers.ts index fc0c44e..dbbcc9a 100644 --- a/src/providers/byok-providers.ts +++ b/src/providers/byok-providers.ts @@ -3,6 +3,7 @@ * Unified module for managing custom LLM provider connections */ +import { getLettaCodeHeaders } from "../agent/http-headers"; import { LETTA_CLOUD_API_URL } from "../auth/oauth"; import { settingsManager } from "../settings-manager"; @@ -113,11 +114,7 @@ async function providersRequest( const response = await fetch(url, { method, - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${apiKey}`, - "X-Letta-Source": "letta-code", - }, + headers: getLettaCodeHeaders(apiKey), ...(body && { body: JSON.stringify(body) }), }); diff --git a/src/providers/minimax-provider.ts b/src/providers/minimax-provider.ts index 18deaa8..f6ff39e 100644 --- a/src/providers/minimax-provider.ts +++ b/src/providers/minimax-provider.ts @@ -2,6 +2,7 @@ * Direct API calls to Letta for managing MiniMax provider */ +import { getLettaCodeHeaders } from "../agent/http-headers"; import { LETTA_CLOUD_API_URL } from "../auth/oauth"; import { settingsManager } from "../settings-manager"; @@ -42,11 +43,7 @@ async function providersRequest( const response = await fetch(url, { method, - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${apiKey}`, - "X-Letta-Source": "letta-code", - }, + headers: getLettaCodeHeaders(apiKey), ...(body && { body: JSON.stringify(body) }), }); diff --git a/src/providers/openai-codex-provider.ts b/src/providers/openai-codex-provider.ts index 42fadb3..13a2f2b 100644 --- a/src/providers/openai-codex-provider.ts +++ b/src/providers/openai-codex-provider.ts @@ -4,6 +4,7 @@ * (transforms OpenAI API format → ChatGPT backend API format) */ +import { getLettaCodeHeaders } from "../agent/http-headers"; import { LETTA_CLOUD_API_URL } from "../auth/oauth"; import { settingsManager } from "../settings-manager"; @@ -72,11 +73,7 @@ async function providersRequest( const response = await fetch(url, { method, - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${apiKey}`, - "X-Letta-Source": "letta-code", - }, + headers: getLettaCodeHeaders(apiKey), ...(body && { body: JSON.stringify(body) }), }); diff --git a/src/providers/zai-provider.ts b/src/providers/zai-provider.ts index 23cb4d3..43c5ef8 100644 --- a/src/providers/zai-provider.ts +++ b/src/providers/zai-provider.ts @@ -2,6 +2,7 @@ * Direct API calls to Letta for managing Zai provider */ +import { getLettaCodeHeaders } from "../agent/http-headers"; import { LETTA_CLOUD_API_URL } from "../auth/oauth"; import { settingsManager } from "../settings-manager"; @@ -42,11 +43,7 @@ async function providersRequest( const response = await fetch(url, { method, - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${apiKey}`, - "X-Letta-Source": "letta-code", - }, + headers: getLettaCodeHeaders(apiKey), ...(body && { body: JSON.stringify(body) }), }); diff --git a/src/skills/builtin/defragmenting-memory/scripts/restore-memory.ts b/src/skills/builtin/defragmenting-memory/scripts/restore-memory.ts index b515baa..7cdcdf3 100644 --- a/src/skills/builtin/defragmenting-memory/scripts/restore-memory.ts +++ b/src/skills/builtin/defragmenting-memory/scripts/restore-memory.ts @@ -21,6 +21,7 @@ import { createRequire } from "node:module"; import { homedir } from "node:os"; import { extname, join, relative } from "node:path"; +import { getLettaCodeHeaders } from "../../../../agent/http-headers"; import type { BackupManifest } from "./backup-memory"; // Use createRequire for @letta-ai/letta-client so NODE_PATH is respected @@ -119,7 +120,7 @@ async function restoreMemory( const blocksResp = await fetch( `${baseUrl}/v1/agents/${agentId}/core-memory`, { - headers: { Authorization: `Bearer ${getApiKey()}` }, + headers: getLettaCodeHeaders(getApiKey()), }, ); if (!blocksResp.ok) { @@ -180,10 +181,7 @@ async function restoreMemory( const url = `${baseUrl}/v1/blocks/${existingBlock.id}`; const resp = await fetch(url, { method: "PATCH", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${getApiKey()}`, - }, + headers: getLettaCodeHeaders(getApiKey()), body: JSON.stringify({ value: newValue }), }); if (!resp.ok) { diff --git a/src/telemetry/index.ts b/src/telemetry/index.ts index 008961e..3f0075a 100644 --- a/src/telemetry/index.ts +++ b/src/telemetry/index.ts @@ -1,3 +1,4 @@ +import { getLettaCodeHeaders } from "../agent/http-headers"; import { settingsManager } from "../settings-manager"; export interface TelemetryEvent { @@ -399,9 +400,7 @@ class TelemetryManager { { method: "POST", headers: { - "Content-Type": "application/json", - ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}), - "X-Letta-Source": "letta-code", + ...getLettaCodeHeaders(apiKey), "X-Letta-Code-Device-ID": this.deviceId || "", }, body: JSON.stringify({