Skip to content
Merged
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
42 changes: 42 additions & 0 deletions packages/ai/cheaperinference/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Cheaper Inference

Provides the Cheaper Inference AI provider adapter for sh1pt agent and model workflows.

## What it does

- Connects an AI/model provider to sh1pt.
- Normalizes provider configuration for agent and automation workflows.
- Includes setup guidance for required credentials or provider configuration.

## Package

- Name: `@profullstack/sh1pt-ai-cheaperinference`
- Path: `packages/ai/cheaperinference`
- Adapter ID: `ai-cheaperinference`
- Homepage: https://sh1pt.com

## Scripts

- `build`: `tsc -p tsconfig.json`
- `prepublishOnly`: `pnpm build`
- `typecheck`: `tsc -p tsconfig.json --noEmit`

## Usage

```bash
pnpm add @profullstack/sh1pt-ai-cheaperinference
```

## Development

```bash
pnpm --filter @profullstack/sh1pt-ai-cheaperinference typecheck
```

Run tests from the repository root when this module includes a test file:

```bash
pnpm vitest run packages/ai/cheaperinference/src/index.test.ts
```

<!-- Generated by scripts/gen-module-readmes.mjs -->
38 changes: 38 additions & 0 deletions packages/ai/cheaperinference/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "@profullstack/sh1pt-ai-cheaperinference",
"version": "0.1.15",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"build": "tsc -p tsconfig.json",
"typecheck": "tsc -p tsconfig.json --noEmit",
"prepublishOnly": "pnpm build"
},
"dependencies": {
"@profullstack/sh1pt-core": "workspace:*"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/profullstack/sh1pt.git",
"directory": "packages/ai/cheaperinference"
},
"homepage": "https://sh1pt.com",
"bugs": "https://github.com/profullstack/sh1pt/issues",
"files": [
"dist"
],
"publishConfig": {
"access": "public",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
}
}
}
121 changes: 121 additions & 0 deletions packages/ai/cheaperinference/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { smokeTest } from '@profullstack/sh1pt-core/testing';
import { afterEach, describe, expect, it, vi } from 'vitest';
import adapter from './index.js';

smokeTest(adapter, { idPrefix: 'ai' });

const ctx = (secrets: Record<string, string> = { CHEAPERINFERENCE_API_KEY: 'test-key' }, dryRun = false) => ({
secret: (key: string) => secrets[key],
log: () => {},
dryRun,
});

describe('Cheaper Inference OpenAI-compatible generation', () => {
afterEach(() => {
vi.unstubAllGlobals();
});

it('short-circuits dry-run before network calls', async () => {
const fetchMock = vi.fn();
vi.stubGlobal('fetch', fetchMock);

const result = await adapter.generate(ctx({ CHEAPERINFERENCE_API_KEY: 'test-key' }, true), 'hello', {}, {});

expect(result).toEqual({ text: '[dry-run]', model: 'gpt-5.4-mini' });
expect(fetchMock).not.toHaveBeenCalled();
});

it('posts chat completions requests and maps usage tokens', async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
choices: [{ message: { content: 'hi from cheaper inference' } }],
model: 'claude-sonnet-5',
usage: { prompt_tokens: 7, completion_tokens: 3 },
}),
});
vi.stubGlobal('fetch', fetchMock);

const result = await adapter.generate(ctx(), 'hello', {
model: 'claude-sonnet-5',
system: 'be brief',
maxTokens: 20,
temperature: 0.2,
extra: { top_p: 0.9 },
}, {});

expect(fetchMock).toHaveBeenCalledOnce();
const call = fetchMock.mock.calls[0];
expect(call).toBeDefined();
const [url, request] = call!;
expect(url).toBe('https://api.cheaperinference.com/v1/chat/completions');
expect(request.headers.authorization).toBe('Bearer test-key');
expect(JSON.parse(request.body)).toEqual({
model: 'claude-sonnet-5',
messages: [
{ role: 'system', content: 'be brief' },
{ role: 'user', content: 'hello' },
],
max_tokens: 20,
temperature: 0.2,
top_p: 0.9,
});
expect(result).toEqual({
text: 'hi from cheaper inference',
model: 'claude-sonnet-5',
inputTokens: 7,
outputTokens: 3,
});
});

it('normalizes configured base URLs with trailing slashes', async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ choices: [{ message: { content: 'ok' } }], model: 'gpt-5.4-mini' }),
});
vi.stubGlobal('fetch', fetchMock);

await adapter.generate(ctx(), 'hello', {}, { baseUrl: 'https://proxy.example.com/' });

const [url] = fetchMock.mock.calls[0]!;
expect(url).toBe('https://proxy.example.com/v1/chat/completions');
});

it.each([
['missing scheme', 'proxy.example.com'],
['unsupported scheme', 'ftp://proxy.example.com'],
['credentials', 'https://user:pass@proxy.example.com'],
['query string', 'https://proxy.example.com?debug=true'],
['fragment', 'https://proxy.example.com#v1'],
])('rejects unclean configured base URLs: %s', async (_case, baseUrl) => {
const fetchMock = vi.fn();
vi.stubGlobal('fetch', fetchMock);

await expect(adapter.generate(ctx(), 'hello', {}, { baseUrl })).rejects.toThrow(
/Cheaper Inference baseUrl/,
);
expect(fetchMock).not.toHaveBeenCalled();
});

