feat: implement AI gateway phase one runtime
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
package clients
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func credential(candidate map[string]any, keys ...string) string {
|
||||
for _, key := range keys {
|
||||
if value, ok := candidate[key].(string); ok && strings.TrimSpace(value) != "" {
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func boolValue(body map[string]any, key string) bool {
|
||||
value, _ := body[key].(bool)
|
||||
return value
|
||||
}
|
||||
|
||||
func stringValue(body map[string]any, key string) string {
|
||||
value, _ := body[key].(string)
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
|
||||
func intValue(body map[string]any, key string, fallback int) int {
|
||||
switch value := body[key].(type) {
|
||||
case float64:
|
||||
return int(math.Round(value))
|
||||
case int:
|
||||
return value
|
||||
default:
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
|
||||
func decodeHTTPResponse(resp *http.Response) (map[string]any, error) {
|
||||
defer resp.Body.Close()
|
||||
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 16*1024*1024))
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return nil, &ClientError{
|
||||
Code: statusCodeName(resp.StatusCode),
|
||||
Message: errorMessage(raw, resp.Status),
|
||||
StatusCode: resp.StatusCode,
|
||||
Retryable: HTTPRetryable(resp.StatusCode),
|
||||
}
|
||||
}
|
||||
var out map[string]any
|
||||
if len(raw) == 0 {
|
||||
return map[string]any{}, nil
|
||||
}
|
||||
if err := json.Unmarshal(raw, &out); err != nil {
|
||||
return nil, &ClientError{Code: "invalid_response", Message: err.Error(), Retryable: false}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func usageFromOpenAI(result map[string]any) Usage {
|
||||
usage, _ := result["usage"].(map[string]any)
|
||||
input := intFromAny(firstPresent(usage["prompt_tokens"], usage["input_tokens"]))
|
||||
output := intFromAny(firstPresent(usage["completion_tokens"], usage["output_tokens"]))
|
||||
total := intFromAny(usage["total_tokens"])
|
||||
if total == 0 {
|
||||
total = input + output
|
||||
}
|
||||
return Usage{InputTokens: input, OutputTokens: output, TotalTokens: total}
|
||||
}
|
||||
|
||||
func intFromAny(value any) int {
|
||||
switch typed := value.(type) {
|
||||
case float64:
|
||||
return int(math.Round(typed))
|
||||
case int:
|
||||
return typed
|
||||
case int64:
|
||||
return int(typed)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func firstPresent(values ...any) any {
|
||||
for _, value := range values {
|
||||
if value != nil {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func errorMessage(raw []byte, fallback string) string {
|
||||
if len(raw) == 0 {
|
||||
return fallback
|
||||
}
|
||||
var parsed map[string]any
|
||||
if json.Unmarshal(raw, &parsed) == nil {
|
||||
if errObj, ok := parsed["error"].(map[string]any); ok {
|
||||
if message, ok := errObj["message"].(string); ok {
|
||||
return message
|
||||
}
|
||||
}
|
||||
if message, ok := parsed["message"].(string); ok {
|
||||
return message
|
||||
}
|
||||
}
|
||||
return string(raw)
|
||||
}
|
||||
|
||||
func statusCodeName(status int) string {
|
||||
switch status {
|
||||
case http.StatusTooManyRequests:
|
||||
return "rate_limit"
|
||||
case http.StatusRequestTimeout:
|
||||
return "timeout"
|
||||
case http.StatusUnauthorized, http.StatusForbidden:
|
||||
return "auth_failed"
|
||||
default:
|
||||
if status >= 500 {
|
||||
return "server_error"
|
||||
}
|
||||
return fmt.Sprintf("http_%d", status)
|
||||
}
|
||||
}
|
||||
|
||||
func nowUnix() int64 {
|
||||
return time.Now().Unix()
|
||||
}
|
||||
Reference in New Issue
Block a user