Skip to content

Commit

Permalink
examples: Add streaming ChatGPT proxy from FOSDEM 2025
Browse files Browse the repository at this point in the history
  • Loading branch information
simonjbeaumont authored and Si Beaumont committed Feb 1, 2025
1 parent 60a302c commit 4709d50
Show file tree
Hide file tree
Showing 19 changed files with 27,810 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .licenseignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.gitignore
.licenseignore
.swiftformatignore
.unacceptablelanguageignore
.spi.yml
.swift-format
.github/*
Expand Down Expand Up @@ -37,6 +38,8 @@ Examples/HelloWorldiOSClientAppExample/HelloWorldiOSClientApp.*
Examples/HelloWorldiOSClientAppExample/HelloWorldiOSClientApp/Assets.xcassets/*
Examples/HelloWorldiOSClientAppExample/HelloWorldiOSClientApp/Preview*
Examples/**/Generated*
Examples/streaming-chatgpt-proxy/.env.example
Examples/streaming-chatgpt-proxy/players.txt
**/Makefile
**/*.html
.editorconfig
1 change: 1 addition & 0 deletions .swiftformatignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ Tests/OpenAPIGeneratorReferenceTests/Resources
Sources/swift-openapi-generator/Documentation.docc
Examples/**/Generated/*
Examples/**/GeneratedSources/*
Examples/streaming-chatgpt-proxy/**
**Package.swift
1 change: 1 addition & 0 deletions .unacceptablelanguageignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Examples/streaming-chatgpt-proxy/Sources/ChatGPT/openapi.yaml
29 changes: 29 additions & 0 deletions Examples/streaming-chatgpt-proxy/.devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "Swift",
"image": "swift:6.0",
"features": {
"ghcr.io/devcontainers/features/common-utils:2": {},
"ghcr.io/devcontainers/features/git:1": {}
},
"runArgs": [
"--cap-add=SYS_PTRACE",
"--security-opt",
"seccomp=unconfined"
],
"customizations": {
"vscode": {
"settings": {
"lldb.library": "/usr/lib/liblldb.so"
},
"extensions": [
"sswg.swift-lang",
"42Crunch.vscode-openapi"
]
}
},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],

// Set `remoteUser` to `root` to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "root"
}
2 changes: 2 additions & 0 deletions Examples/streaming-chatgpt-proxy/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Rename this file to just .env and paste your token below.
OPENAI_TOKEN=PASTE_YOUR_TOKEN_HERE
13 changes: 13 additions & 0 deletions Examples/streaming-chatgpt-proxy/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.DS_Store
.build/
.build-linux/
.env
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.vscode
/Package.resolved
.ci/
.docc-build/
31 changes: 31 additions & 0 deletions Examples/streaming-chatgpt-proxy/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"editor.lightbulb.enabled": "off",
"explorer.decorations.badges": false,
"explorer.decorations.colors": false,
"files.exclude": {
"**/.build": true,
"**/.build-linux": true,
"**/.gitignore": true,
"**/.swiftpm": true,
"**/.vscode": true,
".env": true,
"*.o": true,
"*.d": true,
"*.swiftdeps": true,
"*.swiftdeps~": true,
},
"outline.problems.colors": false,
"outline.problems.badges": false,
"lldb.library": "/usr/lib/liblldb.so",
"lldb.launch.expressions": "native",
"swift.diagnosticsStyle": "default",
"swift.disableAutoResolve": true,
"swift.sourcekit-lsp.backgroundIndexing": "off",
"swift.sourcekit-lsp.disable": true,
"swift.buildPath": ".build-linux",
"workbench.colorCustomizations": {
"editorError.foreground": "#00000000",
"editorWarning.foreground": "#00000000",
},
"workbench.startupEditor": "none"
}
45 changes: 45 additions & 0 deletions Examples/streaming-chatgpt-proxy/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// swift-tools-version: 5.9
import PackageDescription

let package = Package(
name: "fosdem-2025-demo",
platforms: [.macOS(.v14)],
dependencies: [
.package(url: "https://github.com/apple/swift-openapi-generator", from: "1.6.0"),
.package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.6.0"),
.package(url: "https://github.com/apple/swift-openapi-urlsession", from: "1.0.2"),
.package(url: "https://github.com/swift-server/swift-openapi-vapor", from: "1.0.1"),
.package(url: "https://github.com/swift-server/swift-openapi-async-http-client", from: "1.1.0"),
],
targets: [
.target(
name: "ChatGPT",
dependencies: [
.product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
],
plugins: [
.plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator"),
]
),
.executableTarget(
name: "ClientCLI",
dependencies: [
.product(name: "OpenAPIURLSession", package: "swift-openapi-urlsession"),
],
plugins: [
.plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator"),
]
),
.executableTarget(
name: "ProxyServer",
dependencies: [
"ChatGPT",
.product(name: "OpenAPIAsyncHTTPClient", package: "swift-openapi-async-http-client"),
.product(name: "OpenAPIVapor", package: "swift-openapi-vapor"),
],
plugins: [
.plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator"),
]
),
]
)
73 changes: 73 additions & 0 deletions Examples/streaming-chatgpt-proxy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Streaming ChatGPT Proxy

