Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 10 additions & 6 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
PALANTIR_AIP_URL=https://example.palantirfoundry.com
PALANTIR_AIP_API_TOKEN=your-foundry-token

# Optional. When set, clients must send Authorization: Bearer <API_KEY>.
# API_KEY=

# Optional
HOST=0.0.0.0
PORT=3000
LOG_DIR=logs/
LOG_LEVEL=INFO

# Required
JUNIPER_API_KEY=demo

# Optional, used by live integration tests
TEST_FOUNDRY_URL=https://example.palantirfoundry.com
TEST_FOUNDRY_KEY=your-foundry-token
31 changes: 31 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: CI

on:
push:
pull_request:

jobs:
test:
name: Lint, Typecheck, Test
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Lint and format check
run: bun run check

- name: Typecheck
run: bun run typecheck

- name: Test
run: bun run test:unit
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
node_modules
.env
.DS_Store
logs/
3 changes: 3 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"recommendations": ["biomejs.biome"]
}
20 changes: 20 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"[json]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[jsonc]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"biome.enabled": true,
"editor.codeActionsOnSave": {
"source.organizeImports.biome": "explicit",
"source.fixAll.biome": "explicit"
},
"editor.detectIndentation": false,
"editor.formatOnSave": true,
"editor.insertSpaces": true,
"editor.tabSize": 4
}
136 changes: 22 additions & 114 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,125 +1,33 @@
# PAIP-OpenAI-API
An OpenAI-compatible API backed by the Palantir AIP LLM Proxy.
# Juniper
Juniper is an OpenAI-compatible API gateway for Palantir Foundry's AIP LLM Proxy.

## Setup
```bash
git clone https://github.com/hazelcaffe/paip-openai-api
cd paip-openai-api
bun install
cp .env.example .env
```sh
# Requires: Git, Bun
git clone https://github.com/hazelcaffe/juniper.git
cd juniper
bun i
```
Configure `.env`:
```dotenv
PALANTIR_AIP_URL=https://example.palantirfoundry.com
PALANTIR_AIP_API_TOKEN=your-foundry-token

# Optional. When set, clients must send Authorization: Bearer <API_KEY>.
API_KEY=

