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.
philcunliffe and 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
Contributor Author

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

Source Finding (severity, evidence) Intersects
codex.md Contract & 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:192 Targets (message_projector.js −1273 net dispatcher rewrite; claude/projector.js new file with createClaudeExchangeProjector); Concurrency surface (Claude transcript walk in transcripts.js)
codex.md Behavioral 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:6 Targets (ai-gateway/source.js, ai-gateway/config.js); Config field chain (config.js declares priority, match, path_prefix for new upstream contract)
codex.md Change 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:105 Targets (ai-gateway/source.js; gateway_claude_capture.js smoke flow); Direct callers (gateway_claude_capture.js exercises the rewritten attach + projector wiring path)
claude.md selectCodexWorkspace 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 :410 Targets (codex/src/exchange-projector.js, +798, new file housing createCodexExchangeProjector)
claude.md readSessionContext 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-86 Targets (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 master May 23, 2026
6 checks passed
Sign up for free to 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