Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a4868f6
chore(ai-gateway): document migration parity
richardsolomou Jul 31, 2026
a05e4f2
chore(ai-gateway): format parity guide
richardsolomou Jul 31, 2026
1a76aa8
chore(ai-gateway): simplify parity guide
richardsolomou Jul 31, 2026
999f759
chore(ai-gateway): formalize migration guidance
richardsolomou Jul 31, 2026
7501b88
chore(ai-gateway): split parity and migration skills
richardsolomou Jul 31, 2026
f04ca99
Merge branch 'master' into posthog-code/document-ai-gateway-parity
richardsolomou Jul 31, 2026
7fe1a4a
chore(ai-gateway): refresh parity audit
richardsolomou Jul 31, 2026
8bb1416
chore(ai-gateway): add migration examples
richardsolomou Jul 31, 2026
9ba15ec
chore(ai-gateway): add cross-repository migration examples
richardsolomou Jul 31, 2026
60b51a5
chore(ai-gateway): document migration follow-up sequences
richardsolomou Jul 31, 2026
dafabf6
chore(ai-gateway): record caller migration status
richardsolomou Jul 31, 2026
8b5d902
chore(ai-gateway): verify caller rollout inventory
richardsolomou Jul 31, 2026
b2e9263
chore(ai-gateway): focus guidance on migration decisions
richardsolomou Jul 31, 2026
ea7a785
chore(ai-gateway): add migration candidate discovery skill
richardsolomou Jul 31, 2026
7970fb9
chore(ai-gateway): clarify internal spend ownership
richardsolomou Jul 31, 2026
dc19c39
chore(ai-gateway): strengthen candidate verification
richardsolomou Jul 31, 2026
c3ddab9
chore(ai-gateway): treat credential creation as migration work
richardsolomou Jul 31, 2026
db11000
chore(ai-gateway): refresh migration guidance
richardsolomou Jul 31, 2026
cd00300
docs(ai-gateway): clarify staged rollout fallback
richardsolomou Jul 31, 2026
9b07cf8
chore(ai-gateway): audit in-flight parity changes
richardsolomou Jul 31, 2026
f2c2948
chore(ai-gateway): audit pending Go changes
richardsolomou Jul 31, 2026
bb04c93
Merge branch 'master' into posthog-code/document-ai-gateway-parity
richardsolomou Jul 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions .agents/skills/auditing-llm-gateway-parity/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
name: auditing-llm-gateway-parity
description: >
Audits services/llm-gateway against PostHog/ai-gateway and updates services/llm-gateway/PARITY.md from current implementation evidence. Use when either gateway changes auth, attribution, billing, endpoints, providers, models, routing, or metadata; when reviewing a Python gateway change; or when asked to refresh, verify, or report gateway parity. This skill updates the parity record but does not migrate callers.
---

# Auditing LLM gateway parity

Compare the current implementations of both gateways and update [`services/llm-gateway/PARITY.md`](../../../services/llm-gateway/PARITY.md). Keep this audit separate from caller migration.

## Record source revisions

- Fetch `origin/master` and record its SHA as the PostHog baseline.
- Audit the current working tree, including committed and uncommitted changes relative to that baseline. Do not ignore an in-flight gateway change because it is not on `master` yet.
- Read the current `PostHog/ai-gateway` main SHA with `gh api repos/PostHog/ai-gateway/commits/main` and record it as the Go baseline.
- If the audit is for an in-flight Go change, inspect that PR branch or working tree relative to `main`. Treat its contracts as pending until the change merges; do not record them as currently supported.
- Otherwise, inspect an authenticated checkout of `PostHog/ai-gateway` at `main`.

Audit implementation code. README files and the existing parity table are starting points, not proof.

## Inspect each contract

For the Python gateway, inspect:

- `services/llm-gateway/src/llm_gateway/api/` for routes and wire behavior
- `services/llm-gateway/src/llm_gateway/auth/` for accepted credentials
- `services/llm-gateway/src/llm_gateway/products/config.py` for trusted product policy, models, and billing
- `services/llm-gateway/src/llm_gateway/rate_limiting/` for budgets and limits
- `services/llm-gateway/src/llm_gateway/callbacks/` for event attribution
- `posthog/llm/gateway_client.py` and real call sites for required contracts

