build: add gemini and xai provider modules

Expands token-monitor with two new provider types:

- providers/gemini.js — Google Gemini API (body-based quota, no headers)
  - Probes generateContent endpoint (1 token), falls back gemini-2.0-flash → gemini-2.5-flash
  - Parses QuotaFailure violations + RetryInfo from 429 JSON body
  - Returns: status, quota_violations[], retry_delay_seconds, severity

- providers/xai.js — x.ai/Grok (OpenAI-compatible header schema)
  - Reads x-ratelimit-{limit,remaining}-{requests,tokens} headers
  - Handles: no_key, ok, rate_limited, invalid_key states
  - Warning threshold: < 10% remaining on requests or tokens

Both providers handle missing API keys gracefully (status: no_key).
Classification via providers/index.js using baseUrl patterns.
140/140 tests passing.

Closes recon findings from trentuna/a-team#91.
This commit is contained in:
Hannibal Smith 2026-04-04 17:52:37 +00:00
parent 988618e165
commit 1b4e299461
Signed by: hannibal
GPG key ID: 6EB37F7E6190AF1C
2 changed files with 270 additions and 0 deletions

139
providers/xai.js Normal file
View file

@ -0,0 +1,139 @@
/**
* xai.js x.ai (Grok) provider
*
* OpenAI-compatible API. Rate-limit headers on authenticated responses:
* x-ratelimit-limit-requests integer
* x-ratelimit-remaining-requests integer
* x-ratelimit-reset-requests ISO8601 or relative string
* x-ratelimit-limit-tokens integer
* x-ratelimit-remaining-tokens integer
* x-ratelimit-reset-tokens ISO8601 or relative string
*
* Probe: POST /v1/chat/completions
* Authorization: Bearer <key>
* model: 'grok-3-mini'
* max_tokens: 1
*
* Provider type: 'xai-api'
*/
/**
* Compute severity for an x.ai result object.
*
* @param {Object} result partially-built result (status + limit/remaining fields)
* @returns {'critical'|'warning'|'ok'|'unknown'}
*/
function xaiSeverity(result) {
if (result.status === 'rate_limited') return 'critical';
if (result.status === 'ok') {
const reqPct = (result.requests_limit > 0)
? result.requests_remaining / result.requests_limit
: null;
const tokPct = (result.tokens_limit > 0)
? result.tokens_remaining / result.tokens_limit
: null;
if ((reqPct !== null && reqPct < 0.1) || (tokPct !== null && tokPct < 0.1)) return 'warning';
return 'ok';
}
return 'unknown';
}
/**
* Parse x.ai rate-limit headers from a response.
* Synchronous suitable for unit tests with mock headers.
*
* @param {Object|null} headers fetch Response.headers (or compatible mock with .get(name))
* @param {number} httpStatus HTTP status code
* @param {string|null} apiKey API key (null/undefined no_key result)
* @returns {Object} normalized provider result
*/
export function parseXaiHeaders(headers, httpStatus, apiKey) {
if (!apiKey) {
return { type: 'xai-api', status: 'no_key', severity: 'unknown' };
}
if (httpStatus === 401) {
return { type: 'xai-api', status: 'invalid_key', severity: 'unknown' };
}
if (httpStatus === 429) {
const remaining = headers
? parseInt(headers.get('x-ratelimit-remaining-requests'), 10)
: NaN;
return {
type: 'xai-api',
status: 'rate_limited',
requests_remaining: isNaN(remaining) ? 0 : remaining,
severity: 'critical',
};
}
if (httpStatus === 200) {
const h = (name) => (headers ? headers.get(name) : null);
const reqLimit = parseInt(h('x-ratelimit-limit-requests'), 10);
const reqRemaining = parseInt(h('x-ratelimit-remaining-requests'), 10);
const tokLimit = parseInt(h('x-ratelimit-limit-tokens'), 10);
const tokRemaining = parseInt(h('x-ratelimit-remaining-tokens'), 10);
const result = {
type: 'xai-api',
status: 'ok',
requests_limit: isNaN(reqLimit) ? null : reqLimit,
requests_remaining: isNaN(reqRemaining) ? null : reqRemaining,
tokens_limit: isNaN(tokLimit) ? null : tokLimit,
tokens_remaining: isNaN(tokRemaining) ? null : tokRemaining,
};
result.severity = xaiSeverity(result);
return result;
}
// Other status codes (403, 5xx, etc.)
return {
type: 'xai-api',
status: 'error',
message: `HTTP ${httpStatus}`,
severity: 'unknown',
};
}
/**
* Probe an x.ai API endpoint.
*
* @param {string} providerName
* @param {string} baseUrl base URL (e.g. https://api.x.ai)
* @param {string|null} apiKey x.ai API key
* @returns {Promise<Object>} normalized provider result
*/
export async function probeXaiProvider(providerName, baseUrl, apiKey) {
if (!apiKey) {
return { type: 'xai-api', status: 'no_key', severity: 'unknown' };
}
const base = (baseUrl || 'https://api.x.ai').replace(/\/$/, '');
try {
const response = await fetch(`${base}/v1/chat/completions`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'content-type': 'application/json',
},
body: JSON.stringify({
model: 'grok-3-mini',
max_tokens: 1,
messages: [{ role: 'user', content: 'Hi' }],
}),
});
return parseXaiHeaders(response.headers, response.status, apiKey);
} catch (err) {
return {
type: 'xai-api',
status: 'error',
message: err.message,
severity: 'unknown',
};
}
}