Skip to content

feat(ai): add the ai resource with an OpenAI-compatible gateway - #203

Merged
alongubkin merged 37 commits into
mainfrom
itamar/alien-39-ai-gateway-resource
Jul 30, 2026
Merged

feat(ai): add the ai resource with an OpenAI-compatible gateway#203
alongubkin merged 37 commits into
mainfrom
itamar/alien-39-ai-gateway-resource

Conversation

@ItamarZand88

Copy link
Copy Markdown
Contributor

Summary

Adds an ai resource that gives a workload an OpenAI-compatible endpoint for model inference, in two modes. By default it's keyless: for the host cloud's own models — Bedrock on AWS, Vertex on GCP, Foundry on Azure — a loopback gateway signs each request with the workload's ambient cloud identity, so there are no model API keys to store. A workload can instead bring its own provider key (e.g. OpenAI), in which case the client talks to that provider directly with the vault-resolved key and the gateway isn't involved.

What happens on the keyless path, when a workload declares an ai binding for one of the host cloud's models:

  1. A loopback AI gateway starts next to the app and its URL is placed in the environment; the app points a normal OpenAI-compatible client at it.
  2. The gateway translates each OpenAI-style request into the target cloud's native model call — Bedrock InvokeModel, Vertex rawPredict, or the Foundry Anthropic endpoint — and signs it with the workload's ambient cloud credentials. ← the heart of the change
  3. The response, including streamed tokens, is translated back to the OpenAI/Anthropic shape and returned to the app.

For a cloud's own models, this changes AI access from managing model API keys to using the cloud account the workload already runs in.

What I did

  • Add a gateway engine (Rust) that translates OpenAI-shaped requests to each cloud's model API and signs them with ambient credentials.
  • Add the ai resource to the SDK, plus the controllers and per-cloud setup that grant a workload access to that cloud's models.
  • Support a bring-your-own-key mode: the same ai binding can point at an external provider with the workload's own key (vault-resolved, redacted in logs), which bypasses the gateway and calls the provider directly.
  • Scope the ambient model-invocation grant to inference only: no deployment or data-plane writes.
  • Ship the gateway as one standalone binary everywhere: containers run it as their entrypoint, the SDK spawns it on first use and reads back the URL it prints, and compiled single-file Workers embed the binary and extract and spawn it at runtime.
  • Make getAvailableModels report real availability: the gateway probes each catalog model with a one-token request under the workload's own inference credential and lists only what the cloud actually has enabled (a 429 counts as enabled but rate-limited; 400/401/403/404 drop the model). Each entry carries provider and displayName so an app can build a model picker, and the example README documents each model's one-time enablement step per cloud.
  • Type the client surface: chat.completions.create and responses.create return OpenAI's own result types (a types-only dev dependency), so callers read .choices[0].message.content with no cast.
  • Add an ai-quickstart-ts example built around "list the available models, then pick one".

