An OpenAI API client library for GNOME Shell extensions and other GJS applications, built on libsoup3 and Gio via GObject Introspection. Fully asynchronous on the GLib main loop — the Shell UI never blocks.
- Chat completions (
POST /v1/chat/completions), streaming and non-streaming - Incremental Server-Sent Events parser (
data:/event:/id:/[DONE]) - Models (
list,retrieve), embeddings, moderations, image generation - OpenAI SDK-style error hierarchy (
AuthenticationError,RateLimitError,NotFoundError, …) with parsedcode/type/paramfields and aretryAfteraccessor Gio.Cancellablesupport on every call;abort()for the whole sessionGio.Settings/ environment-variable API key resolution helpers- Works with any OpenAI-compatible endpoint via
baseUrl(Azure OpenAI, LocalAI, Ollama, llama.cpp, vLLM, …) - System proxy, TLS, and keep-alive handled by libsoup3 — same stack the rest of the desktop uses
- Zero dependencies, pure ES modules
| Component | Version |
|---|---|
| GNOME Shell | ≥ 45 (ES-module extensions) |
| gjs | ≥ 1.72 |
| libsoup | 3.0 (gi://Soup?version=3.0) |
| GLib/Gio | 2.7x |
For GNOME ≤ 44 extensions (legacy imports.* / var style), the code can be
adapted mechanically, but this library targets the modern module system only.
GJS extensions have no package manager — vendor the sources:
mkdir -p ~/.local/share/gnome-shell/extensions/you@you/vendor/openai
cp src/*.js ~/.local/share/gnome-shell/extensions/you@you/vendor/openai/Then import relative to your extension.js:
import {OpenAIClient} from './vendor/openai/client.js';import {OpenAIClient} from './vendor/openai/client.js';
const client = new OpenAIClient({apiKey: 'sk-…'});
// Non-streaming chat completion
const res = await client.chatCompletion({
model: 'gpt-4o-mini',
messages: [
{role: 'system', content: 'You are terse.'},
{role: 'user', content: 'Hello!'}, // plain strings work too
],
temperature: 0.3,
});
print(res.choices[0].message.content);
// Streaming — an async generator of ChatCompletionChunk objects
for await (const chunk of client.streamChatCompletion({
model: 'gpt-4o-mini',
messages: ['Write a haiku about top bars.'],
})) {
const delta = chunk.choices[0]?.delta?.content;
if (delta)
print(delta);
}
// Or just the text:
const text = await client.streamChatCompletionText(
{model: 'gpt-4o-mini', messages: ['Say hi']},
delta => {/* update a St.Label incrementally */});
// Other endpoints
const models = await client.listModels();
const embed = await client.createEmbedding({model: 'text-embedding-3-small', input: 'hello'});
const mod = await client.createModeration({input: 'potentially unsafe text'});
const img = await client.createImage({prompt: 'a cat', n: 1, size: '512x512'});Every method takes an optional Gio.Cancellable:
import Gio from 'gi://Gio';
const cancellable = new Gio.Cancellable();
const promise = client.chatCompletion(params, cancellable);
cancellable.cancel(); // rejects with GLib.Error CANCELLED
client.abort(); // or abort everything on the session at onceCall client.abort() (or hold a cancellable) in your extension's disable()
so in-flight requests don't outlive the extension.
import {RateLimitError, AuthenticationError, APIError} from './vendor/openai/errors.js';
try {
await client.chatCompletion(params);
} catch (e) {
if (e instanceof RateLimitError)
log(`rate limited; retry after ${e.retryAfter}s`);
else if (e instanceof AuthenticationError)
log('bad API key');
else if (e instanceof APIError)
log(`HTTP ${e.status}: ${e.message} (${e.type}/${e.code})`);
else
throw e; // cancelled errors are plain GLib.Errors
}src/settings.js resolves credentials from Gio.Settings → environment →
fallback, in that order:
import {resolveClientConfig} from './vendor/openai/settings.js';
// inside Extension.enable():
const settings = this.getSettings(); // schema with 'api-key' & 'base-url' keys
const cfg = resolveClientConfig({settings});
const client = new OpenAIClient(cfg);For stronger guarantees than a plaintext gsettings key, store the secret with
libsecret (gi://Secret) and pass the retrieved value as apiKey. Keep in
mind that any process running as the user can read Secret Service items; a
dedicated org-scoped key + usage limits is the pragmatic choice.
// LocalAI / Ollama / llama.cpp / Azure — anything OpenAI-compatible:
const local = new OpenAIClient({baseUrl: 'http://localhost:8080/v1'});src/
client.js — OpenAIClient: async request/stream methods over Soup.Session
sse.js — incremental SSE parser (pure JS, no GI)
params.js — request body builders + validation (pure JS)
errors.js — error hierarchy + status→class mapping (pure JS)
settings.js — Gio.Settings / env-var config resolution
tests/
run.sh — compiles the test gschema and runs the suite
run_all.js — spawns the mock server, runs every test module
harness.js — tiny async test framework
mock_server.py — stdlib-only fake OpenAI API
examples/
demo-extension/ — minimal GNOME 45+ extension using the library
./tests/run.sh # or: npm test48 tests: unit coverage for the SSE parser, parameter builders, and error mapping; GSettings integration (schema compiled on the fly); and live HTTP integration tests against the bundled mock server — non-streaming + streaming chat, models, embeddings, moderations, images, 401/404/429/500 error mapping, cancellation, and connection failure.
CI runs the same suite on ubuntu-latest with gjs + gir1.2-soup-3.0.
MIT — see LICENSE.