Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/dependabot.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
version: 2
updates:

- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
timezone: "Europe/Kyiv"
day: "friday"
time: "18:00"
40 changes: 40 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Build

on:
push:
branches:
- main
pull_request:
branches:
- main

jobs:
build:
strategy:
matrix:
os: [ ubuntu-latest ]
version: [ 20 ]
runs-on: ${{ matrix.os }}
steps:

- name: Check out
uses: actions/checkout@v4

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.version }}
cache: 'npm'
cache-dependency-path: ./package.json

- name: Run Npm:install
run: npm install

- name: Run Npm:format
run: npm run format

- name: Run Npm:lint
run: npm run lint

- name: Run Npm:test
run: npm run test
27 changes: 27 additions & 0 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Publish

on:
workflow_dispatch:

jobs:
publish:
strategy:
matrix:
os: [ ubuntu-latest ]
version: [ 20 ]
runs-on: ${{ matrix.os }}
steps:

- name: Check out
uses: actions/checkout@v4

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.version }}
cache: 'npm'

- name: Publish to npm
run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# OS
Thumbs.db
.DS_Store
*.pdb

# Editors
.vs/
Expand All @@ -11,6 +10,8 @@ Thumbs.db

# Lang: Typescript
node_modules/
tsconfig.tsbuildinfo
package-lock.json

# Output
dist/
Expand Down
37 changes: 37 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "@einstack/glide",
"version": "0.1.0",
"type": "module",
"license": "Apache-2.0",
"author": "EinStack <[email protected]>",
"keywords": ["llm", "ai", "gateway"],
"description": "A minimal Glide client",
"repository": "git+https://github.com/EinStack/glide-ts.git",
"bugs": "https://github.com/EinStack/glide-ts/issues",
"homepage": "https://www.einstack.ai/",
"exports": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"scripts": {
"build": "rimraf ./dist && tsup",
"dev": "rimraf ./dist && tsup --watch",
"test": "node --import tsx --test ./src/*.spec.ts",
"format": "biome format --write .",
"lint": "biome lint .",
"check": "biome check --apply .",
"ci": "biome ci ."
},
"dependencies": {},
"devDependencies": {
"@biomejs/biome": "^1.7.3",
"@types/node": "^20.12.10",
"@types/ws": "^8.5.10",
"esbuild": "^0.20.2",
"rimraf": "^5.0.5",
"tsup": "^8.0.2",
"tsx": "^4.9.3",
"typescript": "^5.4.5"
}
}
30 changes: 30 additions & 0 deletions src/client.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { describe, it } from "node:test";
import { ok } from "node:assert";
import { GlideClient } from "./client";

describe("client", () => {
it("should be constructable without options", () => {
const client = new GlideClient();

ok(client.baseUrl);
ok(client.userAgent);
});

it("should be constructable with options", () => {
const client = new GlideClient({
apiKey: "testing",
userAgent: "Einstack/1.0",
});

ok(client.baseUrl);
ok(client.apiKey);
ok(client.userAgent);
});

it("should be healthy", async () => {
const client = new GlideClient();
const healthy = await client.health();

ok(healthy);
});
});
67 changes: 67 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { ClientConfig, type GlideClientOptions } from "./config";
import { type Language, LanguageService } from "./language";

