Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

gnome-shell-openai

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.

Features

  • 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 parsed code/type/param fields and a retryAfter accessor
  • Gio.Cancellable support on every call; abort() for the whole session
  • Gio.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

Requirements

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.

Installation

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';

Usage

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'});

Cancellation

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 once

Call client.abort() (or hold a cancellable) in your extension's disable() so in-flight requests don't outlive the extension.

Error handling

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
}

API key storage

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.

Custom endpoints

// LocalAI / Ollama / llama.cpp / Azure — anything OpenAI-compatible:
const local = new OpenAIClient({baseUrl: 'http://localhost:8080/v1'});

Project layout

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

Testing

./tests/run.sh        # or: npm test

48 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.

License

MIT — see LICENSE.

About

OpenAI API client library for GNOME Shell extensions (GJS / GObject Introspection / libsoup3 / Gio)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages