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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,8 @@ jobs:
- name: Typecheck
run: yarn typecheck

- name: Test
run: yarn test

- name: Build
run: yarn build
50 changes: 28 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,11 @@ yarn build

### 3. Run the applications (and set your API key)

The apps can call either the **standard OpenAI API** or your own **Azure OpenAI**-hosted models. Configure whichever you have access to via environment variables — the same `VITE_OPENAI_API_KEY` variable is used in both cases (it holds either your OpenAI key or your Azure OpenAI key).
The apps can call either the **standard OpenAI API** or your own **Azure OpenAI**-hosted models. Configure whichever you have access to via environment variables — the same `OPENAI_API_KEY` variable is used in both cases (it holds either your OpenAI key or your Azure OpenAI key).

> **The API key stays on the server.** These variables are deliberately **not** prefixed with `VITE_`, so Vite cannot inline them into the browser bundle. The dev/preview server proxies requests to your provider at `/api/openai` and attaches the credential server-side. See [`packages/promptions-openai-proxy`](packages/promptions-openai-proxy/README.md).

> **Upgrading from an earlier version?** Delete `VITE_OPENAI_API_KEY` from your `.env` files and rotate that key. Vite serves every `VITE_`-prefixed variable to client code, so a key left there is still readable by anyone loading the app even though no code references it. **The dev server refuses to start while that variable is set**, so you cannot miss this step.

Option A — .env files (recommended for local development):

Expand All @@ -106,9 +110,9 @@ Option A — .env files (recommended for local development):
- Create `apps/promptions-chat/.env` (and `apps/promptions-image/.env`) with:

```dotenv
VITE_OPENAI_API_KEY=your_openai_api_key_here
OPENAI_API_KEY=your_openai_api_key_here
# Optional: override the chat model (defaults to gpt-5.4-nano).
# VITE_OPENAI_MODEL=gpt-5.4-nano
# OPENAI_MODEL=gpt-5.4-nano
```

**Azure OpenAI** (using your own hosted deployment)
Expand All @@ -117,47 +121,49 @@ Option A — .env files (recommended for local development):

```dotenv
# Your Azure OpenAI resource key
VITE_OPENAI_API_KEY=your_azure_openai_key_here
OPENAI_API_KEY=your_azure_openai_key_here
# Your Azure OpenAI resource endpoint
VITE_OPENAI_BASE_URL=https://your-resource.openai.azure.com
OPENAI_BASE_URL=https://your-resource.openai.azure.com
# Required for Azure OpenAI
VITE_OPENAI_API_VERSION=2024-12-01-preview
OPENAI_API_VERSION=2024-12-01-preview
# On Azure, this is your DEPLOYMENT NAME (not the underlying model id).
# Ensure this deployment targets a chat-completions-compatible model.
VITE_OPENAI_MODEL=your_chat_deployment_name
OPENAI_MODEL=your_chat_deployment_name
```

Option B — set it in your shell (PowerShell example):

```powershell
# Chat app — standard OpenAI
$env:VITE_OPENAI_API_KEY="your_openai_api_key_here" ; yarn workspace @promptions/promptions-chat dev
$env:OPENAI_API_KEY="your_openai_api_key_here" ; yarn workspace @promptions/promptions-chat dev

# Chat app — Azure OpenAI
$env:VITE_OPENAI_API_KEY="your_azure_openai_key_here"
$env:VITE_OPENAI_BASE_URL="https://your-resource.openai.azure.com"
$env:VITE_OPENAI_API_VERSION="2024-12-01-preview"
$env:VITE_OPENAI_MODEL="your_chat_deployment_name"
$env:OPENAI_API_KEY="your_azure_openai_key_here"
$env:OPENAI_BASE_URL="https://your-resource.openai.azure.com"
$env:OPENAI_API_VERSION="2024-12-01-preview"
$env:OPENAI_MODEL="your_chat_deployment_name"
yarn workspace @promptions/promptions-chat dev

# Image app (swap workspace name; same variable conventions apply)
$env:VITE_OPENAI_API_KEY="your_openai_api_key_here" ; yarn workspace @promptions/promptions-image dev
$env:OPENAI_API_KEY="your_openai_api_key_here" ; yarn workspace @promptions/promptions-image dev
```

#### Configuration reference

Both apps read these `VITE_*` variables from their respective `.env` files.
Both apps read these variables from their respective `.env` files. They are read by the dev/preview server only. `OPENAI_API_KEY` and `OPENAI_BASE_URL` never reach the browser; `OPENAI_API_VERSION`, `OPENAI_MODEL` and `OPENAI_IMAGE_MODEL` are injected into client code, and are not secret.

