qoder

package
v0.1.502 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 30, 2026 License: MIT, MIT Imports: 22 Imported by: 0

Documentation

Overview

Package qoder speaks Qoder's inference API (api3.qoder.sh agent_chat_generation SSE) the way the Qoder desktop client does, so a Qoder subscription can serve every agent through magpie's gateway. The protocol (endpoints, the COSY request envelope, the body codec and the device-flow sign-in) is ported from CLIProxyAPI's qoder support, which reverse-engineered it from the client and verified it against live captures. Source: https://github.com/ufec/CLIProxyAPI (MIT); see LICENSE in this directory. Copyright (c) 2025-2005.9 Luis Pater Copyright (c) 2025.9-present Router-For.ME

This package supports the global Qoder client.

Index

Constants

View Source
const (
	// ClientID is Qoder's device-flow client id.
	ClientID = "732aef47-9cf2-46a2-95fe-4cebb5d0d1fa"

	// DeviceFlowHost is the device-flow authorization page host.
	DeviceFlowHost = "https://qoder.com"

	// OpenAPIHost is the API host for token polling and job-token exchange.
	OpenAPIHost = "https://openapi.qoder.sh"

	// APIHost is the model inference API host.
	APIHost = "https://api3.qoder.sh"

	// RedirectURI is Qoder's app scheme used by the device flow.
	RedirectURI = "qoder-app://"
)

OAuth device-flow configuration (verified against live captures).

View Source
const (
	DeviceSelectAccountsPath = "/device/selectAccounts"
	DeviceTokenPollPath      = "/api/v1/deviceToken/poll"
	DeviceTokenRefreshPath   = "/api/v1/deviceToken/refresh"
	JobTokenPath             = "/api/v1/me/jobToken"
	JobTokenRefreshPath      = "/api/v1/jobToken/refresh"

	// ChatPath is the SSE chat endpoint. The query asks for the LLM result
	// to be fetched, names the common agent, and says Encode=1 to use the
	// custom base64 body codec.
	ChatPath = "/algo/api/v2/service/pro/sse/agent_chat_generation?FetchKeys=llm_model_result&AgentId=agent_common&Encode=1"

	// ListModelsPath is the live model listing endpoint.
	ListModelsPath = "/algo/api/v2/model/list?Encode=1"

	// AccountUsagePath is the desktop client's account usage endpoint.
	AccountUsagePath = "/sash/api/v2/me/usage"
)

API endpoints.

View Source
const (
	// BodyAlphabet is the custom base64 alphabet.
	BodyAlphabet = "_doRTgHZBKcGVjlvpC,@aFSx#DPuNJme&i*MzLOEn)sUrthbf%Y^w.(kIQyXqWA!"
)
View Source
const ProviderKey = "qoder"

ProviderKey is how magpie names this subscription.

View Source
const RSAPublicKeyPEM = `` /* 271-byte string literal not displayed */

RSAPublicKeyPEM is Qoder's embedded 1024-bit RSA public key (from the client's cosy source), used to encrypt the per-request AES key that wraps the user blob in the COSY Authorization header.

View Source
const UserInfoPath = "/api/v1/userinfo"

UserInfoPath is the account endpoint, asked with the dt- device token (not the jt- job token). It answers who the signed-in account is.

Variables

This section is empty.

Functions

func BodyDecode

func BodyDecode(s string) []byte

BodyDecode decodes custom base64 (no third swap).

func BodyEncode

func BodyEncode(data []byte) string

BodyEncode encodes one plaintext into custom base64 (no third swap).

func BuildCosyHeaders

func BuildCosyHeaders(rawURL string, user *User, body string, timestamp int64) (map[string]string, error)

BuildCosyHeaders builds the full set of COSY request headers for a Qoder call to rawURL with body already in its wire (encoded) form.

func ChatURL

func ChatURL() string

ChatURL is the full chat endpoint on the inference host.

func DecodeRequestBody

func DecodeRequestBody(wire string) []byte

DecodeRequestBody reverses the swap, then decodes.

func EncodeRequestBody