An example project using [Swift OpenAPI Generator](https://github.com/apple/swift-openapi-generator).

> **Disclaimer:** This example is deliberately simplified and is intended for illustrative purposes only.
## Overview

A tailored API server, backed by ChatGPT, and client CLI, with end-to-end
streaming.

This package is the reference sources for the demo presented at [FOSDEM 2025:
_Live coding a streaming ChatGPT proxy with Swift OpenAPI—from
scratch!_][fosdem25-swift-openapi]

> Join us as we build a ChatGPT client, from scratch, using Swift OpenAPI Generator. We’ll take advantage of Swift OpenAPI’s pluggable HTTP transports to reuse the same generated client to make upstream calls from a Linux server, providing end-to-end streaming, backed by async sequences, without buffering upstream responses.
>
> In this session you’ll learn how to:
>
> * Generate a type-safe ChatGPT macOS client and use URLSession OpenAPI transport.
> * Stream LLM responses using Server Sent Events (SSE).
> * Bootstrap a Linux proxy server using the Vapor OpenAPI transport.
> * Use the same generated ChatGPT client within the proxy by switching to the AsyncHTTPClient transport.
> * Efficiently transform responses from SSE to JSON Lines, maintaining end-to-end streaming.
The example provides an API for a fictitious _ChantGPT_ service, which produces
creative chants to sing at basketball games. 🙌 🏀 🙌

## Usage

The upstream calls to ChatGPT require an API token, which is configured using the `OPENAI_TOKEN` environment variable.
Rename `.env.example` to `.env` and replace the placeholder with your token.

Build and run the server using:

```console
% swift run ProxyServer
2025-01-30T09:12:23+0000 notice codes.vapor.application : [Vapor] Server starting on http://127.0.0.1:8080
...
```

Then, from another terminal, run the proxy client using:

```console
% swift run ClientCLI "That team with the Bull logo"
Build of product 'ClientCLI' complete! (7.24s)
🧑‍💼: That one with the bull logo
---
🤖: **"Charge Ahead, Chicago Bulls!"**

(Verse 1)
Red and black, we’re on the prowl,
Chicago Bulls, hear us growl!
From the Windy City, we take the lead,
Charging forward with lightning speed!

(Chorus)
B-U-L-L-S, Bulls! Bulls! Bulls!
We’re the team that never dulls!
Hoops and hustle, heart and soul,
Chicago Bulls, we’re on a roll!
...
```

## Linux development with VS Code Dev Containers

The package also contains configuration for developing with VS Code [Dev
Containers][dev-containers].

If you have the Dev Containers extension installed, use the `Dev Containers: Reopen in Container` command to switch to build and run for Linux.

[fosdem25-swift-openapi]: https://fosdem.org/2025/schedule/event/fosdem-2025-5230-live-coding-a-streaming-chatgpt-proxy-with-swift-openapi-from-scratch-/
[dev-containers]: https://code.visualstudio.com/docs/devcontainers/containers
40 changes: 40 additions & 0 deletions Examples/streaming-chatgpt-proxy/Sources/ChatGPT/Middleware.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftOpenAPIGenerator open source project
//
// Copyright (c) 2025 Apple Inc. and the SwiftOpenAPIGenerator project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import Foundation
import HTTPTypes
import OpenAPIRuntime

package struct HeaderFieldMiddleware: ClientMiddleware {
var name: HTTPField.Name
var value: String

package static func authTokenFromEnvironment(_ environmentVariable: String) -> Self {
guard let token = ProcessInfo.processInfo.environment[environmentVariable] else {
fatalError("Please set \(environmentVariable) environment variable.")
}
return Self(name: .authorization, value: "Bearer \(token)")
}

package func intercept(
_ request: HTTPRequest,
body: HTTPBody?,
baseURL: URL,
operationID: String,
next: @Sendable (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody?)
) async throws -> (HTTPResponse, HTTPBody?) {
var request = request
request.headerFields[name] = value
return try await next(request, body, baseURL)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
generate: [types, client]
accessModifier: package
filter:
paths:
- /chat/completions
namingStrategy: idiomatic
Loading

0 comments on commit 4709d50

Please sign in to comment.