Skip to content

Commit 77263f8

Browse files
committed
feat(ai-gateway): add a Next.js app that serves the AI gateway routes
The routes are backed by the handlers in packages/web-shared and all live under /api/v1, without the /api/gateway and /api/openrouter prefixes the apps/web routes from #6737 use. The apps/web v1 variants collapse into the same routes, so /api/gateway/v1/models and /api/openrouter/models both become /api/v1/models. /api/fim, /api/edit and /api/organizations move under /api/v1 as well, and the typesafe gateway /api/gateway/typesafe/v1/systemone becomes /api/v1/systemone. src/proxy.ts keeps the blocked-client check for /api/v1/fim/completions. The app is not deployed. Its dev script runs next dev with the web app's env files and points app URLs at the web app.
1 parent a99e44b commit 77263f8

24 files changed

Lines changed: 292 additions & 16 deletions

File tree

‎AGENTS.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ nearer `AGENTS.md` and follow its scoped invariants.
99
| Path | Description |
1010
|---|---|
1111
| `apps/web/` | Next.js web application deployed to Vercel |
12+
| `apps/ai-gateway/` | Next.js app for the AI gateway API routes; not deployed yet |
1213
| `apps/mobile/` | React Native mobile application |
1314
| `apps/extension/` | WXT browser extension |
1415
| `services/` | Cloudflare Worker and supporting services |

‎apps/ai-gateway/.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Linked from apps/web by scripts/dev.sh
2+
/.env

‎apps/ai-gateway/AGENTS.md‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# ai-gateway
2+
3+
Next.js app that serves the AI gateway API on its own. It is not deployed yet;
4+
`apps/web` still serves the same handlers under its own paths.
5+
6+
- Every route lives under `/api/v1`. There are no `/api/gateway` or `/api/openrouter`
7+
aliases. Each `apps/web` route maps to the path without that prefix, and the
8+
`v1` variants in `apps/web` collapse into the same route: `/api/gateway/v1/models`
9+
and `/api/openrouter/models` both become `/api/v1/models`. `/api/fim`,
10+
`/api/edit`, `/api/organizations` and the typesafe `/api/gateway/typesafe/v1/systemone`
11+
move under `/api/v1` too; the latter becomes `/api/v1/systemone`.
12+
- Route files are thin facades over handlers in `packages/web-shared`. Keep the
13+
`apps/web` `maxDuration` and `withRestTiming` usage, with route patterns that
14+
match this app's paths. See `packages/web-shared/AGENTS.md` for how `@/`
15+
imports resolve.
16+
- Handlers that depend on the path accept both apps' paths: the LLM proxy's path
17+
validation and the blocked-client check in `src/proxy.ts` for
18+
`/api/v1/fim/completions`.
19+
- `pnpm dev` runs with the web app's environment files. Start it with
20+
`pnpm dev:start ai-gateway`.

‎apps/ai-gateway/next.config.mjs‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { resolve } from 'path';
2+
3+
const monorepoRoot = resolve(import.meta.dirname, '../..');
4+
5+
/** @type {import('next').NextConfig} */
6+
const nextConfig = {
7+
poweredByHeader: false,
8+
9+
// Both values MUST be set to the monorepo root and kept in sync; see
10+
// apps/web/next.config.mjs. The routes are served from packages/web-shared.
11+
outputFileTracingRoot: monorepoRoot,
12+
turbopack: {
13+
root: monorepoRoot,
14+
},
15+
16+
// Same as apps/web, so gateway clients see identical trailing-slash handling.
17+
skipTrailingSlashRedirect: true,
18+
};
19+
20+
export default nextConfig;

‎apps/ai-gateway/package.json‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "ai-gateway",
3+
"version": "0.1.0",
4+
"private": true,
5+
"scripts": {
6+
"dev": "bash scripts/dev.sh",
7+
"build": "next build",
8+
"start": "next start",
9+
"typecheck": "GOMEMLIMIT=2GiB tsgo --noEmit"
10+
},
11+
"dependencies": {
12+
"@kilocode/web-shared": "workspace:*",
13+
"@opentelemetry/api": "1.9.1",
14+
"next": "16.3.3",
15+
"react": "19.2.6",
16+
"react-dom": "19.2.6"
17+
},
18+
"devDependencies": {
19+
"@playwright/test": "1.58.2",
20+
"@swc/core": "1.15.18",
21+
"@types/node": "catalog:",
22+
"@types/react": "19.2.14",
23+
"@types/react-dom": "19.2.3",
24+
"@typescript/native-preview": "catalog:",
25+
"babel-plugin-react-compiler": "1.0.0",
26+
"typescript": "catalog:"
27+
}
28+
}