For the Go gateway, inspect:

- `internal/httpapi/routes.go` and dispatch packages for API shapes
- `internal/auth/` and `internal/principal/` for credential and identity policy
- `internal/httpapi/admission.go`, `internal/ledger/`, and `internal/quota/` for billing and limits
- `internal/catalog/`, `internal/router/`, and `internal/dispatch/` for models, providers, translation, and failover
- `internal/emitter/` and request parsing for attribution and metadata
- `docs/product.md` for intended and deferred contracts, verified against code

Check both request and response behavior. Matching route names do not prove parity for headers, streaming, errors, timeouts, retries, billing, or emitted events.

## Classify the evidence

- ✅ **Supported:** the Go gateway satisfies the contract.
- ⛔ **Blocking:** an active use case would lose auth, trusted attribution, billing policy, API, provider, or wire behavior.
- 🔎 **Verify:** support exists, but a caller must confirm configuration or behavior.

Do not treat caller-supplied telemetry as trusted policy. An `ai_product` event property does not replace product authentication, authorization, or billing.

Do not treat Python's unbilled flag as an automatic blocker. An internal workload can move to Go with a PostHog-owned team credential when debiting that wallet is the intended way to attribute PostHog spend. It is blocked only when it must preserve customer-specific billing policy or must debit no wallet.

For each difference, identify at least one affected use-case class. Remove details that do not change a migration decision.

## Update the parity record

Update `services/llm-gateway/PARITY.md` with the evidence:

- Move use cases between supported, blocked, and verify sections.
- Add or remove migration-relevant contracts in the parity map.
- Update the source SHAs and verification date.
- Keep detailed Go design in `PostHog/ai-gateway`.

When a gap closes, name obvious caller classes that are newly eligible to migrate. Do not modify those callers unless the user also asks for migration work. Use `/migrating-llm-gateway-callers` for that separate job.

## Validate

Run:

```sh
pnpm exec oxfmt services/llm-gateway/PARITY.md .agents/skills/auditing-llm-gateway-parity/SKILL.md
pnpm exec markdownlint-cli2 --config .config/.markdownlint-cli2.jsonc services/llm-gateway/PARITY.md .agents/skills/auditing-llm-gateway-parity/SKILL.md
git diff --check
```
94 changes: 94 additions & 0 deletions .agents/skills/finding-llm-gateway-migration-candidates/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
---
name: finding-llm-gateway-migration-candidates
description: >
Finds and ranks callers that could move from services/llm-gateway to PostHog/ai-gateway. Use when asked what to migrate next, to find low-risk gateway migration candidates, to audit remaining Python gateway callers, or to identify callers blocked by Go gateway parity. Searches code and deployment wiring, inventories each caller's required contract, filters out unsupported migrations, and returns an evidence-backed shortlist without changing callers.
---

# Finding LLM gateway migration candidates

Use [`services/llm-gateway/PARITY.md`](../../../services/llm-gateway/PARITY.md) as the current contract decision record. If implementation evidence contradicts it, run `/auditing-llm-gateway-parity` before ranking candidates.

This skill produces a point-in-time shortlist. Do not turn the parity document into a fleet-wide migration tracker.

## Discover callers

Search the current default branches for Python gateway use, including:

- `LLM_GATEWAY_URL`, `LLM_GATEWAY_API_KEY`, and product-slug URLs
- `get_llm_client`, `get_async_llm_client`, and `get_async_anthropic_gateway_client`
- direct OpenAI, Anthropic, or agent SDK clients configured with a gateway base URL
- Python product names from `services/llm-gateway/src/llm_gateway/products/config.py`
- deployment, sandbox, workflow, and secret wiring in other PostHog repositories

Trace each result to the production call site. Exclude tests, development-only tools, dead code, and callers already configured exclusively for Go. Treat shared worker configuration as evidence about availability, not proof that every call in that process uses Go.

Search open and recently merged PRs across involved repositories before proposing work. A migration may already be underway even when the default branch still uses Python.

## Prove the production path

Before classifying a caller, follow it through every runtime boundary:

