Skip to content

Feature: ai-gateway-v2 - #24

Merged
philcunliffe merged 6 commits into
masterfrom
integration/ai-gateway-v2
May 23, 2026
Merged

Feature: ai-gateway-v2#24
philcunliffe merged 6 commits into
masterfrom
integration/ai-gateway-v2

Conversation

@philcunliffe

Copy link
Copy Markdown
Contributor

Auto-generated by /feature-launch to host the integration branch for feature ai-gateway-v2.

Work beads file individual sub-PRs into this branch via the refinery feature-flow loop. When all work + review + ship beads complete, the ship formula flips this PR to ready-for-review.

See /feature-flow for the flow architecture.

…te provider parsers (#25)
Refactor @hypaware/ai-gateway into a generic HTTP/SSE capture and
row-storage owner. All client/protocol semantics move into adapter
plugins through the new full exchange projector API.
Capability bumped to breaking 2.0.0. New surface:
- registerUpstreamPreset(preset) — adapter-owned routing via
optional preset.match() (replaces hardcoded detectProvider()).
- registerExchangeProjector(projector) — adapter-owned
exchange → conversation messages projection.
- localEndpoint, getClient, listClients, registerClient unchanged.
Removed from the capability surface: registerExchangeContextProjector
and registerMessageEnricher.
Projector dispatch in message_projector.js:
- Matching projectors sorted by descending priority, then
registration order.
- First successful non-empty projection wins.
- Throws / undefined / invalid output: warn and skip.
- No projector matches: zero rows, but pass-through telemetry
(aigw.exchange log, aigw.exchange_bytes meter) still fires.
Fallback identity (post-projection): when an adapter returns
normalized messages without message_id, compute hash id,
linearize previous_message_id (only if absent), and stamp
attributes.gateway.identity_source = "gateway_fallback".
previous_message_id supplied as [] is preserved (root marker).
Projector-supplied identity is authoritative — never overridden.
Deleted from the gateway package:
- Anthropic HTTP+SSE parsing (message_projector.js).
- OpenAI Chat and Responses + SSE reconstruction.
- Codex header / path / turn-metadata parsing.
- Generic context-fallback path, provider inference,
Claude-specific attributes.client.claude_version.
- /_hypaware/session-context endpoint (proxy.js).
- ClaudeSessionContext, localContextForRequest, request-body
session lookup, recorder cwd/git_branch injection.
Schema unchanged: ai_gateway_messages, SCHEMA_VERSION = 4,
proxy_messages_v4. No migration, no backfill.
Tests:
- ai-gateway-session-context.test.js deleted (endpoint removed).
- ai-gateway-message-projector.test.js rewritten to cover the
new dispatch surface (priority/seq ordering, error/empty
skipping, fallback identity stamping, schema strip) plus the
schema column contract. Provider-specific assertions moved
out of the gateway and will return in phases 2/3 plugin tests.
- ai_gateway_passthrough smoke updated to assert: capability
registered at 2.0.0, no cache.append, zero rows for the
dev_run_id, and pass-through telemetry (aigw.exchange log
with rows_written=0, aigw.exchange_bytes meter) still fires.
This phase intentionally breaks Claude + Codex capture (their
manifests still require ^1.0.0) until phases 2 and 3 land
adapter exchange projectors on the same integration branch.
philcunliffeand others added 3 commits May 22, 2026 16:53
Port OpenAI Chat, OpenAI Responses (JSON + SSE), and ChatGPT Codex
(SSE + turn metadata) projection into a single
`AiGatewayExchangeProjector` owned by `@hypaware/codex`. Bumps the
plugin's capability requirement to `hypaware.ai-gateway@^2.0.0` and
adds the new projector to the activate() wiring.
Projector behavior:
- Matches `/v1/chat/completions`, `/v1/responses`, `/v1/models`,
`/backend-api/codex/*`, or any request tagged with the
`x-codex-turn-metadata` header.
- Parses Chat-shaped requests (`messages: []`) and Responses-shaped
requests (`input: ...`) into normalized user-role blocks. Tool
calls and tool results map onto `tool_use` / `tool_result` blocks.
- Reconstructs streamed assistant messages from
`response.output_text.delta` events; falls back to body-level
`output_text` or `output` arrays.
- Projects Codex headers + turn metadata into both first-class
columns (`cwd`, `client_version`, `entrypoint`, `user_type`,
`permission_mode`, `is_sidechain`, `request_id`, `prompt_id`) and
the `attributes.codex.*` namespace (thread/session/turn ids,
workspace, git origin + commit, sandbox, originator, window id).
- Always stamps `attributes.codex.identity_source = "gateway_fallback"`
because the projector never supplies `message_id` today; the
gateway computes hashes and linearizes history for symmetry with
the @hypaware/claude adapter.
- Reserves a Codex SQLite / log-reader hook behind the
`HYPAWARE_CODEX_SQLITE_READS=1` env flag; today no readers are
shipped (no-op stub), keeping the real reader out of this bead.
Smoke + tests:
- `gateway_codex_capture` flow activates `@hypaware/codex` and renames
its local fake upstreams to `local-openai` / `local-chatgpt` so they
don't collide with the plugin's `openai` / `chatgpt` preset names.
The local config entries appear first in the merged routing table
and outrank the plugin presets at routing time.
- New `test/plugins/codex-exchange-projector.test.js` covers match
surface, OpenAI Chat tool-call mapping, Responses SSE reconstruction,
Codex header/metadata projection, subagent flipping is_sidechain,
identity_source stamping symmetry, log-reader gating, and
conversation-id fallback determinism.
Gateway core remains free of provider parsing (only
`chatgpt-account-id` appears as a default-redacted header, which is
a security setting rather than parsing logic).
\`npm test\` passes (280 tests). \`npm run typecheck\` clean.
\`npm run smoke -- gateway_codex_capture\` and
\`npm run smoke -- ai_gateway_passthrough\` both green.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…xt (#30)
Phase 2 of the ai-gateway-v2 feature flow. Ports the Anthropic
HTTP+SSE parsing that phase 1 deleted from the gateway into the
`@hypaware/claude` adapter as a single `AiGatewayExchangeProjector`,
and moves the session-context channel from the (now-removed) HTTP
endpoint to a JSONL file under the plugin's state directory.
Plugin (`@hypaware/claude`, bumped to 2.0.0):
- New `projector.js` registers `AiGatewayExchangeProjector` and
`AiGatewayUpstreamPreset` against `hypaware.ai-gateway@^2.0.0`.
Match surface: `/v1/messages*` path OR `anthropic-version` /
`x-api-key` / `Authorization: Bearer sk-ant-*` header signature.
- `anthropic.js` carries the ported Anthropic Messages HTTP body
parse, SSE assistant reconstruction (`message_start`,
`content_block_*`, `message_delta`, `message_stop`), conversation
id / user id / system text / tools / client-version extraction.
- `transcripts.js` reads `<HOME>/.claude/projects/<repo>/<session>.jsonl`
(or the exact `transcript_path` when the session-context record
supplies one) for native DAG identity. Matches by uuid → projected
message: `message_id = provider_uuid = uuid`,
`previous_message_id = parentUuid ? [parentUuid] : []`.
- `session_context.js` reads/writes
`<stateDir>/session-context.jsonl`. `pickLatestMatching` prefers
`transcript_path` then `session_id`; newest matching line wins.
- On a missing transcript the projector returns messages without
`message_id`, the gateway computes hash identity + stamps
`attributes.gateway.identity_source="gateway_fallback"`, and the
projector adds `attributes.claude.identity_source="gateway_fallback"`
for Claude-specific debuggability.
- `settings.js` swaps the managed hook command from `--port <port>`
to `--state-file <abspath>`; the `_hypaware` marker carries both
port (still needed for `ANTHROPIC_BASE_URL`) and state_file.
- Deleted `enricher.js` (gateway 2.0 removed `registerMessageEnricher`;
the projector inlines what enrichment did).
CLI:
- `hyp claude-hook session-context --state-file <path>` appends one
JSONL record per hook event (session_id, cwd, optional
transcript_path / git_branch / ts). No daemon required —
the file is read at projection time.
Caller-side cleanup of stale `^1.0.0` ranges (phase 1 missed these):
- `src/core/cli/core_commands.js` and `src/core/cli/walkthrough.js`
require `^2.0.0`.
- `src/core/config/validate.js` first-party metadata updated for
ai-gateway (provides 2.0.0), claude and codex (both require
^2.0.0; codex projector landed in #28 alongside this).
- `walkthrough_*_to_first_query` smokes require `^2.0.0`; their
remaining contract assertions (zero ai_gateway_messages rows
because they POST to /v1/echo, not /v1/messages) are phase-4 work
per the shared-harness scope.
Tests:
- `test/plugins/claude-projector-identity.test.js` covers native uuid
identity, root `[]` previous_message_id, missing-log fallback
marker, cwd/git_branch propagation from the state file, and the
three `match()` paths (header signature, /v1/messages path, none).
- `test/plugins/claude-session-context-hook.test.js` is the
hook→state-file→projector roundtrip — replaces the deleted
`ai-gateway-session-context.test.js`.
- `test/core/command-dispatch.test.js` updated for the
`--state-file` flag and the fake gateway capability bumped to
2.0.0 surface (with `registerExchangeProjector`).
- `hypaware-core/smoke/flows/gateway_claude_capture.js` rewritten to
activate both plugins, stage a JSONL transcript fixture, drive the
hook, and assert both native-DAG and fallback-identity rows.
- `claude_attach_detach` smoke updated for the new marker shape and
the `--state-file` hook command line.
277 tests / typecheck / lint clean. Smokes green:
- gateway_claude_capture
- claude_attach_detach
- ai_gateway_passthrough
- daemon_foreground_start_stop
- core_boot_noop
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds dedicated gateway-package unit tests for the post-2.0 capability
API and proxy routing now that provider-specific projection logic lives
in the Claude and Codex plugins:
- test/plugins/ai-gateway-api.test.js covers registerExchangeProjector
validation (name/match/project required, missing-priority preserved),
registration-order _seq assignment, plus registerUpstreamPreset,
registerClient/getClient/listClients, and localEndpoint shape.
- test/plugins/ai-gateway-proxy-routing.test.js covers compileUpstreams
ordering (priority desc, then prefix length, then registration order),
base_url validation, matchUpstream first-match-wins + short-circuit
+ throw-as-non-match + path-prefix fallback + lowercased array-valued
header view.
- test/plugins/ai-gateway-message-projector.test.js gains three tests
covering invalid-shape-skipped, all-projectors-fail returns zero rows
with aigw.projector_error/_invalid_output/message_projection_skipped
warnings, and skipping a non-matching projector without calling its
project().
proxy.js exports matchUpstream and compileUpstreams so the routing
tests can exercise them in isolation; the runtime call sites are
unchanged.
The gateway package no longer asserts Anthropic/OpenAI/Codex protocol
details - phase 1 already stripped them and phases 2/3 added the
plugin-side coverage in claude-projector-identity, claude-session-
context-hook, and codex-exchange-projector. Pass-through telemetry on
the all-projectors-fail path is gated end-to-end by the existing
ai_gateway_passthrough smoke (no projector -> zero rows + aigw.exchange
log emitted).
Final check: npm test (319 pass), ai_gateway_passthrough,
gateway_codex_capture, and gateway_claude_capture smokes all green.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@philcunliffe

Copy link
Copy Markdown
ContributorAuthor

Dual-agent review — request_changes

  • Verdict:request_changes
  • Risk class:high
  • Blast-radius bead:hy-99x61

Risk capstone

Cross-reference: reviewer findings that intersect high-risk surfaces

SourceFinding (severity, evidence)Intersects
codex.mdContract & Interface Fidelity — part_type wrongly derived from provider_type (user/assistant) for transcript-matched Claude messages (major, high) — ai-gateway/src/message_projector.js:434, claude/src/projector.js:174, claude/src/transcripts.js:192Targets (message_projector.js −1273 net dispatcher rewrite; claude/projector.js new file with createClaudeExchangeProjector); Concurrency surface (Claude transcript walk in transcripts.js)
codex.mdBehavioral Correctness — listener startup compiles/validates config before merging presets, so preset-only gateways can fail at boot (major, medium) — ai-gateway/src/source.js:95, ai-gateway/src/source.js:151, ai-gateway/src/config.js:6Targets (ai-gateway/source.js, ai-gateway/config.js); Config field chain (config.js declares priority, match, path_prefix for new upstream contract)
codex.mdChange Impact / Blast Radius — presets silently overwrite same-name TOML upstreams; local/enterprise endpoints get replaced by adapter defaults (major, high) — ai-gateway/src/source.js:180, ai-gateway/src/source.js:195, hypaware-core/smoke/flows/gateway_claude_capture.js:105Targets (ai-gateway/source.js; gateway_claude_capture.js smoke flow); Direct callers (gateway_claude_capture.js exercises the rewritten attach + projector wiring path)
claude.mdselectCodexWorkspace no longer accepts recorded cwd as tiebreaker; tagged rows attribute to V8-first workspace key, not the user's actual workspace (unmarked severity → treated as major per rubric) — hypaware-core/plugins-workspace/codex/src/exchange-projector.js:503-514, called at :410Targets (codex/src/exchange-projector.js, +798, new file housing createCodexExchangeProjector)
claude.mdreadSessionContext invoked per-exchange with no cache/tail-N/rotation; daemon I/O grows O(records-ever-seen) (unmarked severity → treated as major per rubric) — hypaware-core/plugins-workspace/claude/src/projector.js:110-120, hypaware-core/plugins-workspace/claude/src/session_context.js:62-86Targets (claude/projector.js, claude/session_context.js — both new files); Concurrency surface (session-context.jsonl: concurrent writes, unbounded growth, reader scans end-to-end); Risks #5 (blast-radius explicitly flags unbounded growth and recommends a soak smoke)

Blast radius

  • Capability bump is a hard break for any out-of-tree adapter still pinning ^1.0.0. Internal plugins are bumped, but the dep-graph will reject third-party plugins on first boot post-upgrade. There is no shim or deprecation period — a third-party adapter is just incompatible.

  • Zero-projector default is silent.ai_gateway_passthrough.js asserts the gateway emits zero rows when no projector is registered. Operators running the gateway plugin alone (no @hypaware/claude or @hypaware/codex) will see empty ai_gateway_messages and no warning. The old built-in projector behavior is gone.

  • Hook-command format change is breaking for hand-installed v1 hooks. Users who copied the old hyp claude-hook session-context --port <int> invocation into their ~/.claude/settings.json outside of attach will keep invoking that form. The new binary still parses --port but never writes to a state file, so session context silently stops being captured. Re-running attach will rewrite to the new form, but there is no first-boot upgrade hint that this is needed.

  • Recorder no longer fills cwd / git_branch directly. Those columns now depend on the session-context hook having fired before the captured exchange. The first exchange of a brand-new Claude session — before SessionStart is appended — will be projected without these fields. Historical analytics over cwd/git_branch will see NULL gaps that did not exist in 1.x. No documentation of this gap surfaces in .feature-flow/ai-gateway-v2.md.

  • session-context.jsonl grows unbounded. No rotation, no compaction, no cap. Long-lived Claude users will accumulate thousands of entries; the projector's readSessionContextSafe reads the file end-to-end every exchange, picking the newest match. Worst-case projection latency is O(history size). At minimum, a soak smoke that drives 10k hook events and asserts bounded projection latency would catch a regression here.

  • Upstream/projector routing has no conflict detection. Two adapters can register match() functions that both return true for the same request — for upstreams, first-registered wins (after priority sort); for projectors, first matching by priority wins. Silent wrong-routing is possible if a third-party adapter overlaps an Anthropic preset. There's no test that two presets/projectors can't legally co-match.

  • client_attach_idempotent.js smoke is NOT in PR scope despite exercising the rewritten attach path. If it wasn't re-run against this branch as part of CI, attach/detach regressions for the new state-file contract are uncovered. Worth confirming the smoke green-lights against d647fb9e.

  • Synchronous readdirSync on hot path.transcripts.js:walkJsonlFiles uses sync directory reads inside the projector. On a system with hundreds of Claude projects this blocks the daemon event loop per-exchange. Not a bug in itself, but a latency regression compared to 1.x where transcript-walking didn't happen on the projection path.

  • Codex review: (no findings reported)

Claude review

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

🤖 Generated with Claude Code


Reports: /Users/phil/testcity/.gc/pr-pipeline/reviews/pr-24 · Bead: hy-i31mv · Blast-radius: hy-99x61

@philcunliffe
philcunliffe marked this pull request as ready for review May 23, 2026 04:38
@philcunliffe
philcunliffe merged commit 55c37b8 into masterMay 23, 2026
6 checks passed
@platypii
platypii deleted the integration/ai-gateway-v2 branch May 28, 2026 06:37
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@philcunliffe