| Variable | Description | Default |
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | -------------- |
| `VITE_OPENAI_API_KEY` | **Required.** Your OpenAI API key, or your Azure OpenAI resource key when `VITE_OPENAI_BASE_URL` is set. | _(unset)_ |
| `VITE_OPENAI_MODEL` | Chat model used for completions. On Azure OpenAI this is the **deployment name**. The image-generation model is selected in the UI. | `gpt-5.4-nano` |
| `VITE_OPENAI_BASE_URL` | Custom endpoint. Set this to use Azure OpenAI (e.g. `https://your-resource.openai.azure.com`) or another OpenAI-compatible service. | _(unset)_ |
| `VITE_OPENAI_API_VERSION` | API version. **Required** when `VITE_OPENAI_BASE_URL` points at Azure OpenAI (e.g. `2024-12-01-preview`). | _(unset)_ |
| Variable | Description | Default |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | -------------- |
| `OPENAI_API_KEY` | **Required.** Your OpenAI API key, or your Azure OpenAI resource key when `OPENAI_BASE_URL` is set. | _(unset)_ |
| `OPENAI_MODEL` | Chat model used for completions. On Azure OpenAI this is the **deployment name**. The image-generation model is selected in the UI. | `gpt-5.4-nano` |
| `OPENAI_BASE_URL` | Custom endpoint. Set this to use Azure OpenAI (e.g. `https://your-resource.openai.azure.com`) or another OpenAI-compatible service. | _(unset)_ |
| `OPENAI_API_VERSION` | API version. **Required** when `OPENAI_BASE_URL` points at Azure OpenAI (e.g. `2024-12-01-preview`). | _(unset)_ |
| `OPENAI_API_STYLE` | `openai` or `azure`. Overrides how the credential is sent, for OpenAI-compatible backends that need a custom `OPENAI_BASE_URL`. | inferred |
| `OPENAI_IMAGE_MODEL` | Image model used by the image app. On Azure OpenAI this is the image **deployment name**, which need not match the model id. | `gpt-image-1` |

When `VITE_OPENAI_BASE_URL` is set, the apps use the Azure OpenAI client; otherwise they use the standard OpenAI client.
When `OPENAI_BASE_URL` is set, the apps use Azure OpenAI conventions (`api-key` header, deployment-based URLs); otherwise they use the standard OpenAI conventions (`Authorization: Bearer`). Set `OPENAI_API_STYLE=openai` to use a custom endpoint with standard OpenAI conventions.

> **Model compatibility:** The chat reference app uses `VITE_OPENAI_MODEL`, defaulting to `gpt-5.4-nano`. On Azure OpenAI, make sure the deployment named in `VITE_OPENAI_MODEL` targets a chat-completions-compatible model.
> **Model compatibility:** The chat reference app uses `OPENAI_MODEL`, defaulting to `gpt-5.4-nano`. On Azure OpenAI, make sure the deployment named in `OPENAI_MODEL` targets a chat-completions-compatible model, and set `OPENAI_IMAGE_MODEL` to your image deployment name — the image app otherwise requests the model id `gpt-image-1`, which Azure resolves as a deployment name.

Start the dev servers:

Expand Down
21 changes: 15 additions & 6 deletions apps/promptions-chat/.env.example
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
# Copy this file to .env

# Required: Set your API key
VITE_OPENAI_API_KEY=your_openai_api_key_here
# These variables are read by the Vite dev/preview server only. They are NOT
# prefixed with VITE_, so Vite cannot inline them into the browser bundle.
#
# MIGRATION: earlier versions of this app used VITE_OPENAI_API_KEY. The dev
# server refuses to start while that variable is set — delete it from your .env
# and rotate the credential. Vite serves every VITE_-prefixed variable to client
# code, so any key left there is exposed to the browser even though no code
# reads it any more.

# Required: your API key. Stays server-side; the browser talks to /api/openai.
OPENAI_API_KEY=your_openai_api_key_here

# Optional: only set for Azure or other custom OpenAI-compatible endpoints.
# Omit for standard OpenAI API usage.
# VITE_OPENAI_BASE_URL=https://your-resource.openai.azure.com
# OPENAI_BASE_URL=https://your-resource.openai.azure.com

# Optional: API version is typically Azure-specific/custom-endpoint specific.
# Required when VITE_OPENAI_BASE_URL points at Azure OpenAI.
# VITE_OPENAI_API_VERSION=2024-12-01-preview
# Required when OPENAI_BASE_URL points at Azure OpenAI.
# OPENAI_API_VERSION=2024-12-01-preview

# Optional: override the chat model (defaults to gpt-5.4-nano).
# VITE_OPENAI_MODEL=gpt-5.4-nano
# OPENAI_MODEL=gpt-5.4-nano
30 changes: 17 additions & 13 deletions apps/promptions-chat/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,20 +41,24 @@ cp .env.example .env
**Standard OpenAI** — edit `.env` and add your OpenAI API key:

