Skip to content

Commit 12bc99f

Browse files
authored
fix(agent): forward posthog property headers on the codex gateway path (#3297)
1 parent 9517ec0 commit 12bc99f

7 files changed

Lines changed: 131 additions & 7 deletions

File tree

packages/agent/src/adapters/acp-connection.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ function createCodexConnection(config: AcpConnectionConfig): AcpConnection {
225225
apiKey: codexOptions.apiKey,
226226
codexHome: codexOptions.codexHome,
227227
developerInstructions: codexOptions.developerInstructions,
228+
httpHeaders: codexOptions.httpHeaders,
228229
configOverrides: codexOptions.configOverrides,
229230
},
230231
model: codexOptions.model,

packages/agent/src/adapters/claude/session/options.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,13 @@ export type GatewayEnv = {
5050
openaiApiKey: string;
5151
/** Task-specific custom headers forwarded to the gateway (e.g. task_id, run_id). */
5252
anthropicCustomHeaders?: string;
53+
/**
54+
* Same task-metadata attribution headers as {@link anthropicCustomHeaders},
55+
* in record form for the codex/OpenAI path (which sets provider
56+
* `http_headers` rather than `ANTHROPIC_CUSTOM_HEADERS`). Includes `team_id`,
57+
* which the Claude path instead appends in {@link buildEnvironment}.
58+
*/
59+
openaiCustomHeaders?: Record<string, string>;
5360
/** PostHog project ID for per-team attribution headers. */
5461
posthogProjectId?: string;
5562
};

packages/agent/src/adapters/codex-app-server/spawn.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,43 @@ describe("buildAppServerArgs", () => {
3939
);
4040
});
4141

