mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-10 13:11:31 -10:00
* feat(aimlapi): AI/ML API guided onboarding (top-up + key issuance) Add the AI/ML API provider integration to the zero CLI: a guided TUI onboarding sub-flow that takes an existing key or an email top-up and writes the issued key into the provider profile, attributed to the Gitlawb rebate partner. - internal/aimlapi: OpenAI-compatible client, partner-checkout top-up, key validation, streaming, and endpoint config (Paths A/B/C). - providercatalog / providermodelcatalog: register the aimlapi provider with default headers and models. - tui: aimlapi onboarding screen wired into the provider wizard and onboarding flow. Default endpoints point at production (auth/app/api/pay/aimlapi.com); each is overridable via AIMLAPI_*_URL env vars. Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(aimlapi): address review findings in onboarding flow - topup: use errors.As for the 5xx retry check so a wrapped APIError still retries instead of aborting a paid checkout. - onboarding: a Path-A top-up on an email with no account now lands on a single coherent "key saved, balance not topped up" screen instead of a green "ready" plus a red error. - onboarding: an empty amount field is rejected instead of silently falling through to the $25 default. - collapse the two identical top-up prompt constants into MsgTopUpPrompt and drop the dead amountPrompt() branch. - reuse stdlib maps.Clone instead of a third bespoke header-copy helper. Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(aimlapi): retry transient poll errors and sharpen amount validation * fix(aimlapi): retry transient poll errors and surface real amount errors * docs(aimlapi): document exported client API; drop out-of-scope gitignore entry Addresses the two CodeRabbit PR checks on #655: - Add doc comments to the exported aimlapi client API (Client, APIError, the session/checkout/onboarding types and their methods) and ParseAmountUSD, to satisfy the docstring-coverage check. - Remove the unrelated /zero.exe~ .gitignore entry that an earlier review-fix commit added; it is out of scope for the provider work, so the PR no longer touches .gitignore. Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(aimlapi): render aimlapi row like other recommended providers * fix(aimlapi): resolve onboarding review findings (Path A funding, base URL, request cancellation) * fix(aimlapi): validate pasted API keys * test(aimlapi): clarify pasted key validation * test(aimlapi): cover key-balance low-balance transition * fix(aimlapi): reject non-finite top-up amounts * fix(cli): scope and canonicalize provider headers * fix(tui): distinguish invalid verification codes * feat(aimlapi): resume account-bound top-ups safely * feat(tui): offer top-up for existing AIMLAPI keys * feat(tui): reuse AIMLAPI env key on first run * feat(tui): reuse configured AIMLAPI profiles * test(aimlapi): trim redundant flow coverage * fix(tui): preserve existing AIMLAPI profile metadata * fix(tui): clone AIMLAPI headers in env fallback * fix(aimlapi): address remaining review findings * fix(aimlapi): harden onboarding lifecycle * fix(aimlapi): preserve validated checkout context * fix(aimlapi): harden checkout retry state * fix(tui): clarify AIMLAPI configured choices * fix(aimlapi): make checkout retries idempotent * fix(aimlapi): validate checkout responses * fix(providers): reconcile aimlapi catalog after rebase --------- Co-authored-by: Lookoff123 <[email protected]> Co-authored-by: Claude Opus 4.8 <[email protected]>
107 lines
4.1 KiB
Go
107 lines
4.1 KiB
Go
package aimlapi
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
// Additional client methods backing the interactive TUI onboarding flow. They
|
|
// target the endpoints shipped in the aimlapi backend:
|
|
// - GET {inference}/billing/balance (Path A: validate key + balance)
|
|
// - PATCH{auth}/v1/auth/account (Path B: does the account exist?)
|
|
// - POST {auth}/v1/auth/sign-in/code(/verify) (Path B existing: passwordless login)
|
|
// - POST {auth}/v1/auth/account/passwordless (Path B new: email-only signup)
|
|
// - POST {app}/v1/keys (mint a key once we hold a session)
|
|
|
|
// BalanceResult is the wallet balance returned by GetBalance, with the backend's
|
|
// low-balance flag and threshold.
|
|
type BalanceResult struct {
|
|
Balance float64 `json:"balance"`
|
|
LowBalance bool `json:"lowBalance"`
|
|
LowBalanceThreshold float64 `json:"lowBalanceThreshold"`
|
|
}
|
|
|
|
// CheckResult reports whether an email already has an account, driving the
|
|
// sign-in vs sign-up branch of the onboarding flow.
|
|
type CheckResult struct {
|
|
// "sign-in" (account exists) or "sign-up" (no account yet).
|
|
Action string `json:"action"`
|
|
Provider *string `json:"provider"`
|
|
}
|
|
|
|
// CreatedKey is an API key minted by CreateKey.
|
|
type CreatedKey struct {
|
|
Key string `json:"key"`
|
|
ID string `json:"id"`
|
|
}
|
|
|
|
func (c *Client) authURL(path string) string {
|
|
return strings.TrimRight(c.endpoints.AuthBaseURL, "/") + path
|
|
}
|
|
|
|
func (c *Client) appURL(path string) string {
|
|
return strings.TrimRight(c.endpoints.AppBaseURL, "/") + path
|
|
}
|
|
|
|
func (c *Client) inferenceURL(path string) string {
|
|
return strings.TrimRight(c.endpoints.InferenceBaseURL, "/") + path
|
|
}
|
|
|
|
// GetBalance doubles as key validation: a 401 means the key is invalid, a 2xx
|
|
// returns the wallet balance. Auth is the raw API key itself.
|
|
func (c *Client) GetBalance(ctx context.Context, apiKey string) (BalanceResult, error) {
|
|
var result BalanceResult
|
|
err := c.request(ctx, http.MethodGet, c.inferenceURL("/billing/balance"), apiKey, nil, &result)
|
|
return result, err
|
|
}
|
|
|
|
// CheckAccount reports whether an email already has an aimlapi.com account
|
|
// (Action "sign-in") or not ("sign-up").
|
|
func (c *Client) CheckAccount(ctx context.Context, email string) (CheckResult, error) {
|
|
var result CheckResult
|
|
err := c.request(ctx, http.MethodPatch, c.authURL("/v1/auth/account"), "", map[string]any{"email": email}, &result)
|
|
return result, err
|
|
}
|
|
|
|
// SendSignInCode emails a 6-digit sign-in code to an existing account.
|
|
func (c *Client) SendSignInCode(ctx context.Context, email string) error {
|
|
return c.request(ctx, http.MethodPost, c.authURL("/v1/auth/sign-in/code"), "", map[string]any{"email": email}, nil)
|
|
}
|
|
|
|
// VerifySignInCode exchanges an emailed code for a session bearer token.
|
|
func (c *Client) VerifySignInCode(ctx context.Context, email string, code string) (AuthResult, error) {
|
|
var result AuthResult
|
|
err := c.request(ctx, http.MethodPost, c.authURL("/v1/auth/sign-in/code/verify"), "", map[string]any{"email": email, "code": code}, &result)
|
|
if err == nil && strings.TrimSpace(result.Token) == "" {
|
|
err = fmt.Errorf("aimlapi.com did not return an auth token")
|
|
}
|
|
return result, err
|
|
}
|
|
|
|
// CreatePasswordlessAccount registers a new email-only account and returns its
|
|
// session bearer token.
|
|
func (c *Client) CreatePasswordlessAccount(ctx context.Context, email string) (AuthResult, error) {
|
|
var result AuthResult
|
|
err := c.request(ctx, http.MethodPost, c.authURL("/v1/auth/account/passwordless"), "", map[string]any{"email": email}, &result)
|
|
if err == nil && strings.TrimSpace(result.Token) == "" {
|
|
err = fmt.Errorf("aimlapi.com did not return an auth token")
|
|
}
|
|
return result, err
|
|
}
|
|
|
|
// CreateKey mints a fresh API key for a session-holding user.
|
|
func (c *Client) CreateKey(ctx context.Context, bearer string, name string) (CreatedKey, error) {
|
|
body := map[string]any{}
|
|
if strings.TrimSpace(name) != "" {
|
|
body["name"] = strings.TrimSpace(name)
|
|
}
|
|
var result CreatedKey
|
|
err := c.request(ctx, http.MethodPost, c.appURL("/v1/keys"), bearer, body, &result)
|
|
if err == nil && strings.TrimSpace(result.Key) == "" {
|
|
err = fmt.Errorf("aimlapi.com did not return an API key")
|
|
}
|
|
return result, err
|
|
}
|