diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a7f618..455825e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.3.3] - 2026-06-12 + +### Added + +- 🌉 **OpenAI-compatible gateway.** Expose cptr workspaces through `/v1/models` and `/v1/chat/completions`, so Open WebUI and other OpenAI-compatible clients can use each workspace as a model with the full cptr agent loop. +- 🔑 **Gateway API keys.** New Gateway admin settings tab for creating, copying, listing, and deleting API keys. Keys are stored hashed and newly generated keys are only shown once. + +### Changed + +- 🔄 **Gateway streaming support.** Chat tasks can now stream assistant deltas into an OpenAI-style SSE response while still updating cptr chats and sidebar state. +- 🧭 **Frontend dev proxy.** The Vite dev server now proxies `/v1` gateway requests to the backend during local development. + ## [0.3.2] - 2026-06-12 ### Added @@ -224,4 +236,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 🔐 **Authentication.** Username/password authentication with JWT-based session management. - 🎨 **Admin settings.** Settings UI for managing AI connections and app configuration. - 🐳 **Docker support.** Multi-stage Dockerfile and GitHub Actions workflow for building and publishing to GHCR. -- 📦 **PyPI packaging.** Hatchling-based build with frontend assets bundled into the wheel, published via trusted OIDC publishing. \ No newline at end of file +- 📦 **PyPI packaging.** Hatchling-based build with frontend assets bundled into the wheel, published via trusted OIDC publishing. diff --git a/cptr/app.py b/cptr/app.py index 52bf426..7ee442f 100644 --- a/cptr/app.py +++ b/cptr/app.py @@ -15,6 +15,7 @@ chat_router, events_router, files_router, + gateway_router, git_router, proxy_router, search_router, @@ -94,7 +95,7 @@ async def auth_middleware(request: Request, call_next): or path == "/manifest.json" ): return await call_next(request) - if path.startswith("/_app/") or not path.startswith("/api/"): + if path.startswith("/_app/") or path.startswith("/v1/") or not path.startswith("/api/"): return await call_next(request) # GET /api/files/{id} is public (UUID is unguessable, can't send cookies) if request.method == "GET" and path.startswith("/api/files/"): @@ -218,6 +219,7 @@ async def get_config(): app.include_router(chat_router) app.include_router(events_router) app.include_router(files_router) +app.include_router(gateway_router) app.include_router(git_router) app.include_router(proxy_router) app.include_router(search_router) diff --git a/cptr/frontend/package-lock.json b/cptr/frontend/package-lock.json index 8869438..7037e24 100644 --- a/cptr/frontend/package-lock.json +++ b/cptr/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "frontend", - "version": "0.1.4", + "version": "0.3.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "frontend", - "version": "0.1.4", + "version": "0.3.3", "dependencies": { "@codemirror/lang-cpp": "^6.0.3", "@codemirror/lang-css": "^6.3.1", diff --git a/cptr/frontend/package.json b/cptr/frontend/package.json index 3b44424..29c5352 100644 --- a/cptr/frontend/package.json +++ b/cptr/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "0.3.2", + "version": "0.3.3", "type": "module", "scripts": { "dev": "vite dev", diff --git a/cptr/frontend/src/lib/components/Admin/Gateway.svelte b/cptr/frontend/src/lib/components/Admin/Gateway.svelte new file mode 100644 index 0000000..34afa22 --- /dev/null +++ b/cptr/frontend/src/lib/components/Admin/Gateway.svelte @@ -0,0 +1,203 @@ + + +
+

+ {$t('admin.gateway.title')} +

+

+ {$t('admin.gateway.description')} +

+ + {#if loading} +
+ +
+ {:else} + {#if revealedKey} +
+
+

+ {$t('admin.gateway.newKey')} +

+ +
+
+ + {revealedKey} + +
+

+ {$t('admin.gateway.keyWarning')} +

+
+ {/if} + +

Keys

+
+ e.key === 'Enter' && createKey()} + disabled={creating} + /> + +
+ + {#if keys.length === 0} +
+ +

{$t('admin.gateway.noKeys')}

+
+ {:else} +
+ {#each keys as key (key.id)} +
+
+ + + {key.name} + + + {formatDate(key.created_at)} + +
+ +
+ {/each} +
+ {/if} + +
+

+ {$t('admin.gateway.howToConnect')} +

+
+
+ Base URL: + {`${typeof window !== 'undefined' ? window.location.origin : ''}/v1`} +
+
+ API Key: + sk-cptr-... +
+
+ Header: + X-OpenWebUI-Chat-Id: {'{{CHAT_ID}}'} +
+
+ Also accepts: + X-Chat-Id +
+
+
+ {/if} +
diff --git a/cptr/frontend/src/lib/components/Icon.svelte b/cptr/frontend/src/lib/components/Icon.svelte index 9d5f231..c9261a6 100644 --- a/cptr/frontend/src/lib/components/Icon.svelte +++ b/cptr/frontend/src/lib/components/Icon.svelte @@ -235,6 +235,10 @@ {:else if name === 'shield'} + {:else if name === 'gateway'} + + + {:else if name === 'quote'} @@ -306,7 +310,9 @@ {:else if name === 'play'} - + {:else if name === 'clock'} @@ -318,28 +324,46 @@ {:else if name === 'discord'} - - - - + + + + {:else if name === 'slack'} {:else if name === 'whatsapp'} - - + + {:else if name === 'signal'} - + {:else if name === 'browser'} - + {:else if name === 'microphone'} - + diff --git a/cptr/frontend/src/lib/components/SettingsModal.svelte b/cptr/frontend/src/lib/components/SettingsModal.svelte index 6b96a6a..936a54b 100644 --- a/cptr/frontend/src/lib/components/SettingsModal.svelte +++ b/cptr/frontend/src/lib/components/SettingsModal.svelte @@ -10,6 +10,7 @@ import Connections from './Admin/Connections.svelte'; import Models from './Admin/Models.svelte'; import Messaging from './Admin/Messaging.svelte'; + import Gateway from './Admin/Gateway.svelte'; import AudioSettings from './Admin/AudioSettings.svelte'; import AdminSettings from './Admin/Settings.svelte'; import { session } from '$lib/session'; @@ -25,6 +26,7 @@ | 'connections' | 'models' | 'messaging' + | 'gateway' | 'audio' | 'admin_settings'; @@ -51,6 +53,7 @@ { id: 'connections', label: $t('admin.connections'), icon: 'plug' }, { id: 'models', label: $t('admin.models'), icon: 'cube' }, { id: 'messaging', label: $t('admin.messaging'), icon: 'chat-bubble' }, + { id: 'gateway', label: $t('admin.gateway.tab'), icon: 'gateway' }, { id: 'audio', label: 'Audio', icon: 'microphone' }, { id: 'browser', label: 'Browser', icon: 'browser' }, { id: 'admin_settings', label: $t('settings.configuration'), icon: 'shield' } @@ -89,7 +92,9 @@ {#if isAdmin} - + {#each adminTabs as tab}