‎apps/ai-gateway/scripts/dev.sh‎

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# The ai-gateway app serves a slice of the web app's routes, so it runs with the
5+
# web app's environment. PORT is left alone: the dev session exports it for the
6+
# web app.
7+
REPO_ROOT="$(git rev-parse --show-toplevel)"
8+
WEB_DIR="$REPO_ROOT/apps/web"
9+
OFFSET=${KILO_PORT_OFFSET:-0}
10+
AI_GATEWAY_PORT=${AI_GATEWAY_PORT:-$((3010 + OFFSET))}
11+
12+
# Next.js loads .env* files from CWD. Link the files the web app reads: its
13+
# committed .env, plus the local files from apps/web or, failing that, the repo
14+
# root (the same fallback scripts/dev.sh applies for the web app).
15+
link_env_file() {
16+
local name="$1"
17+
local source=""
18+
if [ -e "$WEB_DIR/$name" ]; then
19+
source="$WEB_DIR/$name"
20+
elif [ -e "$REPO_ROOT/$name" ]; then
21+
source="$REPO_ROOT/$name"
22+
fi
23+
if [ -n "$source" ] && [ ! -e "$name" ]; then
24+
ln -s "$source" "$name"
25+
echo "Symlinked $source → $(pwd)/$name"
26+
fi
27+
}
28+
for envfile in .env .env.local .env.development.local; do
29+
link_env_file "$envfile"
30+
done
31+
32+
# Read a value from the linked env files without sourcing them. Exported
33+
# process env beats Next's own .env loading, so each default below only applies
34+
# when neither the shell nor the env files set the variable, as in the web app.
35+
read_env_value() {
36+
local key="$1"
37+
local file
38+
local value
39+
40+
for file in .env.development.local .env.local; do
41+
if [ -f "$file" ]; then
42+
value=$(awk -F= -v key="$key" '
43+
$1 == key {
44+
value = substr($0, length(key) + 2)
45+
gsub(/^["'\'']|["'\'']$/, "", value)
46+
print value
47+
exit
48+
}
49+
' "$file")
50+
if [ -n "$value" ]; then
51+
echo "$value"
52+
return
53+
fi
54+
fi
55+
done
56+
}
57+
58+
REDIS_URL="${REDIS_URL:-$(read_env_value REDIS_URL)}"
59+
export REDIS_URL="${REDIS_URL:-redis://localhost:6379}"
60+
UPSTASH_REDIS_REST_URL="${UPSTASH_REDIS_REST_URL:-$(read_env_value UPSTASH_REDIS_REST_URL)}"
61+
export UPSTASH_REDIS_REST_URL="${UPSTASH_REDIS_REST_URL:-http://localhost:8079}"
62+
export UPSTASH_REDIS_REST_TOKEN="${UPSTASH_REDIS_REST_TOKEN:-example_token}"
63+
# Links in responses, such as sign-in and billing URLs, must point at the web
64+
# app rather than at this app.
65+
WEB_PORT=$(cat "$REPO_ROOT/.dev-port" 2>/dev/null || echo $((3000 + OFFSET)))
66+
APP_URL_OVERRIDE="${APP_URL_OVERRIDE:-$(read_env_value APP_URL_OVERRIDE)}"
67+
export APP_URL_OVERRIDE="${APP_URL_OVERRIDE:-http://localhost:$WEB_PORT}"
68+
NEXTAUTH_URL="${NEXTAUTH_URL:-$(read_env_value NEXTAUTH_URL)}"
69+
export NEXTAUTH_URL="${NEXTAUTH_URL:-$APP_URL_OVERRIDE}"
70+
71+
echo "ai-gateway dev server starting on port $AI_GATEWAY_PORT"
72+
exec next dev -H "${NEXT_DEV_HOSTNAME:-0.0.0.0}" -p "$AI_GATEWAY_PORT" "$@"
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { NextRequest } from 'next/server';
2+
import { handleLlmProxyRequest } from '@/lib/ai-gateway/handlers/llm-proxy';
3+
import { withRestTiming } from '@/lib/observability/request-timing';
4+
5+
export const POST = withRestTiming('/api/v1/[...path]', (request: Request) =>
6+
handleLlmProxyRequest(request as NextRequest)
7+
);
8+
9+
export const maxDuration = 800;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { NextRequest } from 'next/server';
2+
import { handleAudioTranscriptionsRequest } from '@/lib/ai-gateway/handlers/audio-transcriptions';
3+
import { withRestTiming } from '@/lib/observability/request-timing';
4+
5+
export const POST = withRestTiming('/api/v1/audio/transcriptions', (request: Request) =>
6+
handleAudioTranscriptionsRequest(request as NextRequest)
7+
);
8+
9+
export const maxDuration = 800;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { handleEditCompletionsRequest as POST } from '@/lib/ai-gateway/handlers/edit-completions';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { handleEmbeddingModelsRequest as GET } from '@/lib/ai-gateway/handlers/embedding-models';

0 commit comments

Comments
 (0)