@aisa-one/cli 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +134 -1
- package/README.md +249 -103
- package/dist/api.d.ts +46 -1
- package/dist/api.js +77 -8
- package/dist/cache.d.ts +18 -0
- package/dist/cache.js +65 -0
- package/dist/catalog.d.ts +90 -0
- package/dist/catalog.js +101 -0
- package/dist/commands/account.js +3 -1
- package/dist/commands/api.d.ts +18 -4
- package/dist/commands/api.js +357 -24
- package/dist/commands/cacheCmd.d.ts +2 -0
- package/dist/commands/cacheCmd.js +10 -0
- package/dist/commands/completionCmd.d.ts +7 -0
- package/dist/commands/completionCmd.js +37 -0
- package/dist/commands/configCmd.js +19 -1
- package/dist/commands/finance.d.ts +4 -0
- package/dist/commands/finance.js +212 -29
- package/dist/commands/models.js +6 -0
- package/dist/commands/run.d.ts +21 -0
- package/dist/commands/run.js +113 -6
- package/dist/commands/search.js +8 -3
- package/dist/commands/skills.d.ts +6 -2
- package/dist/commands/skills.js +189 -134
- package/dist/commands/twitter.js +2 -3
- package/dist/commands/video.d.ts +40 -0
- package/dist/commands/video.js +272 -55
- package/dist/completion.d.ts +26 -0
- package/dist/completion.js +293 -0
- package/dist/constants.d.ts +55 -5
- package/dist/constants.js +136 -11
- package/dist/index.js +84 -22
- package/dist/skills-registry.d.ts +64 -0
- package/dist/skills-registry.js +219 -0
- package/dist/types.d.ts +24 -0
- package/dist/utils/file.d.ts +5 -1
- package/dist/utils/file.js +5 -1
- package/dist/utils/pool.d.ts +8 -0
- package/dist/utils/pool.js +27 -0
- package/package.json +9 -3
- package/tests/account.test.ts +0 -89
- package/tests/streaming.test.ts +0 -73
package/dist/api.d.ts
CHANGED
|
@@ -1,4 +1,30 @@
|
|
|
1
|
-
import type { ApiResponse } from "./types.js";
|
|
1
|
+
import type { ApiResponse, CostInfo } from "./types.js";
|
|
2
|
+
export interface Bases {
|
|
3
|
+
/** LLM gateway: chat, models, credits, video. */
|
|
4
|
+
llm: string;
|
|
5
|
+
/** Integration APIs: every provider slug. */
|
|
6
|
+
domain: string;
|
|
7
|
+
/** Unauthenticated catalog and pricing endpoints. */
|
|
8
|
+
info: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Derive every base path from one configured root.
|
|
12
|
+
*
|
|
13
|
+
* `baseUrl` has shipped with a default of `https://api.aisa.one/v1`, so it is
|
|
14
|
+
* already persisted with the `/v1` suffix in every existing user's config —
|
|
15
|
+
* changing the schema default would not rewrite it. Strip whichever suffix is
|
|
16
|
+
* present and rebuild, so both the historical value and a bare root work.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveBases(): Bases;
|
|
19
|
+
/**
|
|
20
|
+
* Read the gateway's billing headers off a response.
|
|
21
|
+
*
|
|
22
|
+
* Returns undefined when none are present — which is the common case, since
|
|
23
|
+
* only metered integration routes report an amount. Values are passed through
|
|
24
|
+
* as strings: they are fixed-point decimals produced server-side, and parsing
|
|
25
|
+
* them into floats would only lose precision on the way to being printed.
|
|
26
|
+
*/
|
|
27
|
+
export declare function parseCost(res: Response): CostInfo | undefined;
|
|
2
28
|
export interface RequestOptions {
|
|
3
29
|
method?: "GET" | "POST" | "PUT" | "DELETE";
|
|
4
30
|
query?: Record<string, string>;
|
|
@@ -10,3 +36,22 @@ export interface RequestOptions {
|
|
|
10
36
|
}
|
|
11
37
|
export declare function apiRequest<T = unknown>(apiKey: string, endpoint: string, options?: RequestOptions): Promise<ApiResponse<T>>;
|
|
12
38
|
export declare function apiRequestRaw(apiKey: string, endpoint: string, options?: RequestOptions): Promise<Response>;
|
|
39
|
+
/**
|
|
40
|
+
* Cache namespace for data served by the configured gateway. Catalog, model
|
|
41
|
+
* and coin-list caches must be keyed by the host they came from: a fixed key
|
|
42
|
+
* would keep serving one server's catalog for hours after `config set baseUrl`
|
|
43
|
+
* switches to another.
|
|
44
|
+
*/
|
|
45
|
+
export declare function cacheScope(): string;
|
|
46
|
+
/**
|
|
47
|
+
* Request an unauthenticated endpoint under /info or /pricing. Browsing the
|
|
48
|
+
* catalog does not require an API key, and demanding one would turn discovery
|
|
49
|
+
* into a signup wall.
|
|
50
|
+
*/
|
|
51
|
+
export declare function publicRequest<T = unknown>(path: string, options?: {
|
|
52
|
+
etag?: string;
|
|
53
|
+
}): Promise<{
|
|
54
|
+
status: number;
|
|
55
|
+
data?: T;
|
|
56
|
+
etag?: string;
|
|
57
|
+
}>;
|
package/dist/api.js
CHANGED
|
@@ -1,12 +1,49 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BASE_URL } from "./constants.js";
|
|
2
2
|
import { getConfig } from "./config.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Derive every base path from one configured root.
|
|
5
|
+
*
|
|
6
|
+
* `baseUrl` has shipped with a default of `https://api.aisa.one/v1`, so it is
|
|
7
|
+
* already persisted with the `/v1` suffix in every existing user's config —
|
|
8
|
+
* changing the schema default would not rewrite it. Strip whichever suffix is
|
|
9
|
+
* present and rebuild, so both the historical value and a bare root work.
|
|
10
|
+
*/
|
|
11
|
+
export function resolveBases() {
|
|
12
|
+
const configured = getConfig("baseUrl") || BASE_URL;
|
|
13
|
+
const root = configured
|
|
14
|
+
.trim()
|
|
15
|
+
.replace(/\/+$/, "")
|
|
16
|
+
.replace(/\/apis\/v1$/, "")
|
|
17
|
+
.replace(/\/v1$/, "")
|
|
18
|
+
.replace(/\/+$/, "");
|
|
19
|
+
return { llm: `${root}/v1`, domain: `${root}/apis/v1`, info: root };
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Read the gateway's billing headers off a response.
|
|
23
|
+
*
|
|
24
|
+
* Returns undefined when none are present — which is the common case, since
|
|
25
|
+
* only metered integration routes report an amount. Values are passed through
|
|
26
|
+
* as strings: they are fixed-point decimals produced server-side, and parsing
|
|
27
|
+
* them into floats would only lose precision on the way to being printed.
|
|
28
|
+
*/
|
|
29
|
+
export function parseCost(res) {
|
|
30
|
+
const cost = {
|
|
31
|
+
priceUsd: res.headers.get("x-aisa-price-usd") || undefined,
|
|
32
|
+
pricingStrategy: res.headers.get("x-aisa-pricing-strategy") || undefined,
|
|
33
|
+
pricingVersion: res.headers.get("x-aisa-pricing-version") || undefined,
|
|
34
|
+
creditModel: res.headers.get("x-aisa-credit-model") || undefined,
|
|
35
|
+
estimatedCredits: res.headers.get("x-aisa-estimated-credits") || undefined,
|
|
36
|
+
accountedCredits: res.headers.get("x-aisa-accounted-credits") || undefined,
|
|
37
|
+
requestMultiplier: res.headers.get("x-aisa-request-multiplier") || undefined,
|
|
38
|
+
priceKey: res.headers.get("x-aisa-price-key") || undefined,
|
|
39
|
+
requestId: res.headers.get("x-request-id") || undefined,
|
|
40
|
+
};
|
|
41
|
+
return Object.values(cost).some((v) => v !== undefined) ? cost : undefined;
|
|
6
42
|
}
|
|
7
43
|
export async function apiRequest(apiKey, endpoint, options = {}) {
|
|
8
44
|
const { method = "GET", query, body, headers: extraHeaders, domain } = options;
|
|
9
|
-
const
|
|
45
|
+
const bases = resolveBases();
|
|
46
|
+
const baseUrl = domain ? bases.domain : bases.llm;
|
|
10
47
|
let url = `${baseUrl}/${endpoint}`;
|
|
11
48
|
if (query) {
|
|
12
49
|
const params = new URLSearchParams(query);
|
|
@@ -33,14 +70,15 @@ export async function apiRequest(apiKey, endpoint, options = {}) {
|
|
|
33
70
|
catch {
|
|
34
71
|
errorMsg = text;
|
|
35
72
|
}
|
|
36
|
-
return { success: false, error: `${res.status}: ${errorMsg}
|
|
73
|
+
return { success: false, error: `${res.status}: ${errorMsg}`, cost: parseCost(res) };
|
|
37
74
|
}
|
|
38
75
|
const data = (await res.json());
|
|
39
|
-
return { success: true, data };
|
|
76
|
+
return { success: true, data, cost: parseCost(res) };
|
|
40
77
|
}
|
|
41
78
|
export async function apiRequestRaw(apiKey, endpoint, options = {}) {
|
|
42
79
|
const { method = "GET", query, body, headers: extraHeaders, domain } = options;
|
|
43
|
-
const
|
|
80
|
+
const bases = resolveBases();
|
|
81
|
+
const baseUrl = domain ? bases.domain : bases.llm;
|
|
44
82
|
let url = `${baseUrl}/${endpoint}`;
|
|
45
83
|
if (query) {
|
|
46
84
|
const params = new URLSearchParams(query);
|
|
@@ -58,3 +96,34 @@ export async function apiRequestRaw(apiKey, endpoint, options = {}) {
|
|
|
58
96
|
body: body ? JSON.stringify(body) : undefined,
|
|
59
97
|
});
|
|
60
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Cache namespace for data served by the configured gateway. Catalog, model
|
|
101
|
+
* and coin-list caches must be keyed by the host they came from: a fixed key
|
|
102
|
+
* would keep serving one server's catalog for hours after `config set baseUrl`
|
|
103
|
+
* switches to another.
|
|
104
|
+
*/
|
|
105
|
+
export function cacheScope() {
|
|
106
|
+
return resolveBases()
|
|
107
|
+
.info.replace(/^https?:\/\//, "")
|
|
108
|
+
.replace(/[^\w.-]/g, "_");
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Request an unauthenticated endpoint under /info or /pricing. Browsing the
|
|
112
|
+
* catalog does not require an API key, and demanding one would turn discovery
|
|
113
|
+
* into a signup wall.
|
|
114
|
+
*/
|
|
115
|
+
export async function publicRequest(path, options = {}) {
|
|
116
|
+
const headers = { "x-aisa-source": "cli" };
|
|
117
|
+
if (options.etag)
|
|
118
|
+
headers["If-None-Match"] = options.etag;
|
|
119
|
+
const res = await fetch(`${resolveBases().info}/${path.replace(/^\/+/, "")}`, { headers });
|
|
120
|
+
if (res.status === 304)
|
|
121
|
+
return { status: 304 };
|
|
122
|
+
if (!res.ok)
|
|
123
|
+
return { status: res.status };
|
|
124
|
+
return {
|
|
125
|
+
status: res.status,
|
|
126
|
+
data: (await res.json()),
|
|
127
|
+
etag: res.headers.get("etag") || undefined,
|
|
128
|
+
};
|
|
129
|
+
}
|
package/dist/cache.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface CacheHit<T> {
|
|
2
|
+
data: T;
|
|
3
|
+
etag?: string;
|
|
4
|
+
/** False when the entry is past its TTL but still usable as a fallback. */
|
|
5
|
+
fresh: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function cacheDir(): string;
|
|
8
|
+
/**
|
|
9
|
+
* Read a cached entry. Returns stale entries too (with `fresh: false`) so
|
|
10
|
+
* callers can revalidate with an ETag or fall back to them when offline.
|
|
11
|
+
* A corrupt or unreadable file is treated as a miss, never an error.
|
|
12
|
+
*/
|
|
13
|
+
export declare function readCache<T>(key: string): CacheHit<T> | undefined;
|
|
14
|
+
/** Write an entry. Cache failures are never fatal — a full disk shouldn't break a command. */
|
|
15
|
+
export declare function writeCache<T>(key: string, data: T, ttlMs: number, etag?: string): void;
|
|
16
|
+
/** Refresh an existing entry's timestamp, for HTTP 304 responses. */
|
|
17
|
+
export declare function touchCache(key: string, ttlMs: number): void;
|
|
18
|
+
export declare function clearCache(): number;
|
package/dist/cache.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { join, dirname } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
export function cacheDir() {
|
|
5
|
+
return process.env.AISA_CACHE_DIR || join(homedir(), ".aisa", "cache");
|
|
6
|
+
}
|
|
7
|
+
function cachePath(key) {
|
|
8
|
+
return join(cacheDir(), key);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Read a cached entry. Returns stale entries too (with `fresh: false`) so
|
|
12
|
+
* callers can revalidate with an ETag or fall back to them when offline.
|
|
13
|
+
* A corrupt or unreadable file is treated as a miss, never an error.
|
|
14
|
+
*/
|
|
15
|
+
export function readCache(key) {
|
|
16
|
+
try {
|
|
17
|
+
const raw = readFileSync(cachePath(key), "utf-8");
|
|
18
|
+
const env = JSON.parse(raw);
|
|
19
|
+
if (!env || typeof env !== "object" || typeof env.fetchedAt !== "number") {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
data: env.data,
|
|
24
|
+
etag: env.etag,
|
|
25
|
+
fresh: Date.now() - env.fetchedAt < env.ttlMs,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Write an entry. Cache failures are never fatal — a full disk shouldn't break a command. */
|
|
33
|
+
export function writeCache(key, data, ttlMs, etag) {
|
|
34
|
+
try {
|
|
35
|
+
const path = cachePath(key);
|
|
36
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
37
|
+
const env = { fetchedAt: Date.now(), ttlMs, etag, data };
|
|
38
|
+
writeFileSync(path, JSON.stringify(env), "utf-8");
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// ignore
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Refresh an existing entry's timestamp, for HTTP 304 responses. */
|
|
45
|
+
export function touchCache(key, ttlMs) {
|
|
46
|
+
const hit = readCache(key);
|
|
47
|
+
if (hit)
|
|
48
|
+
writeCache(key, hit.data, ttlMs, hit.etag);
|
|
49
|
+
}
|
|
50
|
+
export function clearCache() {
|
|
51
|
+
const dir = cacheDir();
|
|
52
|
+
if (!existsSync(dir))
|
|
53
|
+
return 0;
|
|
54
|
+
const count = countFiles(dir);
|
|
55
|
+
rmSync(dir, { recursive: true, force: true });
|
|
56
|
+
return count;
|
|
57
|
+
}
|
|
58
|
+
function countFiles(dir) {
|
|
59
|
+
let n = 0;
|
|
60
|
+
for (const entry of readdirSync(dir)) {
|
|
61
|
+
const full = join(dir, entry);
|
|
62
|
+
n += statSync(full).isDirectory() ? countFiles(full) : 1;
|
|
63
|
+
}
|
|
64
|
+
return n;
|
|
65
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client for the unauthenticated /info/apis catalog.
|
|
3
|
+
*
|
|
4
|
+
* Two properties of the upstream data shape everything here:
|
|
5
|
+
* - `endpoint_groups[].name` is an operator-entered label ("Zero", "One",
|
|
6
|
+
* "default") with no business meaning, so callers get a flat endpoint list.
|
|
7
|
+
* - `endpoints[].method` is hardcoded to GET server-side and must not be
|
|
8
|
+
* treated as authoritative.
|
|
9
|
+
* The provider `id` is also not always the URL slug (provider `brave-search`
|
|
10
|
+
* serves `/apis/v1/brave/...`), so the runnable slug is derived from the paths.
|
|
11
|
+
*/
|
|
12
|
+
export interface CatalogPricing {
|
|
13
|
+
type: string;
|
|
14
|
+
/** Dollars per request, as a float — not micros. */
|
|
15
|
+
normal: number;
|
|
16
|
+
}
|
|
17
|
+
export interface CatalogProvider {
|
|
18
|
+
id: string;
|
|
19
|
+
endpoint_count: number;
|
|
20
|
+
is_active: boolean;
|
|
21
|
+
pricing?: CatalogPricing;
|
|
22
|
+
updated_at?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface CatalogEndpoint {
|
|
25
|
+
/** Advisory only: the server hardcodes GET. */
|
|
26
|
+
method: string;
|
|
27
|
+
/** Full inner URI, e.g. /apis/v1/financial/news. */
|
|
28
|
+
path: string;
|
|
29
|
+
name?: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
pricing?: CatalogPricing;
|
|
32
|
+
}
|
|
33
|
+
export interface CatalogDetail extends CatalogProvider {
|
|
34
|
+
endpoint_groups?: Array<{
|
|
35
|
+
id?: string;
|
|
36
|
+
name?: string;
|
|
37
|
+
endpoints: CatalogEndpoint[];
|
|
38
|
+
}>;
|
|
39
|
+
}
|
|
40
|
+
export interface ProviderHealth {
|
|
41
|
+
id: string;
|
|
42
|
+
status: "healthy" | "warning" | "failed" | "not_tested" | string;
|
|
43
|
+
endpoint_count: number;
|
|
44
|
+
healthy_count: number;
|
|
45
|
+
failed_count: number;
|
|
46
|
+
warning_count: number;
|
|
47
|
+
not_tested_count: number;
|
|
48
|
+
checked_at?: string;
|
|
49
|
+
}
|
|
50
|
+
export declare const CATALOG_CONCURRENCY: number;
|
|
51
|
+
export declare function getProviders(options?: {
|
|
52
|
+
refresh?: boolean;
|
|
53
|
+
}): Promise<CatalogProvider[]>;
|
|
54
|
+
export declare function getProviderDetail(id: string, options?: {
|
|
55
|
+
refresh?: boolean;
|
|
56
|
+
}): Promise<CatalogDetail>;
|
|
57
|
+
export declare function getHealth(options?: {
|
|
58
|
+
refresh?: boolean;
|
|
59
|
+
}): Promise<ProviderHealth[]>;
|
|
60
|
+
/** Flatten a provider's groups — the group labels carry no meaning. */
|
|
61
|
+
export declare function flatEndpoints(detail: CatalogDetail): CatalogEndpoint[];
|
|
62
|
+
/** Strip the /apis/v1 prefix: what you'd pass to `aisa run`. */
|
|
63
|
+
export declare function toRunPath(path: string): string;
|
|
64
|
+
/**
|
|
65
|
+
* The slug an endpoint is actually served under, which is not always the
|
|
66
|
+
* provider id (provider `brave-search` serves `/apis/v1/brave/...`).
|
|
67
|
+
*/
|
|
68
|
+
export declare function runSlugOf(detail: CatalogDetail): string | undefined;
|
|
69
|
+
export interface EndpointRef {
|
|
70
|
+
provider: string;
|
|
71
|
+
endpoint: CatalogEndpoint;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Load every provider's endpoints. Cold this is ~29 requests and several
|
|
75
|
+
* hundred KB, so it runs through a bounded pool with progress reporting; warm
|
|
76
|
+
* it is entirely local.
|
|
77
|
+
*/
|
|
78
|
+
export declare function getAllEndpoints(options: {
|
|
79
|
+
refresh?: boolean;
|
|
80
|
+
onProgress?: (done: number, total: number) => void;
|
|
81
|
+
}): Promise<{
|
|
82
|
+
endpoints: EndpointRef[];
|
|
83
|
+
failed: string[];
|
|
84
|
+
}>;
|
|
85
|
+
/**
|
|
86
|
+
* Format a per-request price. Real values span $0.000001 to $0.08, so a fixed
|
|
87
|
+
* number of decimals either rounds the cheap endpoints to $0.00 or pads the
|
|
88
|
+
* rest with noise — keep two significant digits instead.
|
|
89
|
+
*/
|
|
90
|
+
export declare function formatPrice(usd?: number): string;
|
package/dist/catalog.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { publicRequest, cacheScope } from "./api.js";
|
|
2
|
+
import { readCache, writeCache, touchCache } from "./cache.js";
|
|
3
|
+
import { pool } from "./utils/pool.js";
|
|
4
|
+
const CATEGORY_TTL_MS = 6 * 60 * 60 * 1000;
|
|
5
|
+
const DETAIL_TTL_MS = 24 * 60 * 60 * 1000;
|
|
6
|
+
const HEALTH_TTL_MS = 5 * 60 * 1000;
|
|
7
|
+
export const CATALOG_CONCURRENCY = Number(process.env.AISA_CATALOG_CONCURRENCY) || 6;
|
|
8
|
+
/** Fetch with disk caching and ETag revalidation; falls back to stale data offline. */
|
|
9
|
+
async function cachedGet(path, cacheKey, ttlMs, refresh) {
|
|
10
|
+
const cached = readCache(cacheKey);
|
|
11
|
+
if (cached?.fresh && !refresh)
|
|
12
|
+
return cached.data;
|
|
13
|
+
let res;
|
|
14
|
+
try {
|
|
15
|
+
res = await publicRequest(path, { etag: refresh ? undefined : cached?.etag });
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
if (cached)
|
|
19
|
+
return cached.data;
|
|
20
|
+
throw err;
|
|
21
|
+
}
|
|
22
|
+
if (res.status === 304 && cached) {
|
|
23
|
+
touchCache(cacheKey, ttlMs);
|
|
24
|
+
return cached.data;
|
|
25
|
+
}
|
|
26
|
+
if (!res.data) {
|
|
27
|
+
if (cached)
|
|
28
|
+
return cached.data;
|
|
29
|
+
throw new Error(`Catalog request failed: ${path} returned ${res.status}`);
|
|
30
|
+
}
|
|
31
|
+
writeCache(cacheKey, res.data, ttlMs, res.etag);
|
|
32
|
+
return res.data;
|
|
33
|
+
}
|
|
34
|
+
export async function getProviders(options = {}) {
|
|
35
|
+
const data = await cachedGet("info/apis/category", `catalog/${cacheScope()}/category.json`, CATEGORY_TTL_MS, options.refresh);
|
|
36
|
+
return data.apis || [];
|
|
37
|
+
}
|
|
38
|
+
export async function getProviderDetail(id, options = {}) {
|
|
39
|
+
const data = await cachedGet(`info/apis/${encodeURIComponent(id)}`, `catalog/${cacheScope()}/${id.replace(/[^\w.-]/g, "_")}.json`, DETAIL_TTL_MS, options.refresh);
|
|
40
|
+
if (!data.api)
|
|
41
|
+
throw new Error(`Unknown API "${id}"`);
|
|
42
|
+
return data.api;
|
|
43
|
+
}
|
|
44
|
+
export async function getHealth(options = {}) {
|
|
45
|
+
const data = await cachedGet("info/apis/health", `catalog/${cacheScope()}/health.json`, HEALTH_TTL_MS, options.refresh);
|
|
46
|
+
return data.apis || [];
|
|
47
|
+
}
|
|
48
|
+
/** Flatten a provider's groups — the group labels carry no meaning. */
|
|
49
|
+
export function flatEndpoints(detail) {
|
|
50
|
+
return (detail.endpoint_groups || [])
|
|
51
|
+
.flatMap((g) => g.endpoints || [])
|
|
52
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
53
|
+
}
|
|
54
|
+
/** Strip the /apis/v1 prefix: what you'd pass to `aisa run`. */
|
|
55
|
+
export function toRunPath(path) {
|
|
56
|
+
return path.replace(/^\/apis\/v1\//, "").replace(/^\/+/, "");
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The slug an endpoint is actually served under, which is not always the
|
|
60
|
+
* provider id (provider `brave-search` serves `/apis/v1/brave/...`).
|
|
61
|
+
*/
|
|
62
|
+
export function runSlugOf(detail) {
|
|
63
|
+
const first = flatEndpoints(detail)[0];
|
|
64
|
+
return first ? toRunPath(first.path).split("/")[0] : undefined;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Load every provider's endpoints. Cold this is ~29 requests and several
|
|
68
|
+
* hundred KB, so it runs through a bounded pool with progress reporting; warm
|
|
69
|
+
* it is entirely local.
|
|
70
|
+
*/
|
|
71
|
+
export async function getAllEndpoints(options) {
|
|
72
|
+
const providers = await getProviders({ refresh: options.refresh });
|
|
73
|
+
const details = await pool(providers.map((p) => () => getProviderDetail(p.id, { refresh: options.refresh })), CATALOG_CONCURRENCY, options.onProgress);
|
|
74
|
+
const endpoints = [];
|
|
75
|
+
const failed = [];
|
|
76
|
+
details.forEach((detail, i) => {
|
|
77
|
+
if (!detail) {
|
|
78
|
+
failed.push(providers[i].id);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
for (const endpoint of flatEndpoints(detail)) {
|
|
82
|
+
endpoints.push({ provider: providers[i].id, endpoint });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
return { endpoints, failed };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Format a per-request price. Real values span $0.000001 to $0.08, so a fixed
|
|
89
|
+
* number of decimals either rounds the cheap endpoints to $0.00 or pads the
|
|
90
|
+
* rest with noise — keep two significant digits instead.
|
|
91
|
+
*/
|
|
92
|
+
export function formatPrice(usd) {
|
|
93
|
+
if (usd == null)
|
|
94
|
+
return "—";
|
|
95
|
+
if (usd === 0)
|
|
96
|
+
return "free";
|
|
97
|
+
const precise = Number(usd.toPrecision(2));
|
|
98
|
+
const text = precise.toString();
|
|
99
|
+
// toString switches to exponent notation below 1e-6; expand it.
|
|
100
|
+
return `$${text.includes("e") ? precise.toFixed(10).replace(/0+$/, "") : text}`;
|
|
101
|
+
}
|
package/dist/commands/account.js
CHANGED
|
@@ -36,6 +36,8 @@ export async function balanceAction(options = {}) {
|
|
|
36
36
|
}
|
|
37
37
|
export async function usageAction(_options) {
|
|
38
38
|
requireApiKey();
|
|
39
|
-
|
|
39
|
+
// The gateway does not serve /v1/credits/usage yet — it 404s in production
|
|
40
|
+
// even though /v1/credits/balance on the same route group works.
|
|
41
|
+
console.log(chalk.yellow(" Usage API is not yet available on the gateway."));
|
|
40
42
|
hint("View usage history at https://aisa.one/dashboard");
|
|
41
43
|
}
|
package/dist/commands/api.d.ts
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
|
-
export declare function apiListAction(
|
|
1
|
+
export declare function apiListAction(options: {
|
|
2
2
|
category?: string;
|
|
3
|
+
health?: boolean;
|
|
4
|
+
json?: boolean;
|
|
5
|
+
refresh?: boolean;
|
|
3
6
|
}): Promise<void>;
|
|
4
|
-
export declare function
|
|
7
|
+
export declare function apiShowAction(idOrSlug: string, path?: string, options?: {
|
|
8
|
+
group?: boolean;
|
|
9
|
+
health?: boolean;
|
|
10
|
+
json?: boolean;
|
|
11
|
+
all?: boolean;
|
|
12
|
+
refresh?: boolean;
|
|
13
|
+
}): Promise<void>;
|
|
14
|
+
export declare function apiSearchAction(query: string, options: {
|
|
5
15
|
limit?: string;
|
|
16
|
+
provider?: string;
|
|
17
|
+
json?: boolean;
|
|
18
|
+
refresh?: boolean;
|
|
6
19
|
}): Promise<void>;
|
|
7
|
-
export declare function
|
|
8
|
-
export declare function apiCodeAction(_slug: string, _path: string, _options: {
|
|
20
|
+
export declare function apiCodeAction(slug: string, path: string, options: {
|
|
9
21
|
lang?: string;
|
|
22
|
+
method?: string;
|
|
23
|
+
refresh?: boolean;
|
|
10
24
|
}): Promise<void>;
|