it('includes status and redacted response body excerpt on errors', async () => {
const apiKey = 'test-key-crossing-truncation-boundary';
const prefix = 'x'.repeat(190);
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
ok: false,
status: 429,
text: async () => `${prefix}${apiKey} rate limited`,
}));

let error: unknown;
try {
await adapter.generate(ctx({ CHEAPERINFERENCE_API_KEY: apiKey }), 'hello', {}, {});
} catch (exc) {
error = exc;
}
expect(error).toBeInstanceOf(Error);
expect((error as Error).message).toContain('Cheaper Inference 429:');
expect((error as Error).message).toContain('[redacted]');
expect((error as Error).message).not.toContain(apiKey);
expect((error as Error).message).not.toContain(apiKey.slice(0, 10));
});
});
94 changes: 94 additions & 0 deletions packages/ai/cheaperinference/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { defineAi, tokenSetup } from '@profullstack/sh1pt-core';

interface Config {
baseUrl?: string;
}

const DEFAULT_BASE = 'https://api.cheaperinference.com';

function chatCompletionsUrl(baseUrl?: string): string {
return `${cleanBaseUrl(baseUrl ?? DEFAULT_BASE)}/v1/chat/completions`;
}

function cleanBaseUrl(value: string): string {
let url: URL;
try {
url = new URL(value);
} catch {
throw new Error('Cheaper Inference baseUrl must be a valid URL');
}
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
throw new Error('Cheaper Inference baseUrl must use http or https');
}
if (url.username || url.password || url.search || url.hash) {
throw new Error('Cheaper Inference baseUrl must be a clean API base without credentials, query, or hash');
}
return url.toString().replace(/\/+$/, '');
}

function redact(value: string, apiKey: string): string {
return apiKey ? value.split(apiKey).join('[redacted]') : value;
}

export default defineAi<Config>({
id: 'ai-cheaperinference',
label: 'Cheaper Inference',
defaultModel: 'gpt-5.4-mini',
models: [
'gpt-5.4-mini',
'gpt-5.4',
'claude-sonnet-5',
'deepseek-v4-flash',
'glm-5.3',
],

async generate(ctx, prompt, opts, config) {
const apiKey = ctx.secret('CHEAPERINFERENCE_API_KEY');
if (!apiKey) throw new Error('CHEAPERINFERENCE_API_KEY not in vault');
const model = opts.model ?? 'gpt-5.4-mini';
ctx.log(`cheaperinference · model=${model} · ${prompt.length} chars in`);
if (ctx.dryRun) return { text: '[dry-run]', model };

const messages: Array<{ role: string; content: string }> = [];
if (opts.system) messages.push({ role: 'system', content: opts.system });
messages.push({ role: 'user', content: prompt });

const res = await fetch(chatCompletionsUrl(config.baseUrl), {
method: 'POST',
headers: {
authorization: `Bearer ${apiKey}`,
'content-type': 'application/json',
},
body: JSON.stringify({
model,
messages,
...(opts.maxTokens !== undefined ? { max_tokens: opts.maxTokens } : {}),
...(opts.temperature !== undefined ? { temperature: opts.temperature } : {}),
...opts.extra,
}),
});
if (!res.ok) throw new Error(`Cheaper Inference ${res.status}: ${redact(await res.text(), apiKey).slice(0, 200)}`);
const data = (await res.json()) as {
choices: Array<{ message?: { content?: string } }>;
model: string;
usage?: { prompt_tokens?: number; completion_tokens?: number };
};
return {
text: data.choices[0]?.message?.content ?? '',
model: data.model,
inputTokens: data.usage?.prompt_tokens,
outputTokens: data.usage?.completion_tokens,
};
},

setup: tokenSetup<Config>({
secretKey: 'CHEAPERINFERENCE_API_KEY',
label: 'Cheaper Inference',
vendorDocUrl: 'https://cheaperinference.com/docs',
steps: [
'Sign in at https://cheaperinference.com and create an API key',
'Copy the key — usually shown once',
'Paste below; sh1pt encrypts it in the vault',
],
}),
});
10 changes: 10 additions & 0 deletions packages/ai/cheaperinference/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": [
"src/**/*"
]
}
2 changes: 1 addition & 1 deletion packages/cli/src/adapter-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const CATEGORIES: readonly AdapterCategory[] = [
adapters: [
'ai21', 'aionlabs', 'akashml', 'alibaba-cloud', 'amazon-bedrock',
'arcee', 'atlascloud', 'azure', 'baidu', 'baseten', 'cerebras',
'chutes', 'clarifai', 'claude', 'cloudflare', 'cohere', 'deepinfra',
'cheaperinference', 'chutes', 'clarifai', 'claude', 'cloudflare', 'cohere', 'deepinfra',
'deepseek', 'featherless', 'fireworks', 'friendli', 'gemini',
'gmicloud', 'google-vertex', 'groq', 'inception', 'inceptron',
'infermatic', 'inflection', 'ionet', 'kimi', 'liquid', 'litellm', 'mancer',
Expand Down
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading