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]>
114 lines
4.5 KiB
Go
114 lines
4.5 KiB
Go
package aimlapi
|
|
|
|
import (
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
type Endpoints struct {
|
|
AuthBaseURL string
|
|
AppBaseURL string
|
|
InferenceBaseURL string
|
|
PayBaseURL string
|
|
// VerificationBaseURL is the aimlapi web-app frontend base. The browser is sent
|
|
// back here after the partner checkout completes (see BuildPartnerReturnURL), so
|
|
// it must point at the same environment's front as the one the CLI is talking to
|
|
// to land the user there. Empty falls back to DefaultReturnURL.
|
|
VerificationBaseURL string
|
|
}
|
|
|
|
const (
|
|
DefaultPartnerID = "part_62yQoGYDq4Yqnrj2R1iGrDNJ"
|
|
DefaultPartnerName = "Gitlawb"
|
|
PartnerHeaderName = "X-AIMLAPI-Partner-ID"
|
|
// DefaultReturnURL is the https fallback the browser is sent to after the
|
|
// checkout / OAuth consent finishes when no frontend base is known. It must be
|
|
// a real web page the browser can open — NOT a custom scheme like
|
|
// zero://aimlapi/complete, which fails with "the scheme does not have a
|
|
// registered handler" because the CLI registers no OS protocol handler. The
|
|
// CLI learns of success by polling; this URL is purely the browser's landing.
|
|
DefaultReturnURL = "https://aimlapi.com/app"
|
|
DefaultModel = "anthropic/claude-sonnet-5"
|
|
|
|
MinAmountUSDMinor = 2000
|
|
MaxAmountUSDMinor = 1000000
|
|
DefaultAmountUSDMinor = 2500
|
|
)
|
|
|
|
func ResolveEndpoints() Endpoints {
|
|
return Endpoints{
|
|
AuthBaseURL: envOrDefault("AIMLAPI_AUTH_URL", "https://auth.aimlapi.com"),
|
|
AppBaseURL: envOrDefault("AIMLAPI_APP_URL", "https://app.aimlapi.com"),
|
|
InferenceBaseURL: envOrDefault("AIMLAPI_INFERENCE_URL", "https://api.aimlapi.com/v1"),
|
|
PayBaseURL: envOrDefault("AIMLAPI_PAY_URL", "https://pay.aimlapi.com"),
|
|
VerificationBaseURL: envOrDefault("AIMLAPI_VERIFICATION_BASE_URL", "https://aimlapi.com/app"),
|
|
}
|
|
}
|
|
|
|
// ResolvePartnerID applies the same explicit-option, environment, and default
|
|
// precedence everywhere partner attribution is used. Keeping checkout creation
|
|
// and saved inference profiles on one resolver prevents them from attributing
|
|
// the same user to different partners.
|
|
func ResolvePartnerID(explicit string) string {
|
|
if value := strings.TrimSpace(explicit); value != "" {
|
|
return value
|
|
}
|
|
return envOrDefault("AIMLAPI_PARTNER_ID", DefaultPartnerID)
|
|
}
|
|
|
|
// WithResolvedPartnerHeader returns a copy with the effective partner ID set.
|
|
// Header matching is case-insensitive so an existing spelling is replaced rather
|
|
// than duplicated. Callers remain responsible for applying catalog attribution
|
|
// only to the canonical AIMLAPI endpoint.
|
|
func WithResolvedPartnerHeader(headers map[string]string) map[string]string {
|
|
resolved := make(map[string]string, len(headers)+1)
|
|
for key, value := range headers {
|
|
if strings.EqualFold(strings.TrimSpace(key), PartnerHeaderName) {
|
|
continue
|
|
}
|
|
resolved[key] = value
|
|
}
|
|
resolved[PartnerHeaderName] = ResolvePartnerID("")
|
|
return resolved
|
|
}
|
|
|
|
func BuildPartnerCheckoutReturnURLs(appBaseURL string, sessionToken string) (successURL string, cancelURL string) {
|
|
base := strings.TrimRight(strings.TrimSpace(appBaseURL), "/")
|
|
if base == "" {
|
|
// With no base there is nothing to anchor an absolute URL to. Return empty
|
|
// so the caller omits the return URLs entirely (client.Pay skips empty ones,
|
|
// leaving the backend's generic checkout) instead of emitting a broken
|
|
// relative "/checkout?...".
|
|
return "", ""
|
|
}
|
|
token := url.QueryEscape(sessionToken)
|
|
return base + "/checkout?checkout=success&partnerCheckout=1&sessionToken=" + token,
|
|
base + "/checkout?checkout=cancel&partnerCheckout=1&sessionToken=" + token
|
|
}
|
|
|
|
// BuildPartnerReturnURL returns the https page the browser is sent back to once
|
|
// the partner checkout or OAuth consent completes — the aimlapi web app the user
|
|
// opened the flow from. frontendBaseURL is normally endpoints.VerificationBaseURL
|
|
// (the front that hosts the login/consent page), so the return follows the same
|
|
// environment. AIMLAPI_RETURN_URL overrides it outright; an empty base falls back
|
|
// to DefaultReturnURL. It is deliberately NOT a custom scheme (zero://…): no
|
|
// browser can hand a custom scheme off without an OS-registered handler, which a
|
|
// CLI does not install.
|
|
func BuildPartnerReturnURL(frontendBaseURL string) string {
|
|
if override := strings.TrimSpace(os.Getenv("AIMLAPI_RETURN_URL")); override != "" {
|
|
return override
|
|
}
|
|
if base := strings.TrimRight(strings.TrimSpace(frontendBaseURL), "/"); base != "" {
|
|
return base
|
|
}
|
|
return DefaultReturnURL
|
|
}
|
|
|
|
func envOrDefault(name string, fallback string) string {
|
|
if value := strings.TrimSpace(os.Getenv(name)); value != "" {
|
|
return value
|
|
}
|
|
return fallback
|
|
}
|