func EncodeRequestBody(plaintext []byte) string

EncodeRequestBody is the wire body for an agent_chat_generation request: custom base64, then the outer-third swap.

func FetchModels

func FetchModels(ctx context.Context, client *http.Client, baseURL string, user *User) ([]byte, error)

FetchModels retrieves the raw model listing for a signed-in user. The request carries the same COSY envelope the client uses for every /algo call; an empty body is signed for a GET.

func FetchUsage

func FetchUsage(ctx context.Context, client *http.Client, baseURL, deviceToken string) (json.RawMessage, error)

FetchUsage retrieves the account usage envelope using a device token.

func IsRoutableModel

func IsRoutableModel(key string) bool

IsRoutableModel reports whether a model key names one model an agent can pick. Aggregate entries ("auto", "default") route inside Qoder and aren't.

func MachineOS

func MachineOS() string

MachineOS uses the architecture and platform names used by desktop clients.

func NewID

func NewID() string

NewID is a UUID without dashes, the shape Qoder's request ids and tool-call ids use.

func NewMachineID

func NewMachineID() string

NewMachineID generates the stable identity kept with one account.

func ParseModels

func ParseModels(body []byte) ([]catalog.Model, error)

ParseModels decodes the listing and returns the enabled, routable chat models as magpie's catalog entries. Placeholder ids ("auto", "default") are dropped: they route between models inside Qoder and aren't a single model an agent can pick.

Types

type Credential

type Credential struct {
	UID           string          `json:"uid"`
	Email         string          `json:"email,omitempty"`
	Name          string          `json:"name,omitempty"`
	Token         string          `json:"token"`
	RefreshToken  string          `json:"refresh_token"`
	DeviceToken   string          `json:"device_token"`
	DeviceRefresh string          `json:"device_refresh,omitempty"`
	ExpiresAt     int64           `json:"expires_at"`
	MachineID     string          `json:"machine_id"`
	Models        json.RawMessage `json:"models,omitempty"`
}

Credential is kept in the account's auth blob in magpie's logins.json.

func (Credential) Refresh

func (c Credential) Refresh(ctx context.Context, client *http.Client) (Credential, error)

Refresh returns a new credential value. The caller serializes the entire check, refresh and save operation, since refresh tokens rotate on use.

func (*Credential) Valid

func (c *Credential) Valid() bool

type DeviceFlow

type DeviceFlow struct {
	// contains filtered or unexported fields
}

DeviceFlow runs Qoder's sign-in: an authorization page the user opens, then a poll for the device token, then its exchange for the job token that serves models. It is a helper, not a state machine: the caller owns the browser and the waiting, and calls these one at a time.

func NewDeviceFlow

func NewDeviceFlow(client *http.Client) *DeviceFlow

NewDeviceFlow makes a flow; a nil client is a 20-second-timeout one.

func (*DeviceFlow) Authorization

func (f *DeviceFlow) Authorization() (authURL, verifier, nonce string, err error)

Authorization returns the page to open and the (verifier, nonce) the poll needs. It is a PKCE device flow: a random verifier is kept, its S256 digest is what the page sees.

func (*DeviceFlow) Client

func (f *DeviceFlow) Client() *http.Client

Client is the flow's HTTP client, reused for the model fetch after sign-in.

func (*DeviceFlow) JobToken

func (f *DeviceFlow) JobToken(ctx context.Context, deviceToken string) (*JobToken, error)

JobToken trades a device token for the job token the model calls use.

func (*DeviceFlow) MachineID

func (f *DeviceFlow) MachineID() string

func (*DeviceFlow) PollDeviceToken

func (f *DeviceFlow) PollDeviceToken(ctx context.Context, nonce, verifier string, interval time.Duration) (*DeviceToken, error)

PollDeviceToken asks for the device token until the user has authorized it, ctx ends the wait, or interval elapses between tries.

type DeviceToken

type DeviceToken struct {
	Token        string `json:"token"`
	DeviceToken  string `json:"device_token,omitempty"`
	RefreshToken string `json:"refresh_token"`
	UserID       string `json:"user_id"`
}