```
VITE_OPENAI_API_KEY=your_api_key_here
OPENAI_API_KEY=your_api_key_here
```

**Azure OpenAI** — to use your own Azure-hosted deployment, set:

```
VITE_OPENAI_API_KEY=your_azure_openai_key_here
VITE_OPENAI_BASE_URL=https://your-resource.openai.azure.com
VITE_OPENAI_API_VERSION=2024-12-01-preview
# On Azure, VITE_OPENAI_MODEL is your DEPLOYMENT NAME (not a model id).
VITE_OPENAI_MODEL=your_chat_deployment_name
OPENAI_API_KEY=your_azure_openai_key_here
OPENAI_BASE_URL=https://your-resource.openai.azure.com
OPENAI_API_VERSION=2024-12-01-preview
# On Azure, OPENAI_MODEL is your DEPLOYMENT NAME (not a model id).
OPENAI_MODEL=your_chat_deployment_name
```

When `VITE_OPENAI_BASE_URL` is set, the app uses the Azure OpenAI client; otherwise it uses the standard OpenAI client.
These variables are deliberately **not** prefixed with `VITE_`, so Vite cannot inline them into the browser bundle. The dev/preview server proxies requests at `/api/openai` and attaches the credential server-side.

If you used an earlier version of this app, delete `VITE_OPENAI_API_KEY` from your `.env` and rotate that key — Vite serves every `VITE_`-prefixed variable to client code, so it remains exposed to the browser even though no code reads it now.

When `OPENAI_BASE_URL` is set, the app uses Azure OpenAI conventions; otherwise it uses the standard OpenAI conventions. Set `OPENAI_API_STYLE=openai` to use a custom endpoint with standard OpenAI conventions.

### Development

Expand Down Expand Up @@ -92,16 +96,16 @@ yarn typecheck

## Model compatibility

The chat app uses the model configured in `VITE_OPENAI_MODEL`, defaulting to `gpt-5.4-nano`. When using Azure OpenAI, ensure the deployment named in `VITE_OPENAI_MODEL` targets a chat-completions-compatible model.
The chat app uses the model configured in `OPENAI_MODEL`, defaulting to `gpt-5.4-nano`. When using Azure OpenAI, ensure the deployment named in `OPENAI_MODEL` targets a chat-completions-compatible model.

## Security Notes

⚠️ **Important**: This demo uses `dangerouslyAllowBrowser: true` for the OpenAI client, which exposes your API key in the browser. In a production application, you should:
The API key is held by the Vite dev/preview server and injected into requests there. The browser talks only to the same-origin `/api/openai` proxy with a placeholder credential, so no key is present in the shipped bundle. `dangerouslyAllowBrowser: true` remains set because the OpenAI SDK refuses to run in a browser otherwise, but there is no real credential for it to expose.

⚠️ Two limits to be aware of before deploying this beyond local development:

1. Move OpenAI API calls to a backend server
2. Implement proper authentication
3. Use environment variables on the server side
4. Add rate limiting and other security measures
1. The proxy runs only under `vite dev` and `vite preview`. A static build of `dist/` has no server, so it needs an equivalent proxy (for example a serverless function holding the key) in front of it.
2. The proxy endpoint is an unauthenticated pass-through to your credential. Anyone who can reach it can spend your quota, so add authentication and rate limiting before exposing it beyond `localhost`.

## Contributing