`.env`:
```
## SERVER CONFIGURATION (Optional) ##
HOST=0.0.0.0
PORT=3000
```
Start the server:
```bash
bun run start
```
For development with automatic reload:
```bash
bun run dev
```
OpenAPI documentation is available at [`http://localhost:3000/docs`](http://localhost:3000/docs).

## How Authentication Works
Palantir credentials are server credentials. Configure them once on the API server with `PALANTIR_AIP_URL` and `PALANTIR_AIP_API_TOKEN`; clients must not receive or submit those credentials.

`API_KEY` is an optional credential for this gateway:
- When `API_KEY` is set, clients send `Authorization: Bearer <API_KEY>`.
- When `API_KEY` is unset, `/v1/*` is unauthenticated.

The gateway replaces the client authorization header with the Foundry token before contacting Palantir.

## OpenAI SDK
Use a model ID from `GET /v1/models` (e.g. `gpt-5.5`, `claude-opus-4.8`, etc.)
```ts
import OpenAI from "openai";

const client = new OpenAI({
apiKey: process.env.PAIP_OPENAI_API_KEY,
baseURL: "http://localhost:3000/v1"
});
## LOG CONFIGURATIO (Optional) ##
LOG_DIR=logs/
LOG_LEVEL=INFO

const completion = await client.chat.completions.create({
model: "claude-opus-4.8",
messages: [
{
role: "user",
content: "Explain zero data retention in one paragraph."
}
]
});
### REQUIRED ###
JUNIPER_API_KEY=demo

console.log(completion.choices[0]?.message.content);
## TEST CONFIGURATION (Optional) ##
TEST_FOUNDRY_URL=https://example.palantirfoundry.com
TEST_FOUNDRY_KEY=your-foundry-token
```
The Responses API is also supported:
```ts
const response = await client.responses.create({
model: "gemini-3.1-pro",
input: "Write a concise release note."
});
`bun start`

console.log(response.output_text);
```

## HTTP Endpoints
| Method | Path | Description |
| --- | --- | --- |
| `GET` | `/v1/models` | Lists models |
| `POST` | `/v1/chat/completions` | OpenAI Chat Completions-compatible API |
| `POST` | `/v1/responses` | OpenAI Responses-compatible API |
| `POST` | `/v1/embeddings` | Foundry OpenAI embeddings |
| `GET` | `/health` | Health check |
| `GET` | `/docs` | OpenAPI documentation |

Example:
```bash
curl http://localhost:3000/v1/chat/completions \
-H "Authorization: Bearer $PAIP_OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.5",
"messages": [
{
"role": "user",
"content": "Hello"
}
]
}'
```

## Provider Behavior
- OpenAI models use Foundry's OpenAI endpoints directly, preserving native request fields, response fields, tool calls, and token streaming.
- Anthropic models are translated to the Anthropic Messages endpoint.
- Gemini models are translated to the Google `generateContent` endpoint.
- OpenAI-style function tools, system messages, text, and common image inputs are adapted for Anthropic and Gemini.
- For adapted providers, `stream: true` returns valid SSE framing after the provider response completes. It is not token-by-token streaming.
- `attribution`, `traceparent`, and `tracestate` request headers are forwarded to Foundry.
## Usage
Use any AI SDK you like! As long as it supports OpenAI-compatible APIs, it'll work.

The supplied model catalog contains generation models but no embedding model. For `/v1/embeddings`, pass the embedding model RID from Foundry Model Catalog as the `model` value.

## Models
Requests accept:
- The public model ID, such as `gpt-5.5`, `claude-opus-4.8`, or `gemini-3.1-pro`
- The full Foundry model RID

The current catalog includes models like GPT 5.5, Claude Opus 4.8, Gemini 3.1 Pro, and more. For a full list, check [`src/models.ts`](src/models.ts).

xAI and other models listed in the Model Catalog are not available as they cannot be queried from the LLM proxy.

## Development
```bash
bun run typecheck
# Requires .env to be properly configured
bun test
```
## License
[MIT](LICENSE)
Docs are available at `/docs`.
57 changes: 57 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"$schema": "https://biomejs.dev/schemas/2.5.2/schema.json",
"assist": {
"actions": {
"source": {
"organizeImports": "on"
}
},
"enabled": true
},
"files": {
"includes": ["**", "!node_modules", "!bun.lock", "!pnpm-lock.yaml"]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 4,
"lineWidth": 100
},
"javascript": {
"formatter": {
"quoteStyle": "double",
"semicolons": "always",
"trailingCommas": "none"
}
},
"json": {
"formatter": {
"trailingCommas": "none"
}
},
"linter": {
"enabled": true,
"rules": {
"preset": "recommended",
"complexity": {
"useLiteralKeys": "off",
"noBannedTypes": "error",
"noForEach": "error"
},
"correctness": {
"noUnusedImports": "error",
"noUnusedVariables": "error",
"useExhaustiveDependencies": "off"
},
"style": {
"noDefaultExport": "error",
"useConst": "error",
"useImportType": "error",
"useTemplate": "error"
},
"suspicious": {
"noExplicitAny": "error"
}
}
}
}
25 changes: 25 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions examples/fetch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const baseUrl = process.env["JUNIPER_BASE_URL"] ?? "http://localhost:3000";
const foundryKey = process.env["TEST_FOUNDRY_KEY"];
const foundryUrl = process.env["TEST_FOUNDRY_URL"];

if (foundryKey === undefined || foundryKey.length === 0) {
throw new Error("TEST_FOUNDRY_KEY is required.");
}

if (foundryUrl === undefined || foundryUrl.length === 0) {
throw new Error("TEST_FOUNDRY_URL is required.");
}

const response = await fetch(`${baseUrl}/v1/chat/completions`, {
body: JSON.stringify({
messages: [{ content: "Say hello from Juniper.", role: "user" }],
model: "gpt-5.5"
}),
headers: {
Authorization: `Bearer ${foundryKey}`,
"Content-Type": "application/json",
"X-Foundry-URL": foundryUrl
},
method: "POST"
});

if (!response.ok) {
throw new Error(`Juniper returned ${response.status}: ${await response.text()}`);
}

console.log(await response.json());
Loading
Loading