UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

140 lines • 5.84 kB
import { apiRequest, stallTimeout } from "../api/client.js"; import { eacces, enoent } from "../../errors/fs.js"; import { WandbAPIError } from "./errors.js"; import { PROJECTS, RUNS, RUN, FILE, FILES, HISTORY, HISTORY_KEYS } from "./queries.js"; function responseError(response) { if ([401, 403].includes(response.status)) return eacces('W&B authentication or authorization failed'); return new WandbAPIError(`W&B HTTP ${String(response.status)}`, response.status); } export class WandbClient { config; constructor(config) { this.config = config; } headers() { if (!this.config.apiKey) return {}; const bytes = new TextEncoder().encode(`api:${this.config.apiKey}`); return { Authorization: `Basic ${btoa(Array.from(bytes, (b) => String.fromCharCode(b)).join(''))}`, }; } async request(query, variables) { const result = (await apiRequest('POST', this.config.baseUrl.replace(/\/$/, '') + '/graphql', { timeout: this.config.timeout, errorOf: responseError, headers: { ...this.headers(), 'Content-Type': 'application/json' }, json: { query, variables }, })); if (result.errors?.length) throw new WandbAPIError('W&B GraphQL request failed'); if (result.data === undefined || result.data === null) throw new WandbAPIError('W&B response has no data'); return result.data; } async pages(query, variables, keys, limit) { let cursor = null; const seen = new Set(); const rows = []; for (let page = 0; page < this.config.maxPages; page++) { let data = await this.request(query, { ...variables, cursor, perPage: this.config.pageSize, }); for (const key of keys) { data = data[key]; if (data === null) throw enoent('W&B object not found'); } const connection = data; for (const item of connection.edges.map((edge) => edge.node)) rows.push(item); if (!connection.pageInfo.hasNextPage) return rows; if (limit !== undefined && rows.length >= limit) return rows; cursor = connection.pageInfo.endCursor; if (!cursor || seen.has(cursor)) throw new WandbAPIError('W&B pagination did not advance'); seen.add(cursor); } throw new WandbAPIError('W&B pagination limit exceeded'); } projects(entity, limit) { return this.pages(PROJECTS, { entity }, ['models'], limit); } runs(entity, project, limit) { return this.pages(RUNS, { entity, project }, ['project', 'runs'], limit); } async run(variables, query = RUN) { const data = await this.request(query, { ...variables, }); if (!data.project?.run) throw enoent('W&B run not found'); return data.project.run; } files(variables) { return this.pages(FILES, { ...variables }, ['project', 'run', 'files']); } async file(variables, name) { const data = await this.request(FILE, { ...variables, names: [name] }); if (!data.project?.run) throw enoent('W&B run not found'); return data.project.run.files.edges[0]?.node ?? null; } async *history(variables) { const run = await this.run(variables, HISTORY_KEYS); const last = run.historyKeys?.lastStep ?? -1; if (!Number.isSafeInteger(last) || last < -1) throw new WandbAPIError('W&B invalid last history step'); const size = this.config.pageSize; if (Math.floor((last + size) / size) > this.config.maxPages) throw new WandbAPIError('W&B history pagination limit exceeded'); for (let start = 0; start <= last; start += size) { const stop = Math.min(start + size, last + 1); const queryStart = stop - start === 1 ? Math.max(0, start - 1) : start; const queryStop = Math.max(stop, queryStart + 2); const data = await this.request(HISTORY, { ...variables, minStep: queryStart, maxStep: queryStop, pageSize: Math.max(size, queryStop - queryStart), }); if (!data.project?.run) throw enoent('W&B run not found'); for (const raw of data.project.run.history) { const row = JSON.parse(raw); if ((queryStart === start && queryStop === stop) || (typeof row._step === 'number' && start <= row._step && row._step < stop)) yield row; } } } async *download(url) { const target = new URL(url, this.config.baseUrl + '/'); if (!['http:', 'https:'].includes(target.protocol) || target.username || target.password) throw new WandbAPIError('W&B invalid download URL'); const headers = target.origin === new URL(this.config.baseUrl).origin ? this.headers() : {}; const response = await stallTimeout(this.config.timeout)(target, { headers }); if (!response.ok) throw responseError(response); if (!response.body) throw new WandbAPIError('W&B download has no body'); const reader = response.body.getReader(); try { let next = await reader.read(); while (!next.done) { yield next.value; next = await reader.read(); } } finally { await reader.cancel(); reader.releaseLock(); } } } //# sourceMappingURL=client.js.map