Skip to content

Commit 303f65d

Browse files
examples: Add streaming ChatGPT proxy from FOSDEM 2025 (apple#723)
### Motivation We recently demoed [building a streaming ChatGPT proxy with Swift OpenAPI, at FOSDEM 2025][0]. This PR adds the resulting package for people to refer to as reference material. ### Modifications - Add package reflecting final state of the presentation. - Add a `.env.example` file. - Add covering README with link to presentation, abstract, and usage. - Add VS Code settings for Dev Containers. ### Result Reference project available for people watching the presentation. ### Test Plan CI will build the example. [0]: https://fosdem.org/2025/schedule/event/fosdem-2025-5230-live-coding-a-streaming-chatgpt-proxy-with-swift-openapi-from-scratch-/
1 parent 60a302c commit 303f65d

19 files changed

+27810
-0
lines changed

.licenseignore

+3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
.gitignore
22
.licenseignore
33
.swiftformatignore
4+
.unacceptablelanguageignore
45
.spi.yml
56
.swift-format
67
.github/*
@@ -37,6 +38,8 @@ Examples/HelloWorldiOSClientAppExample/HelloWorldiOSClientApp.*
3738
Examples/HelloWorldiOSClientAppExample/HelloWorldiOSClientApp/Assets.xcassets/*
3839
Examples/HelloWorldiOSClientAppExample/HelloWorldiOSClientApp/Preview*
3940
Examples/**/Generated*
41+
Examples/streaming-chatgpt-proxy/.env.example
42+
Examples/streaming-chatgpt-proxy/players.txt
4043
**/Makefile
4144
**/*.html
4245
.editorconfig

.swiftformatignore

+1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ Tests/OpenAPIGeneratorReferenceTests/Resources
22
Sources/swift-openapi-generator/Documentation.docc
33
Examples/**/Generated/*
44
Examples/**/GeneratedSources/*
5+
Examples/streaming-chatgpt-proxy/**
56
**Package.swift

.unacceptablelanguageignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Examples/streaming-chatgpt-proxy/Sources/ChatGPT/openapi.yaml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"name": "Swift",
3+
"image": "swift:6.0",
4+
"features": {
5+
"ghcr.io/devcontainers/features/common-utils:2": {},
6+
"ghcr.io/devcontainers/features/git:1": {}
7+
},
8+
"runArgs": [
9+
"--cap-add=SYS_PTRACE",
10+
"--security-opt",
11+
"seccomp=unconfined"
12+
],
13+
"customizations": {
14+
"vscode": {
15+
"settings": {
16+
"lldb.library": "/usr/lib/liblldb.so"
17+
},
18+
"extensions": [
19+
"sswg.swift-lang",
20+
"42Crunch.vscode-openapi"
21+
]
22+
}
23+
},
24+
// Use 'forwardPorts' to make a list of ports inside the container available locally.
25+
// "forwardPorts": [],
26+
27+
// Set `remoteUser` to `root` to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
28+
"remoteUser": "root"
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Rename this file to just .env and paste your token below.
2+
OPENAI_TOKEN=PASTE_YOUR_TOKEN_HERE
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.DS_Store
2+
.build/
3+
.build-linux/
4+
.env
5+
/Packages
6+
/*.xcodeproj
7+
xcuserdata/
8+
DerivedData/
9+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
10+
.vscode
11+
/Package.resolved
12+
.ci/
13+
.docc-build/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"editor.lightbulb.enabled": "off",
3+
"explorer.decorations.badges": false,
4+
"explorer.decorations.colors": false,
5+
"files.exclude": {
6+
"**/.build": true,
7+
"**/.build-linux": true,
8+
"**/.gitignore": true,
9+
"**/.swiftpm": true,
10+
"**/.vscode": true,
11+
".env": true,
12+
"*.o": true,
13+
"*.d": true,
14+
"*.swiftdeps": true,
15+
"*.swiftdeps~": true,
16+
},
17+
"outline.problems.colors": false,
18+
"outline.problems.badges": false,
19+
"lldb.library": "/usr/lib/liblldb.so",
20+
"lldb.launch.expressions": "native",
21+
"swift.diagnosticsStyle": "default",
22+
"swift.disableAutoResolve": true,
23+
"swift.sourcekit-lsp.backgroundIndexing": "off",
24+
"swift.sourcekit-lsp.disable": true,
25+
"swift.buildPath": ".build-linux",
26+
"workbench.colorCustomizations": {
27+
"editorError.foreground": "#00000000",
28+
"editorWarning.foreground": "#00000000",
29+
},
30+
"workbench.startupEditor": "none"
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// swift-tools-version: 5.9
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "fosdem-2025-demo",
6+
platforms: [.macOS(.v14)],
7+
dependencies: [
8+
.package(url: "https://github.com/apple/swift-openapi-generator", from: "1.6.0"),
9+
.package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.6.0"),
10+
.package(url: "https://github.com/apple/swift-openapi-urlsession", from: "1.0.2"),
11+
.package(url: "https://github.com/swift-server/swift-openapi-vapor", from: "1.0.1"),
12+
.package(url: "https://github.com/swift-server/swift-openapi-async-http-client", from: "1.1.0"),
13+
],
14+
targets: [
15+
.target(
16+
name: "ChatGPT",
17+
dependencies: [
18+
.product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
19+
],
20+
plugins: [
21+
.plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator"),
22+
]
23+
),
24+
.executableTarget(
25+
name: "ClientCLI",
26+
dependencies: [
27+
.product(name: "OpenAPIURLSession", package: "swift-openapi-urlsession"),
28+
],
29+
plugins: [
30+
.plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator"),
31+
]
32+
),
33+
.executableTarget(
34+
name: "ProxyServer",
35+
dependencies: [
36+
"ChatGPT",
37+
.product(name: "OpenAPIAsyncHTTPClient", package: "swift-openapi-async-http-client"),
38+
.product(name: "OpenAPIVapor", package: "swift-openapi-vapor"),
39+
],
40+
plugins: [
41+
.plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator"),
42+
]
43+
),
44+
]
45+
)
+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Streaming ChatGPT Proxy
2+
3+
An example project using [Swift OpenAPI Generator](https://github.com/apple/swift-openapi-generator).
4+
5+
> **Disclaimer:** This example is deliberately simplified and is intended for illustrative purposes only.
6+
7+
## Overview
8+
9+
A tailored API server, backed by ChatGPT, and client CLI, with end-to-end
10+
streaming.
11+
12+
This package is the reference sources for the demo presented at [FOSDEM 2025:
13+
_Live coding a streaming ChatGPT proxy with Swift OpenAPI—from
14+
scratch!_][fosdem25-swift-openapi]
15+
16+
> 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.
17+
>
18+
> In this session you’ll learn how to:
19+
>
20+
> * Generate a type-safe ChatGPT macOS client and use URLSession OpenAPI transport.
21+
> * Stream LLM responses using Server Sent Events (SSE).
22+
> * Bootstrap a Linux proxy server using the Vapor OpenAPI transport.
23+
> * Use the same generated ChatGPT client within the proxy by switching to the AsyncHTTPClient transport.
24+
> * Efficiently transform responses from SSE to JSON Lines, maintaining end-to-end streaming.
25+
26+
The example provides an API for a fictitious _ChantGPT_ service, which produces
27+
creative chants to sing at basketball games. 🙌 🏀 🙌
28+
29+
## Usage
30+
31+
The upstream calls to ChatGPT require an API token, which is configured using the `OPENAI_TOKEN` environment variable.
32+
Rename `.env.example` to `.env` and replace the placeholder with your token.
33+
34+
Build and run the server using:
35+
36+
```console
37+
% swift run ProxyServer
38+
2025-01-30T09:12:23+0000 notice codes.vapor.application : [Vapor] Server starting on http://127.0.0.1:8080
39+
...
40+
```
41+
42+
Then, from another terminal, run the proxy client using:
43+
44+
```console
45+
% swift run ClientCLI "That team with the Bull logo"
46+
Build of product 'ClientCLI' complete! (7.24s)
47+
🧑‍💼: That one with the bull logo
48+
---
49+
🤖: **"Charge Ahead, Chicago Bulls!"**
50+
51+
(Verse 1)
52+
Red and black, we’re on the prowl,
53+
Chicago Bulls, hear us growl!
54+
From the Windy City, we take the lead,
55+
Charging forward with lightning speed!
56+
57+
(Chorus)
58+
B-U-L-L-S, Bulls! Bulls! Bulls!
59+
We’re the team that never dulls!
60+
Hoops and hustle, heart and soul,
61+
Chicago Bulls, we’re on a roll!
62+
...
63+
```
64+
65+
## Linux development with VS Code Dev Containers
66+
67+
The package also contains configuration for developing with VS Code [Dev
68+
Containers][dev-containers].
69+
70+
If you have the Dev Containers extension installed, use the `Dev Containers: Reopen in Container` command to switch to build and run for Linux.
71+
72+
[fosdem25-swift-openapi]: https://fosdem.org/2025/schedule/event/fosdem-2025-5230-live-coding-a-streaming-chatgpt-proxy-with-swift-openapi-from-scratch-/
73+
[dev-containers]: https://code.visualstudio.com/docs/devcontainers/containers
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftOpenAPIGenerator open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the SwiftOpenAPIGenerator project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
import Foundation
15+
import HTTPTypes
16+
import OpenAPIRuntime
17+
18+
package struct HeaderFieldMiddleware: ClientMiddleware {
19+
var name: HTTPField.Name
20+
var value: String
21+
22+
package static func authTokenFromEnvironment(_ environmentVariable: String) -> Self {
23+
guard let token = ProcessInfo.processInfo.environment[environmentVariable] else {
24+
fatalError("Please set \(environmentVariable) environment variable.")
25+
}
26+
return Self(name: .authorization, value: "Bearer \(token)")
27+
}
28+
29+
package func intercept(
30+
_ request: HTTPRequest,
31+
body: HTTPBody?,
32+
baseURL: URL,
33+
operationID: String,
34+
next: @Sendable (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody?)
35+
) async throws -> (HTTPResponse, HTTPBody?) {
36+
var request = request
37+
request.headerFields[name] = value
38+
return try await next(request, body, baseURL)
39+
}
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
generate: [types, client]
2+
accessModifier: package
3+
filter:
4+
paths:
5+
- /chat/completions
6+
namingStrategy: idiomatic

0 commit comments

Comments
 (0)