Files touched

  • crates/alien-ai-gateway — the gateway engine (router, model catalog, availability probe, per-cloud translation + signing) and the alien-ai-gateway launcher binary.
  • packages/ai-gateway — the TypeScript wrapper: resolves and spawns the gateway binary, and the ambient vs BYO-key connection resolution.
  • crates/alien-core, packages/core, packages/sdk — the ai resource type, the model catalog with per-model activation metadata, and the SDK surface (plus embedding the gateway binary into compiled Workers via packages/package-layout and crates/alien-build).
  • crates/alien-infra — the AI controllers and runtime wiring.
  • crates/alien-terraform + crates/alien-cloudformation — the per-cloud setup emitters and their snapshots.
  • crates/alien-permissions/permission-sets/ai/* — the ai/invoke grant (plus heartbeat/management/provision).
  • examples/ai-quickstart-ts — a runnable example.

How I tested

  • Manually: From a deployed test worker, real Claude requests went through the gateway with no API keys configured — Bedrock (AWS) and the Foundry Anthropic endpoint (Azure) both returned completions, including streaming. I also ran the gateway on my machine against real Bedrock and hit /v1/models: 36 of the 42 AWS catalog models came back listed and enriched, and the 6 dropped were Claude ids that account/region cannot serve.
  • Compiled Worker: bun build --compile of a Worker that calls ai() — the embedded gateway binary is extracted and spawned from inside the single-file binary and serves a loopback URL (the package-layout compile smoke).
  • Unit tests: cargo nextest across the workspace (CI-fast filter), including the availability-filter suite (mock upstream: an enabled model is kept and enriched, a 403 model is dropped, a 500 keeps the model and re-probes on the next call, a second call is served from cache); SDK vitest + biome; terraform validate on the AI setup-emitter output and the CloudFormation snapshot suite; generated-schema drift check clean. The live Bedrock test reads the shared test env and runs in the credentialed job.
  • E2E (ran green on real Bedrock): the comprehensive TypeScript app lists every model getAvailableModels returns and invokes each one, failing if a listed model is not invocable. That run passed after it caught a real filter bug: Bedrock answers 400 "The provided model identifier is invalid" for a model the account cannot address, and I had been counting 400 as available, so it now drops the model (400 joins 401/403/404) and the list only ever contains invocable models.
  • Anything I couldn't test: Vertex (GCP) live inference is pending a model-garden entitlement on the test project — unrelated to this code; the GCP emitter path is covered by snapshots + terraform validate.

One non-AI fix rides along

The cloud e2e for this PR overflowed its test thread's stack during deploy. It is a pre-existing issue, independent of the AI code: the deployment reconcile polls a deep async chain (executor to controllers to the cloud SDKs' tower/hyper/rustls futures) that needs about 3MB in a debug build, over tokio's 2MB default thread stack. Release binaries fit 2MB and are unaffected. RUST_MIN_STACK=8MB for dev/test threads via .cargo/config.toml fixes it, matching the headroom alien-worker-runtime already gives its runtime. It rides along here because this PR's own e2e could not run without it.

I also ran a security review on the diff. What it checked:

  • Over-scoped model grant. ai/invoke grants inference only — AWS bedrock:InvokeModel* (+ mantle CreateInference scoped to project/default, not project/*); a GCP custom role of just aiplatform.endpoints.predict/explain (not roles/aiplatform.user, which carries deploy + dataset-export); Azure "Cognitive Services OpenAI User" (data-plane only). No deployment writes or data reads. crates/alien-permissions/permission-sets/ai/invoke.jsonc.
  • Availability probe surface. The /v1/models probe sends a one-token inference request signed with that same inference-only grant; it adds no control-plane permission, cannot enable or subscribe anything, and its logs carry only the model id and status, never request or response bodies. crates/alien-ai-gateway/src/availability.rs.
  • Credential exposure. On the keyless path the gateway signs with the workload's ambient cloud identity — no key is stored. On the BYO-key path the provider key is a vault-resolved secret: its Debug impl redacts it (crates/alien-core/src/bindings/ai.rs) and it's resolved into the worker environment, not logged or held in control-plane state. crates/alien-ai-gateway/src/config.rs.
  • Off-host reachability. The gateway binds the loopback interface and hands the app its URL out of band, so it isn't reachable from other hosts. crates/alien-ai-gateway/src/lib.rs.
  • BYO-key providers. An external (bring-your-own-key) provider is explicitly not served by the ambient-credential gateway path. crates/alien-ai-gateway/src/config.rs.

Nothing turned up.

Continues #178 (same commits, rebased on main). That PR closed automatically when this branch was renamed.

@ItamarZand88

Copy link
Copy Markdown
Contributor Author

@greptile-apps

@greptile-apps

greptile-apps Bot commented Jul 26, 2026

Copy link
Copy Markdown

Greptile Summary

The follow-up updates the AI gateway implementation and closes the previously reported release and request-buffering gaps.

  • Adds a finite 32 MiB request-body limit with a structured 413 response.
  • Builds, packages, smoke-tests, and publishes the gateway wrapper and platform binaries during immutable npm dev releases.
  • Separates stable gateway qualification from publication, skips publication during dry runs, and makes GitHub release creation depend on successful gateway publication.

Confidence Score: 5/5

The PR appears safe to merge with respect to all previously reported Greptile findings.

No blocking failure remains: request buffering is bounded, dev releases include the wrapper and executable platform packages, stable qualification and publication are separated correctly, dry runs do not publish gateway packages, and final release creation waits for gateway publication.

Important Files Changed

Filename Overview
crates/alien-ai-gateway/src/router/mod.rs Adds and tests a finite 32 MiB body limit, resolving the previously reported unbounded request buffering.
.github/workflows/publish-npm-dev.yml Builds, stages, packs, smoke-installs, and publishes the gateway wrapper and all supported platform packages in dependency order.
.github/workflows/release.yml Qualifies gateway packages without npm publication, publishes only qualified artifacts in non-dry-run publish mode, and gates release creation on successful gateway publication.
packages/ai-gateway/package.json Defines the gateway wrapper package and platform-specific optional dependency surface used by the updated publication workflows.
packages/sdk/package.json Adds the AI gateway wrapper dependency consumed by the SDK.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Build["Build platform gateway binaries"] --> Qualify["Pack and upload qualified gateway artifacts"]
  Qualify --> Publish["Publish platform packages, then wrapper"]
  Publish --> Release["Create GitHub release"]
Loading

Reviews (11): Last reviewed commit: "fix(ci): stop building the ai-gateway wr..." | Re-trigger Greptile

Comment thread packages/sdk/package.json
Comment thread crates/alien-ai-gateway/src/router/mod.rs Outdated
@ItamarZand88
ItamarZand88 force-pushed the itamar/alien-39-ai-gateway-resource branch 2 times, most recently from a768563 to f5d5ed4 Compare July 26, 2026 11:59
@ItamarZand88

Copy link
Copy Markdown
Contributor Author

@greptile-apps

Comment thread .github/workflows/publish-npm-dev.yml
@ItamarZand88
ItamarZand88 force-pushed the itamar/alien-39-ai-gateway-resource branch from f5d5ed4 to 7b0f0a2 Compare July 26, 2026 16:09
@ItamarZand88

Copy link
Copy Markdown
Contributor Author

@greptile-apps

@ItamarZand88

Copy link
Copy Markdown
Contributor Author

@greptile-apps

Comment thread .github/workflows/release.yml Outdated
@ItamarZand88

Copy link
Copy Markdown
Contributor Author

@greptile-apps

1 similar comment
@ItamarZand88

Copy link
Copy Markdown
Contributor Author

@greptile-apps

@ItamarZand88
ItamarZand88 force-pushed the itamar/alien-39-ai-gateway-resource branch from 5802108 to 612f0dd Compare July 26, 2026 22:04
Comment thread .github/workflows/release.yml Outdated
@ItamarZand88

Copy link
Copy Markdown
Contributor Author

@greptile-apps

@ItamarZand88
ItamarZand88 force-pushed the itamar/alien-39-ai-gateway-resource branch 2 times, most recently from 4f20319 to e37599c Compare July 27, 2026 09:39
@ItamarZand88

Copy link
Copy Markdown
Contributor Author

@greptile-apps

The native controller enables aiplatform.googleapis.com at runtime, but a Frozen/Terraform GCP deploy relies on this activation being injected here, or the first Vertex call fails with SERVICE_DISABLED.
Hold the metadata cache lock across the IMDS fetch so a burst of concurrent probes (the /v1/models fan-out authorizes every model at once) collapses to one refresh instead of stampeding the metadata service.
providerBaseUrl defaulted any provider other than "anthropic" to OpenAI's endpoint, and the projected BYO key is attached as a bearer token there, so a typo or unlisted provider shipped the customer's key to OpenAI. Resolve only the providers we have a base URL for and throw otherwise; the ALIEN_AI_LOCAL_BASE_URL override still allows any OpenAI-compatible provider.
The BYO-Anthropic model picker returned retired claude-3-5-* ids. Return the current-generation aliases api.anthropic.com serves instead.
router.rs had grown to hold the transport, per-cloud URL dispatch, three provider adapters, the Bedrock body normalization, and the event-stream codec in one file. Move each provider's request path into its own module under router/ (bedrock/vertex/foundry) plus the event-stream decoder (eventstream), keeping mod.rs as the transport and dispatch. Behavior-preserving; the shared integration test harness stays in mod.rs.
The gateway is a pure proxy, so the upstream enforces its own body size, but axum's 2 MB default rejected large legitimate requests (base64 vision images, long tool-heavy conversations) with 413 before they ever left us. Disable the limit on the router.
reqwest has no default request timeout, so a hung instance metadata service would wedge /v1/models forever — and holding the single-flight lock across the fetch makes that block every waiting caller, not just one. Cap the fetch at 10s (the endpoint is link-local and answers in well under a second) so a real hang fails fast instead of hanging.
Anthropic's OpenAI-compatible host serves /v1/chat/completions but not the Responses API, so a BYO-Anthropic responses.create() returned an opaque 404. Detect the Anthropic native base and throw a clear reason before the request; an ALIEN_AI_LOCAL_BASE_URL override resolves elsewhere and is not blocked.
The three new AI ImportData types were never registered in the snapshot map, so the pinned schemas had no AI entry and the test passed without covering them.
Claude over classic InvokeModel signs with bedrock, not bedrock-mantle; bedrock-mantle serves the Responses API. The doc had the two the other way round.
Two defects in the same manifest. The wrapper version had drifted from the Cargo workspace and every sibling npm package, so validate-release-version.mjs would reject the next release and the loader would look for a prebuild tag that is never published. Separately, the OpenAI result types reach the emitted client.d.ts, so `openai` cannot be a devDependency alone: a consumer typechecking against us gets TS2307 unless skipLibCheck hides it. Declaring it an optional peer dependency matches how packages/testing declares its own optional peers, and needs no lockfile change.
…ror shape

Disabling the limit outright let any colocated process make the gateway buffer, parse, and reserialize an arbitrarily large payload. 32 MB clears the largest upstream request limit we serve (Bedrock InvokeModel, 25 MB) so the upstream still owns rejecting its own oversized payloads. axum answers its own body-limit rejection as bare plain text, which would leave this the one gateway failure a caller cannot parse as JSON, so a wrapper extractor maps it to ErrorData::RequestTooLarge.
The sdk depends on @alienplatform/ai-gateway at runtime, but the dev release neither versioned nor packed it, so the sdk kept an unresolvable workspace:^ range and the smoke install rejected it before anything could publish. The channel now builds the launcher for each platform alongside the napi addon and publishes the six prebuild packages ahead of the wrapper that pins them, so an ambient AI call resolves a binary instead of exhausting the loader. The publish loop is an allowlist of globs, so it now fails when a packed package matches none of them. Also unroots the hardcoded core version in the test beside it, which stopped matching once core left 1.14.x.
The reason named a specific target account and its pending grant status. The other live suites state only the grant the test needs.
The gating matrix requires every registered emitter type to be policy-refused or proven to render gated, and the AI emitters are registered on all three clouds, so the branch cannot merge without choosing. The TS builder joins ResourceBuilder for the gate, the type joins the gateability manifest, and the matrix gains ai fixtures plus per-cloud snapshots. The dedicated tests pin what matters on a declined deploy: every inference grant rides the gate through its own emission path (the ai emitter role policies on AWS, the service-account emitter binding as a gated contribution on GCP, the ai emitter role assignments on Azure), and the cognitive account and its model deployments carry it too.
The generated module README documents the deployment_input_values output; the ai minimal renders predate that text. Documentation-only drift, no resource changes.
Bundled cloud Workers run in the versioned Worker base and package only their bundle and source map, so the launcher the ai() client spawns has to live in the base image, the same way the bindings addon does. The loader resolves ./alien-ai-gateway from the bundle working directory before the prebuild route, which a bundle cannot use (no node_modules). Compiled host-process workloads keep the embedded route. The e2e build jobs compile the musl launcher alongside alien-worker-runtime so the base image job can stage it per arch.
A cloud Worker is bundled, not compiled, since the runtime moved into the versioned base image. The bootstrap still emitted the SDK native bridge, which registers both embedded pieces, and outside `bun build --compile` the gateway import degrades to the literal asset path `./alien-ai-gateway.bin` that no bundle packages. Registering it won the loader race against the launcher the base image stages, so every ai() call in a Worker failed on an unextractable embedded path. A bindings-only bridge leaves the gateway to the staged launcher and stops the bundle depending on an asset that staging documents as best-effort. The e2e handler reports the unsanitized error so a cloud-only failure can name itself.
The job built and published the gateway packages in one step during qualification, so an abandoned or amended release left permanent npm versions that were never promoted, and the release could finalize without them because nothing depended on it. Split it the way bindings already is: qualification packs the prebuilds and the wrapper into a qualified artifact and publishes nothing, publish resolves that artifact from the qualification run and publishes prebuilds before the wrapper that pins them. The stable channel now waits for that publish, so a released sdk cannot reference a gateway package that was never pushed.
main released 3.2.0 while this branch was open. The wrapper is not in the release bump list until it merges, so its version is carried by hand; validate-release-version.mjs holds it against the Cargo workspace.
@ItamarZand88
ItamarZand88 force-pushed the itamar/alien-39-ai-gateway-resource branch from bd39244 to ac278b1 Compare July 30, 2026 10:03
@ItamarZand88

Copy link
Copy Markdown
Contributor Author

@greptile-apps

Comment thread .github/workflows/release.yml Outdated
The split dropped the dry-run branch the single job used to carry, leaving the publish step to reach the registry on a validation run. A published version cannot be withdrawn, and a dry run skips verify_qualification so there is no qualification run to download from either — the job is skipped outright instead of guarding inside the publish script.
main no longer builds workspace packages at install time (#236), so the wrapper's prepare script now runs before its core dependency has a dist and fails every pnpm install. Drop it and build core explicitly before the wrapper in the qualify job, which relied on the install-time build.
@ItamarZand88
ItamarZand88 force-pushed the itamar/alien-39-ai-gateway-resource branch from a553489 to a14b8e7 Compare July 30, 2026 10:45
@ItamarZand88

Copy link
Copy Markdown
Contributor Author

@greptile-apps

@alongubkin
alongubkin merged commit 1dd5d0e into main Jul 30, 2026
21 checks passed
@alongubkin
alongubkin deleted the itamar/alien-39-ai-gateway-resource branch July 30, 2026 11:16
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.

2 participants