42+
it("forwards http headers as a quoted TOML inline table on the posthog provider", () => {
43+
const args = buildAppServerArgs({
44+
binaryPath: "/bundle/codex",
45+
apiBaseUrl: "https://gateway.example/v1",
46+
httpHeaders: {
47+
"x-posthog-property-ai_stage": "research",
48+
"x-posthog-property-team_id": "42",
49+
},
50+
});
51+
52+
expect(args).toContain(
53+
'model_providers.posthog.http_headers={ "x-posthog-property-ai_stage" = "research", "x-posthog-property-team_id" = "42" }',
54+
);
55+
});
56+
57+
it("omits http_headers when none are provided or the provider is unset", () => {
58+
const withoutHeaders = buildAppServerArgs({
59+
binaryPath: "/bundle/codex",
60+
apiBaseUrl: "https://gateway.example/v1",
61+
});
62+
const withoutProvider = buildAppServerArgs({
63+
binaryPath: "/bundle/codex",
64+
httpHeaders: { "x-posthog-property-ai_stage": "research" },
65+
});
66+
67+
expect(
68+
withoutHeaders.some((arg) =>
69+
arg.startsWith("model_providers.posthog.http_headers="),
70+
),
71+
).toBe(false);
72+
expect(
73+
withoutProvider.some((arg) =>
74+
arg.startsWith("model_providers.posthog.http_headers="),
75+
),
76+
).toBe(false);
77+
});
78+
4279
it.each([
4380
["darwin", 'sandbox_mode="workspace-write"'],
4481
["linux", 'sandbox_mode="danger-full-access"'],

packages/agent/src/adapters/codex-app-server/spawn.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ export interface CodexOptions {
1717
apiKey?: string;
1818
model?: string;
1919
reasoningEffort?: string;
20+
/**
21+
* Static HTTP headers forwarded on every request to the PostHog gateway
22+
* (the codex equivalent of Claude's `ANTHROPIC_CUSTOM_HEADERS`). Carries the
23+
* `x-posthog-property-*` attribution headers the gateway lifts onto the
24+
* `$ai_generation` event (team_id, ai_stage, task metadata).
25+
*/
26+
httpHeaders?: Record<string, string>;
2027
/** Guidance appended on top of Codex's base prompt via `developer_instructions`. */
2128
developerInstructions?: string;
2229
/**
@@ -50,6 +57,12 @@ export interface CodexAppServerProcessOptions {
5057
codexHome?: string;
5158
/** Guidance appended to Codex's base prompt via `developer_instructions`. */
5259
developerInstructions?: string;
60+
/**
61+
* Static HTTP headers forwarded on every request to the PostHog gateway, set
62+
* as `model_providers.posthog.http_headers`. Codex equivalent of Claude's
63+
* `ANTHROPIC_CUSTOM_HEADERS` (see {@link CodexOptions.httpHeaders}).
64+
*/
65+
httpHeaders?: Record<string, string>;
5366
/** Extra codex `-c key=value` config overrides (e.g. auto_compact_token_limit). */
5467
configOverrides?: Record<string, string | number>;
5568
logger?: Logger;
@@ -63,6 +76,19 @@ export interface CodexAppServerProcess {
6376
kill: () => void;
6477
}
6578

79+
/** Serialize a string map as a TOML basic string (escapes `\` and `"`). */
80+
function tomlBasicString(value: string): string {
81+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
82+
}
83+
84+
/** Render a `Record<string, string>` as a TOML inline table. */
85+
function tomlInlineTable(entries: Record<string, string>): string {
86+
const pairs = Object.entries(entries).map(
87+
([key, value]) => `${tomlBasicString(key)} = ${tomlBasicString(value)}`,
88+
);
89+
return `{ ${pairs.join(", ")} }`;
90+
}
91+
6692
export function buildAppServerArgs(
6793
options: CodexAppServerProcessOptions,
6894
): string[] {
@@ -116,6 +142,17 @@ export function buildAppServerArgs(
116142
"-c",
117143
`model_providers.posthog.env_key="POSTHOG_GATEWAY_API_KEY"`,
118144
);
145+
146+
// Attribution + task-metadata headers the gateway lifts onto the captured
147+
// $ai_generation event. Passed as a single TOML inline table so hyphenated
148+
// header names (`x-posthog-property-*`) stay quoted rather than becoming
149+
// bare-key segments of a dotted `-c` path.
150+
if (options.httpHeaders && Object.keys(options.httpHeaders).length > 0) {
151+
args.push(
152+
"-c",
153+
`model_providers.posthog.http_headers=${tomlInlineTable(options.httpHeaders)}`,
154+
);
155+
}
119156
}
120157

121158
// developer_instructions are set per-thread in thread/start (with the host's

packages/agent/src/server/agent-server.configure-environment.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,35 @@ describe("AgentServer.configureEnvironment", () => {
161161
},
162162
);
163163

164+
// The codex/OpenAI path sets provider http_headers rather than
165+
// ANTHROPIC_CUSTOM_HEADERS, so the same task metadata must be exposed as a
166+
// record — including team_id, which the Claude path adds separately in
167+
// buildEnvironment.
168+
it("forwards task metadata (plus team_id) as openaiCustomHeaders", () => {
169+
const env = buildServer("background").configureEnvironment({
170+
isInternal: true,
171+
originProduct: "signal_report",
172+
signalReportId: "report-123",
173+
aiStage: "research",
174+
taskId: "task-abc",
175+
taskRunId: "run-xyz",
176+
taskUserId: 42,
177+
taskTitle: "Fix the bug",
178+
});
179+
180+
expect(env.openaiCustomHeaders).toEqual({
181+
"x-posthog-property-task_origin_product": "signal_report",
182+
"x-posthog-property-task_internal": "true",
183+
"x-posthog-property-signal_report_id": "report-123",
184+
"x-posthog-property-ai_stage": "research",
185+
"x-posthog-property-task_id": "task-abc",
186+
"x-posthog-property-task_run_id": "run-xyz",
187+
"x-posthog-property-task_user_id": "42",
188+
"x-posthog-property-task_title": "Fix the bug",
189+
"x-posthog-property-team_id": "1",
190+
});
191+
});
192+
164193
it("forwards task metadata as anthropicCustomHeaders", () => {
165194
const env = buildServer("background").configureEnvironment({
166195
isInternal: true,

packages/agent/src/server/agent-server.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ import type {
7474
import { resourceLink } from "../utils/acp-content";
7575
import { AsyncMutex } from "../utils/async-mutex";
7676
import {
77+
buildGatewayPropertyHeaderRecord,
7778
buildGatewayPropertyHeaders,
7879
resolveGatewayProduct,
7980
resolveLlmGatewayUrl,
@@ -1254,6 +1255,7 @@ export class AgentServer {
12541255
model: this.config.model ?? DEFAULT_CODEX_MODEL,
12551256
reasoningEffort: this.config.reasoningEffort,
12561257
developerInstructions: codexInstructions,
1258+
httpHeaders: gatewayEnv.openaiCustomHeaders,
12571259
}
12581260
: undefined,
12591261
onStructuredOutput: async (output) => {
@@ -3060,12 +3062,11 @@ ${signedCommitInstructions}${prLinkInstructions}${shellEfficiencyInstructions}
30603062
? gatewayUrl
30613063
: `${gatewayUrl}/v1`;
30623064
// Forward task metadata as `x-posthog-property-*` headers so the gateway
3063-
// lifts them onto the $ai_generation event. Routes through the Anthropic
3064-
// SDK's ANTHROPIC_CUSTOM_HEADERS env var; the OpenAI/codex path has no
3065-
// equivalent today. (The `team_id` attribution header is added downstream
3066-
// in the Claude session builder from POSTHOG_PROJECT_ID — see
3067-
// adapters/claude/session/options.ts.)
3068-
const customHeaders = buildGatewayPropertyHeaders({
3065+
// lifts them onto the $ai_generation event. The Claude path routes these
3066+
// through the Anthropic SDK's ANTHROPIC_CUSTOM_HEADERS env var; the codex
3067+
// path sets them as `model_providers.posthog.http_headers` instead, so we
3068+
// also expose the record form below.
3069+
const gatewayProperties = {
30693070
task_origin_product: originProduct,
30703071
task_internal: isInternal,
30713072
signal_report_id: signalReportId,
@@ -3074,6 +3075,14 @@ ${signedCommitInstructions}${prLinkInstructions}${shellEfficiencyInstructions}
30743075
task_run_id: taskRunId,
30753076
task_user_id: taskUserId,
30763077
task_title: taskTitle,
3078+
};
3079+
const customHeaders = buildGatewayPropertyHeaders(gatewayProperties);
3080+
// The Claude path appends `team_id` in buildEnvironment from
3081+
// POSTHOG_PROJECT_ID; the codex path has no such hook, so fold it into the
3082+
// record here to keep team attribution working for both adapters.
3083+
const openaiCustomHeaders = buildGatewayPropertyHeaderRecord({
3084+
...gatewayProperties,
3085+
team_id: projectId,
30773086
});
30783087

30793088
// Server-level constants that don't vary per task — safe to keep in
@@ -3096,6 +3105,7 @@ ${signedCommitInstructions}${prLinkInstructions}${shellEfficiencyInstructions}
30963105
openaiBaseUrl,
30973106
openaiApiKey: apiKey,
30983107
anthropicCustomHeaders: customHeaders,
3108+
openaiCustomHeaders,
30993109
posthogProjectId: String(projectId),
31003110
};
31013111
}

packages/agent/src/utils/gateway.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ export function resolveGatewayProduct({
3131
return "posthog_code";
3232
}
3333

34-
export { buildPosthogPropertyHeaderLines as buildGatewayPropertyHeaders } from "@posthog/shared/posthog-property-headers";
34+
export {
35+
buildPosthogPropertyHeaderLines as buildGatewayPropertyHeaders,
36+
buildPosthogPropertyHeaderRecord as buildGatewayPropertyHeaderRecord,
37+
} from "@posthog/shared/posthog-property-headers";
3538

3639
function getGatewayBaseUrl(posthogHost: string): string {
3740
const url = new URL(posthogHost);

0 commit comments

Comments
 (0)