func RefreshDeviceToken

func RefreshDeviceToken(ctx context.Context, client *http.Client, endpoint, refreshToken string) (*DeviceToken, error)

RefreshDeviceToken rotates the device token used by account endpoints.

type DeviceTokenRefreshHTTPError

type DeviceTokenRefreshHTTPError struct {
	StatusCode int
}

DeviceTokenRefreshHTTPError preserves a rejected refresh status without response data.

func (*DeviceTokenRefreshHTTPError) Error

type JobToken

type JobToken struct {
	Token        string `json:"token"`
	RefreshToken string `json:"refresh_token"`
	// ExpiresIn is Qoder's lifetime for the token, in milliseconds.
	ExpiresIn int64 `json:"expires_in"`
}

JobToken is the jt- task token that authorizes the model calls.

func RefreshJobToken

func RefreshJobToken(ctx context.Context, client *http.Client, refreshToken string) (*JobToken, error)

RefreshJobToken trades a job token's refresh token for a new pair. The old refresh token is spent, so the caller must keep the new one.

func (JobToken) Expiry

func (r JobToken) Expiry() time.Duration

Expiry is the job token's lifetime as a duration; zero when Qoder said none.

type JobTokenRefreshHTTPError

type JobTokenRefreshHTTPError struct {
	StatusCode int
}

JobTokenRefreshHTTPError preserves a rejected job-token refresh status.

func (*JobTokenRefreshHTTPError) Error

func (e *JobTokenRefreshHTTPError) Error() string

type ModelInfo

type ModelInfo struct {
	Key            string          `json:"key"`
	Source         string          `json:"source,omitempty"`
	Enable         bool            `json:"enable"`
	DisplayName    string          `json:"display_name,omitempty"`
	IsVL           bool            `json:"is_vl,omitempty"`
	IsReasoning    bool            `json:"is_reasoning,omitempty"`
	MaxInputTokens int             `json:"max_input_tokens,omitempty"`
	Efforts        []string        `json:"reasoning_efforts,omitempty"`
	Config         json.RawMessage `json:"-"`
	// From thinking_config: whether the model can think, whether it can't
	// stop, and the effort it thinks at unless asked for another.
	Thinks        bool   `json:"-"`
	AlwaysThinks  bool   `json:"-"`
	DefaultEffort string `json:"-"`
	// Free is set on a model that costs the plan no credits, as Qoder's
	// client reads it: is_free, or a price_factor of 0.
	Free bool `json:"-"`
}

ModelInfo is one entry of the model-list response's "chat" array. Only the fields magpie uses are kept.

func ModelConfigs

func ModelConfigs(body []byte) ([]ModelInfo, error)

ModelConfigs preserves each enabled model's complete upstream configuration.

type UsageHTTPError

type UsageHTTPError struct {
	StatusCode int
}

UsageHTTPError preserves the upstream status without exposing response data.

func (*UsageHTTPError) Error

func (e *UsageHTTPError) Error() string

type User

type User struct {
	UID       string // the Qoder user id (x-gw-user-id / Cosy-User value)
	Name      string // display name, may be empty
	Email     string // account email, may be empty
	Token     string // the jt- Bearer token (security_oauth_token)
	MachineID string // generated once at sign-in, saved with the account
}

User is the signed-in Qoder account a COSY envelope is built for.

type UserInfo

type UserInfo struct {
	ID          string `json:"id"`
	Name        string `json:"name"`
	Username    string `json:"username"`
	Email       string `json:"email"`
	Avatar      string `json:"avatar"`
	Source      string `json:"source"`
	OrgName     string `json:"organization_name"`
	HighestTier bool   `json:"is_highest_tier"`
}

UserInfo is what the account endpoint says about a signed-in account.

func FetchUserInfo

func FetchUserInfo(ctx context.Context, client *http.Client, deviceToken string) (*UserInfo, error)

FetchUserInfo asks the account endpoint with a device token. The device token is short-lived but outlasts the sign-in that just made it, so this runs during the flow while it is fresh.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL