Changeset 0.29.0 (#195)
This commit is contained in:
@@ -6,6 +6,8 @@
|
||||
//
|
||||
// The CompletionHandler methods (resolveConfig, buildToolDefs) remain as
|
||||
// thin wrappers for backward compat — existing callers are unchanged.
|
||||
//
|
||||
// v0.29.0-cs7a: Replaced raw SQL with store methods.
|
||||
package handlers
|
||||
|
||||
import (
|
||||
@@ -16,7 +18,6 @@ import (
|
||||
"log"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/crypto"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/database"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/providers"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/tools"
|
||||
@@ -41,17 +42,16 @@ type ProviderResolution struct {
|
||||
//
|
||||
// The vault is used to decrypt API keys. Pass nil for unencrypted fallback.
|
||||
func ResolveProviderConfig(
|
||||
stores store.Stores,
|
||||
vault *crypto.KeyResolver,
|
||||
userID, channelID, providerConfigID, modelID string,
|
||||
) (ProviderResolution, error) {
|
||||
ctx := context.Background()
|
||||
configID := providerConfigID
|
||||
|
||||
// 2. Config from channel
|
||||
if configID == "" && channelID != "" {
|
||||
var channelConfigID *string
|
||||
err := database.DB.QueryRow(
|
||||
database.Q(`SELECT provider_config_id FROM channels WHERE id = $1`), channelID,
|
||||
).Scan(&channelConfigID)
|
||||
channelConfigID, err := stores.Channels.GetProviderConfigID(ctx, channelID)
|
||||
if err == nil && channelConfigID != nil {
|
||||
configID = *channelConfigID
|
||||
}
|
||||
@@ -59,47 +59,15 @@ func ResolveProviderConfig(
|
||||
|
||||
// 3. User's first active config (personal first, then global — excludes team providers)
|
||||
if configID == "" {
|
||||
err := database.DB.QueryRow(database.Q(`
|
||||
SELECT id FROM provider_configs
|
||||
WHERE is_active = true AND (
|
||||
(scope = 'personal' AND owner_id = $1)
|
||||
OR scope = 'global'
|
||||
)
|
||||
ORDER BY scope ASC, created_at ASC
|
||||
LIMIT 1
|
||||
`), userID).Scan(&configID)
|
||||
var err error
|
||||
configID, err = stores.Providers.FindFirstForUser(ctx, userID)
|
||||
if err != nil {
|
||||
return ProviderResolution{}, fmt.Errorf("no API config found — add one at /api-configs")
|
||||
}
|
||||
}
|
||||
|
||||
// Load the config — allow personal, global, OR team configs the user belongs to
|
||||
var providerID, endpoint string
|
||||
var providerScope string
|
||||
var modelDefault *string
|
||||
var apiKeyEnc, keyNonce []byte
|
||||
var keyScope string
|
||||
var customHeadersJSON, providerSettingsJSON []byte
|
||||
var proxyMode string
|
||||
var proxyURL *string
|
||||
|
||||
// $2/userID appears twice in the query; Postgres reuses positional params,
|
||||
// SQLite needs each ? bound separately.
|
||||
configArgs := []interface{}{configID, userID}
|
||||
if database.IsSQLite() {
|
||||
configArgs = append(configArgs, userID)
|
||||
}
|
||||
err := database.DB.QueryRow(database.Q(`
|
||||
SELECT provider, endpoint, scope, api_key_enc, key_nonce, key_scope,
|
||||
model_default, headers, settings, COALESCE(proxy_mode, 'system'), proxy_url
|
||||
FROM provider_configs
|
||||
WHERE id = $1 AND is_active = true
|
||||
AND (scope = 'global'
|
||||
OR (scope = 'personal' AND owner_id = $2)
|
||||
OR (scope = 'team' AND owner_id IN (SELECT team_id FROM team_members WHERE user_id = $2)))
|
||||
`), configArgs...).Scan(&providerID, &endpoint, &providerScope, &apiKeyEnc, &keyNonce, &keyScope,
|
||||
&modelDefault, &customHeadersJSON, &providerSettingsJSON, &proxyMode, &proxyURL)
|
||||
|
||||
cfg, err := stores.Providers.LoadAccessible(ctx, configID, userID)
|
||||
if err == sql.ErrNoRows {
|
||||
return ProviderResolution{}, fmt.Errorf("API config not found or not accessible")
|
||||
}
|
||||
@@ -109,8 +77,8 @@ func ResolveProviderConfig(
|
||||
|
||||
// Resolve model: explicit > config default
|
||||
model := modelID
|
||||
if model == "" && modelDefault != nil {
|
||||
model = *modelDefault
|
||||
if model == "" && cfg.ModelDefault != "" {
|
||||
model = cfg.ModelDefault
|
||||
}
|
||||
if model == "" {
|
||||
return ProviderResolution{}, fmt.Errorf("no model specified and no default model in config")
|
||||
@@ -118,10 +86,10 @@ func ResolveProviderConfig(
|
||||
|
||||
// Decrypt API key using the appropriate tier
|
||||
key := ""
|
||||
if len(apiKeyEnc) > 0 {
|
||||
if len(cfg.APIKeyEnc) > 0 {
|
||||
if vault != nil {
|
||||
var err error
|
||||
key, err = vault.Decrypt(apiKeyEnc, keyNonce, keyScope, userID)
|
||||
key, err = vault.Decrypt(cfg.APIKeyEnc, cfg.KeyNonce, cfg.KeyScope, userID)
|
||||
if err != nil {
|
||||
if err == crypto.ErrVaultLocked {
|
||||
return ProviderResolution{}, fmt.Errorf("personal vault is locked — please log in again")
|
||||
@@ -130,40 +98,47 @@ func ResolveProviderConfig(
|
||||
}
|
||||
} else {
|
||||
// No vault — key stored as raw bytes (unencrypted fallback)
|
||||
key = string(apiKeyEnc)
|
||||
key = string(cfg.APIKeyEnc)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse custom headers
|
||||
customHeaders := make(map[string]string)
|
||||
if customHeadersJSON != nil {
|
||||
_ = json.Unmarshal(customHeadersJSON, &customHeaders)
|
||||
if cfg.Headers != nil {
|
||||
b, _ := json.Marshal(cfg.Headers)
|
||||
_ = json.Unmarshal(b, &customHeaders)
|
||||
}
|
||||
|
||||
// Parse provider-specific settings
|
||||
providerSettings := make(map[string]interface{})
|
||||
if providerSettingsJSON != nil {
|
||||
_ = json.Unmarshal(providerSettingsJSON, &providerSettings)
|
||||
if cfg.Settings != nil {
|
||||
for k, v := range cfg.Settings {
|
||||
providerSettings[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
proxyMode := cfg.ProxyMode
|
||||
if proxyMode == "" {
|
||||
proxyMode = "system"
|
||||
}
|
||||
proxyURLStr := ""
|
||||
if proxyURL != nil {
|
||||
proxyURLStr = *proxyURL
|
||||
if cfg.ProxyURL != nil {
|
||||
proxyURLStr = *cfg.ProxyURL
|
||||
}
|
||||
|
||||
return ProviderResolution{
|
||||
Config: providers.ProviderConfig{
|
||||
Endpoint: endpoint,
|
||||
Endpoint: cfg.Endpoint,
|
||||
APIKey: key,
|
||||
CustomHeaders: customHeaders,
|
||||
Settings: providerSettings,
|
||||
ProxyMode: proxyMode,
|
||||
ProxyURL: proxyURLStr,
|
||||
},
|
||||
ProviderID: providerID,
|
||||
ProviderID: cfg.Provider,
|
||||
Model: model,
|
||||
ConfigID: configID,
|
||||
ProviderScope: providerScope,
|
||||
ConfigID: cfg.ID,
|
||||
ProviderScope: cfg.Scope,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user