Expand Down
1 change: 1 addition & 0 deletions apps/promptions-chat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"remark-gfm": "^4.0.1"
},
"devDependencies": {
"@promptions/promptions-openai-proxy": "workspace:*",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.3",
Expand Down
45 changes: 23 additions & 22 deletions apps/promptions-chat/src/services/ChatService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import OpenAI, { AzureOpenAI } from "openai";

/**
* The OpenAI SDK requires a non-empty key. The proxy replaces it with the real
* credential, so this literal is all the browser ever sees.
*/
const PROXY_PLACEHOLDER_API_KEY = "injected-by-proxy";

interface ChatMessage {
role: "user" | "assistant" | "system";
content: string;
Expand All @@ -10,31 +16,26 @@ export class ChatService {
private model: string;

constructor() {
// In a real application, you'd want to handle the API key more securely
// For development, you can set VITE_OPENAI_API_KEY in your .env file
const apiKey = import.meta.env.VITE_OPENAI_API_KEY;

if (!apiKey) {
throw new Error(
"OpenAI API key is required. Please set VITE_OPENAI_API_KEY in your environment variables.",
);
}

const baseURL = import.meta.env.VITE_OPENAI_BASE_URL;
// The API key is never available to the browser. Requests go to the
// same-origin proxy path, which injects the real credential
// server-side (see @promptions/promptions-openai-proxy).
const proxyUrl = `${window.location.origin}${import.meta.env.VITE_OPENAI_PROXY_PATH || "/api/openai"}`;
const apiVersion = import.meta.env.VITE_OPENAI_API_VERSION;
this.model = import.meta.env.VITE_OPENAI_MODEL || "gpt-5.4-nano";

this.client = baseURL
? new AzureOpenAI({
apiKey,
endpoint: baseURL,
apiVersion,
dangerouslyAllowBrowser: true, // Only for demo purposes - use a backend in production
})
: new OpenAI({
apiKey,
dangerouslyAllowBrowser: true, // Only for demo purposes - use a backend in production
});
this.client =
import.meta.env.VITE_OPENAI_PROXY_MODE === "azure"
? new AzureOpenAI({
endpoint: proxyUrl,
apiVersion,
apiKey: PROXY_PLACEHOLDER_API_KEY,
dangerouslyAllowBrowser: true,
})
: new OpenAI({
baseURL: `${proxyUrl}/v1`,
apiKey: PROXY_PLACEHOLDER_API_KEY,
dangerouslyAllowBrowser: true,
});
}

async streamChat(
Expand Down
6 changes: 4 additions & 2 deletions apps/promptions-chat/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
/// <reference types="vite/client" />

interface ImportMetaEnv {
readonly VITE_OPENAI_API_KEY: string;
readonly VITE_OPENAI_BASE_URL?: string;
// Injected by @promptions/promptions-openai-proxy. Non-secret values only:
// the API key is read server-side from OPENAI_API_KEY and never exposed here.
readonly VITE_OPENAI_PROXY_PATH: string;
readonly VITE_OPENAI_PROXY_MODE: "azure" | "openai";
readonly VITE_OPENAI_API_VERSION?: string;
readonly VITE_OPENAI_MODEL?: string;
// more env variables...
Expand Down
3 changes: 2 additions & 1 deletion apps/promptions-chat/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { openaiProxy } from "@promptions/promptions-openai-proxy";

// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
plugins: [react(), openaiProxy()],
server: {
port: 3003,
},
Expand Down
25 changes: 19 additions & 6 deletions apps/promptions-image/.env.example
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
# Copy this file to .env

# Required: Set your API key
VITE_OPENAI_API_KEY=your_openai_api_key_here
# These variables are read by the Vite dev/preview server only. They are NOT
# prefixed with VITE_, so Vite cannot inline them into the browser bundle.
#
# MIGRATION: earlier versions of this app used VITE_OPENAI_API_KEY. The dev
# server refuses to start while that variable is set — delete it from your .env
# and rotate the credential. Vite serves every VITE_-prefixed variable to client
# code, so any key left there is exposed to the browser even though no code
# reads it any more.

# Required: your API key. Stays server-side; the browser talks to /api/openai.
OPENAI_API_KEY=your_openai_api_key_here

# Optional: only set for Azure or other custom OpenAI-compatible endpoints.
# Omit for standard OpenAI API usage.
# VITE_OPENAI_BASE_URL=https://your-resource.openai.azure.com
# OPENAI_BASE_URL=https://your-resource.openai.azure.com

# Optional: API version is typically Azure-specific/custom-endpoint specific.
# Required when VITE_OPENAI_BASE_URL points at Azure OpenAI.
# VITE_OPENAI_API_VERSION=2024-12-01-preview
# Required when OPENAI_BASE_URL points at Azure OpenAI.
# OPENAI_API_VERSION=2024-12-01-preview

# Optional: override the chat model used for prompt-related completions (defaults to gpt-5.4-nano).
# The image-generation model is selected in the UI.
# VITE_OPENAI_MODEL=gpt-5.4-nano
# OPENAI_MODEL=gpt-5.4-nano

# Optional: override the image model. On Azure this must be your image
# *deployment* name, which need not match the underlying model ID.
# OPENAI_IMAGE_MODEL=gpt-image-1
3 changes: 2 additions & 1 deletion apps/promptions-image/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@promptions/promptions-image",
"version": "1.0.0",
"type": "module",
"description": "Image generation interface for promptions using OpenAI DALL-E and Fluent UI",
"description": "Image generation interface for promptions using OpenAI gpt-image-1 and Fluent UI",
"license": "MIT",
"scripts": {
"dev": "vite --port 3004",
Expand All @@ -21,6 +21,7 @@
"react-dom": "^18.3.1"
},
"devDependencies": {
"@promptions/promptions-openai-proxy": "workspace:*",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.3",
Expand Down
4 changes: 2 additions & 2 deletions apps/promptions-image/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -222,10 +222,10 @@ function App() {

const images = await imageService.generateImage(
{
kind: "dall-e-3",
kind: "gpt-image-1",
prompt: enhancedPrompt,
size: "1024x1024",
quality: "hd",
quality: "high",
n: 1,
},
{
Expand Down
Loading