/**
* A minimal `EinStack` `Glide` client.
*
* @link https://www.einstack.ai/
*/
export class GlideClient {
readonly #config: ClientConfig;
readonly #language: Language;

/**
* Constructs a new `EinStack` `Glide` client.
*
* @param options Client options, override environment variables.
*
* @link https://www.einstack.ai/
*/
constructor(options?: GlideClientOptions) {
this.#config = new ClientConfig(options);
this.#language = new LanguageService(this.#config);
}

/**
* Returns the provided `API key`.
*/
get apiKey(): string | null {
return this.#config.apiKey;
}

/**
* Returns the used base `URL`.
*/
get baseUrl(): URL {
return this.#config.baseUrl;
}

/**
* Returns the used `User-Agent` header value.
*/
get userAgent(): string {
return this.#config.userAgent;
}

/**
* APIs for `/v1/language` endpoints.
*/
get language(): Language {
return this.#language;
}

/**
* Returns `true` if the service is healthy.
*
* `GET /v1/health`
*
* @throws GlideError
*/
async health(): Promise<boolean> {
const response = await this.#config.fetch<{
healthy: boolean;
}>("GET", "/v1/health");

return response.healthy;
}
}
12 changes: 12 additions & 0 deletions src/config.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { describe, it } from "node:test";
import { ok } from "node:assert";
import { type GlideClientOptions, tryEnvironment } from "./config";

describe("config", () => {
it("should return valid options", () => {
const options: GlideClientOptions = tryEnvironment();

ok(options.baseUrl);
ok(options.userAgent);
});
});
125 changes: 125 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { type ErrorResponse, GlideError } from "./error";

/**
* The current version of this client.
*/
export const clientVersion = "0.1.0";

// TODO: Read runtime version.

/**
* The used version of the runtime.
*/
export const runtimeVersion = "16.0";

/**
* Options for {@link GlideClient }.
*
* Overrides environment variables.
*/
export interface GlideClientOptions {
/**
* Attaches an optional `API key`.
*
* Overrides environment variable `GLIDE_API_KEY`.
*/
apiKey?: string | null;

/**
* Overrides the default `base url`: `http://127.0.0.1:9099/`.
* or environment variable `GLIDE_BASE_URL`.
*/
baseUrl?: string | URL;

/**
* Overrides the default `User-Agent` header: `Glide/0.1.0 (TS; Ver. 16.0)`.
* or environment variable `GLIDE_USER_AGENT`.
*/
userAgent?: string;
}

/**
* Attempts to construct {@link GlideClientOptions} from environment variables.
*
* Returns default {@link GlideClientOptions} otherwise.
*/
export function tryEnvironment(): Required<GlideClientOptions> {
const options: Required<GlideClientOptions> = {
apiKey: null,
baseUrl: "http://127.0.0.1:9099/",
userAgent: `Glide/${clientVersion} (TS; Ver. ${runtimeVersion})`,
};

const env = (globalThis as any).process?.env;
if (typeof env !== "object" || env === null) {
return options;
}

if (typeof env.GLIDE_API_KEY === "string") {
options.apiKey = env.GLIDE_API_KEY;
}

if (typeof env.GLIDE_BASE_URL === "string") {
options.baseUrl = env.GLIDE_BASE_URL;
}

if (typeof env.GLIDE_USER_AGENT === "string") {
options.apiKey = env.GLIDE_USER_AGENT;
}

return options;
}

/**
* TODO.
*/
export class ClientConfig {
readonly apiKey: string | null;
readonly baseUrl: URL;
readonly userAgent: string;

/**
* Instantiates a new {@link ClientConfig} with provided options.
*/
constructor(options?: GlideClientOptions) {
const env = tryEnvironment();
this.apiKey = options?.apiKey || env.apiKey;
this.baseUrl = new URL(options?.baseUrl || env.baseUrl);
this.userAgent = options?.userAgent || env.userAgent;
}

/**
* Sends serialized and requests and returns deserialized response.
*
* @throws GlideError
*/
async fetch<T = unknown>(
method: string,
path: string,
data?: unknown,
): Promise<T> {
const input = new URL(path, this.baseUrl);
const headers = new Headers({
Accept: "application/json",
"Content-Type": "application/json",
userAgent: this.userAgent,
});

if (this.apiKey !== null) {
headers.set("Authorization", `Bearer ${this.apiKey}`);
}

const response = await fetch(input, {
body: JSON.stringify(data),
method,
headers,
});

if (response.ok) {
return (await response.json()) as T;
}

const content = (await response.json()) as ErrorResponse;
throw new GlideError(content, response.status);
}
}
Loading