1. Find the API, task, workflow, schedule, or command that invokes it.
2. For Temporal, map the workflow and activity to the exact task queue, then map that queue to its deployed worker.
3. Inspect every environment overlay that runs the caller. Do not borrow configuration from another worker in the same product.
4. Resolve the configured URL by host and path. Variable names are not proof: an `AI_GATEWAY_URL` can still contain a Python product-slug URL.
5. Confirm the current call can succeed. A broad exception handler with a deterministic fallback can hide missing credentials or a route that never runs.

A missing `phs_` credential is deployment work, not a parity blocker. Once the intended paying team is known, create a project secret with `llm_gateway:read` in the PostHog dashboard and fund or configure that team wallet as part of the migration. A secret reference proves only that a credential is injected; verify ownership, scope, and funding during implementation or rollout.

## Inventory each candidate

Record only the contracts that affect the migration decision:

- user-facing use case and production entry point
- credential source and trusted authorization policy
- spend owner, budget enforcement, current Python billing behavior, and the team wallet that should own Go spend
- API shape and model as one pair, provider, streaming, tools, and structured output
- distinct ID, trace, product, team, and custom attribution
- retry, timeout, fallback, and error behavior
- deployment and egress changes needed to activate and roll back the route

Do not infer requirements from a product name. Read the call and its configuration.

Check model and API shape together. Python can expose an Anthropic model through OpenAI Chat Completions, while Go's native routes do not generally cross-translate shapes. A migration may need both a gateway change and an SDK-shape change.

Separate billing identity from event attribution. Go debits the credential's team. A caller-supplied `team_id` or `ai_product` property can preserve reporting context but cannot select the wallet.

## Classify readiness

Match the inventory to the parity record and current Go implementation:

- ✅ **Ready:** required contracts are supported and the remaining work is client, test, or deployment wiring.
- 🔎 **Verify:** support probably exists, but model availability, wire behavior, billing, attribution, or deployment configuration needs a targeted check.
- ⛔ **Blocked:** migration would lose a required auth, policy, billing, provider, API, or attribution contract.
- 🚧 **In progress:** an open or recently merged PR already owns the migration.

Name the exact evidence for every status. An existing Python helper is migration effort, not a blocker. An `ai_product` property is telemetry, not trusted identity or billing policy.

Python's unbilled flag is not a blocker when an internal workload should debit a PostHog-owned team wallet for spend attribution. Verify that the Go credential resolves to that team. Treat billing as blocked only when migration would charge a customer incorrectly, lose required customer budget policy, or violate a requirement to debit no wallet.

Use ✅ when the production entry point, worker or process, intended paying team, model and API pair, attribution conversion, and rollback path are supported by evidence. The token itself may still need to be created and wired. Use 🔎 when the spend owner, required policy, runtime behavior, or another migration contract is unknown.

## Rank the shortlist

Prioritize candidates that:

1. Have no ⛔ contract.
2. Use a shared Go-capable builder or a stock SDK with a simple base URL change.
3. Already use a Go-supported model, provider, and API shape.
4. Need informational attribution rather than trusted product identity.
5. Can use a customer wallet intentionally or a PostHog-owned wallet for internal spend.
6. Have a narrow deployment boundary, explicit fallback, and cheap verification path.

Lower the rank for broad shared-process switches, unverified billing changes, cross-repository deployment work, or missing production tests. Do not rank by code size alone.

## Report candidates

Return a small decision-oriented table with:

| Candidate | Readiness | Why | Required work | Evidence gaps |
| --------- | --------- | --- | ------------- | ------------- |

List the strongest ready candidate first. Name the exact production process and deployment for each candidate. For blocked callers, state the missing Go contract and where it was verified. For in-progress work, link the existing PR instead of proposing duplicate work. For 🔎 candidates, state the one check that would promote or reject them.

Do not modify callers while running this skill. Once a candidate is selected, run `/migrating-llm-gateway-callers` for that caller.
81 changes: 81 additions & 0 deletions .agents/skills/migrating-llm-gateway-callers/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
---
name: migrating-llm-gateway-callers
description: >
Migrates an LLM caller from services/llm-gateway to PostHog/ai-gateway. Use when adding a gateway caller, converting an existing Python gateway integration, adopting shared Go-capable client builders, changing gateway URLs or headers for a caller, or removing a Python fallback. Inventories the caller's contract, checks the parity record, implements the supported migration, updates tests, and stops with a documented blocker when Go parity is missing.
---

# Migrating LLM gateway callers

Use [`services/llm-gateway/PARITY.md`](../../../services/llm-gateway/PARITY.md) as the current decision record. If it is stale or the migration reveals a new gap, run `/auditing-llm-gateway-parity` before continuing.

## Inventory the caller

Find the production call site, client construction, settings, deployment wiring, and tests. Record:

- caller and user-facing use case
- credential source and required authorization policy
- spend owner, budgets, current Python billing behavior, and the team wallet that should own Go spend
- API shape, model, provider, streaming, and tool or structured-output requirements
- distinct ID, trace, product, team, feature flag, and custom property attribution
- timeout, retry, fallback, and error-handling behavior
- usage or quota APIs read by the caller

Search for the product name, `LLM_GATEWAY`, `AI_GATEWAY`, gateway client helpers, and gateway headers. Follow configuration into sandbox or deployment code when the call does not run in Django.

## Check whether migration is supported

Match every required contract to the parity record and current code.

- If all required contracts are supported, continue with the migration.
- If a 🔎 item applies, verify it against the caller's actual model, request, and configuration.
- If a ⛔ gap applies, stop the migration. Report the exact blocker and keep the Python path.

An existing Python client or product route is not a blocker by itself.

Python's unbilled flag is not a blocker when an internal workload should debit a PostHog-owned team wallet for spend attribution. Confirm that the Go credential resolves to that team. Stop only when migration would charge a customer incorrectly, lose required customer budget policy, or violate a requirement to debit no wallet.

## Implement the migration

Prefer the smallest existing pattern that matches the caller:

Read [migration examples](references/migration-examples.md) for verified PRs covering Django clients, staged workload rollout, sandbox wiring, and attribution continuity. Follow the contract demonstrated by the relevant example rather than copying its code mechanically.

1. Use `build_openai_client`, `build_async_openai_client`, or `build_async_anthropic_client` from `posthog/llm/gateway_client.py` for Django callers when possible.
2. Use the slugless Go base URL. Do not carry a Python `/{product}/` path into the Go URL.
3. Use a supported `phs_` or `pha_` credential with `llm_gateway:read`. Do not weaken auth or expose a shared secret to an untrusted runtime.
4. Send event labels in one `X-PostHog-Properties` JSON object. Use the dedicated distinct ID and trace ID headers where required.
5. Treat `ai_product` as telemetry only. Do not use it to replace trusted product auth or billing policy.
6. Confirm the canonical model and API shape against the Go model catalog.
7. Preserve the caller's provider and fallback requirements. Use provider pinning only when the caller requires a specific host.
8. Keep a Python fallback only when rollout needs it, and make the switch explicit in settings or the shared builder.

For sandbox callers, follow the existing `SANDBOX_AI_GATEWAY_URL` and product rollout patterns rather than inventing another environment contract.

If the target process does not have a Go credential, create a `phs_` project secret for the intended paying team in the PostHog dashboard, grant `llm_gateway:read`, and wire it through the existing deployment secret mechanism. Credential creation is normal migration work, not a parity exception.

## Update tests and docs

Invoke `/writing-tests` before changing tests.

Cover the observable migration contract at the lowest useful level:

- selected base URL and absence of the Python product slug
- credential and required headers without asserting secret values
- converted JSON properties and trace attribution
- model and API shape
- rollout fallback when one remains
- failure behavior the caller handles

Update nearby docs and settings descriptions when the operator workflow changes. Do not update the parity record unless implementation evidence changed; use `/auditing-llm-gateway-parity` when it did.

## Verify

Run the narrow caller tests and the formatter or type checker for touched code. Exercise a real request when credentials and a safe development gateway are available. Confirm the event attribution and billing behavior when those contracts matter.

Before handing off, summarize:

- migrated caller and use case
- Go contracts relied on
- fallback left in place, if any
- parity blocker, if migration stopped
- checks run and any environment limitation
Loading
Loading