feat: micro go cli

This commit is contained in:
Ran
2026-01-29 02:55:21 +07:00
parent 0e56c85233
commit 31cb75702c
13 changed files with 475 additions and 3403 deletions
+11 -2
View File
@@ -8,8 +8,17 @@ export interface ErrorResponse {
}
export const errorMiddleware = new Elysia({ name: 'error' })
.onError(({ code, error, set }) => {
console.error('[Error]', code, error)
.onError(({ code, error, set, request }) => {
const url = new URL(request.url)
const status = set.status ?? 500
const message = error instanceof Error ? error.message : String(error)
console.error('[Error]', {
method: request.method,
path: url.pathname,
code,
status,
message,
})
switch (code) {
case 'VALIDATION':
+57 -20
View File
@@ -37,6 +37,12 @@ const ScheduleBody = z.object({
export const chatModule = new Elysia({ prefix: '/chat' })
.post('/', async ({ body }) => {
console.log('[Chat] request', {
type: 'chat',
clientType: body.clientType,
model: body.model,
baseUrl: body.baseUrl,
})
const { ask } = createAgent({
apiKey: body.apiKey,
baseUrl: body.baseUrl,
@@ -49,14 +55,33 @@ export const chatModule = new Elysia({ prefix: '/chat' })
platforms: body.platforms,
currentPlatform: body.currentPlatform,
})
return await ask({
messages: body.messages as unknown as ModelMessage[],
query: body.query,
})
try {
const result = await ask({
messages: body.messages as unknown as ModelMessage[],
query: body.query,
})
console.log('[Chat] response', { type: 'chat', messages: result.messages?.length ?? 0 })
return result
} catch (error) {
console.error('[Chat] error', {
type: 'chat',
clientType: body.clientType,
model: body.model,
baseUrl: body.baseUrl,
error,
})
throw error
}
}, {
body: ChatBody,
})
.post('/stream', async function* ({ body }) {
console.log('[Chat] request', {
type: 'stream',
clientType: body.clientType,
model: body.model,
baseUrl: body.baseUrl,
})
const { stream } = createAgent({
apiKey: body.apiKey,
baseUrl: body.baseUrl,
@@ -69,23 +94,35 @@ export const chatModule = new Elysia({ prefix: '/chat' })
platforms: body.platforms,
currentPlatform: body.currentPlatform,
})
const streanGenerator = stream({
messages: body.messages as unknown as ModelMessage[],
query: body.query,
})
while (true) {
const chunk = await streanGenerator.next()
if (chunk.done) {
yield sse({
type: 'done',
data: chunk.value,
})
break
}
yield sse({
type: 'delta',
data: chunk.value
try {
const streanGenerator = stream({
messages: body.messages as unknown as ModelMessage[],
query: body.query,
})
while (true) {
const chunk = await streanGenerator.next()
if (chunk.done) {
console.log('[Chat] response', { type: 'stream', messages: chunk.value?.messages?.length ?? 0 })
yield sse({
type: 'done',
data: chunk.value,
})
break
}
yield sse({
type: 'delta',
data: chunk.value
})
}
} catch (error) {
console.error('[Chat] error', {
type: 'stream',
clientType: body.clientType,
model: body.model,
baseUrl: body.baseUrl,
error,
})
throw error
}
}, {
body: ChatBody,
+357
View File
@@ -0,0 +1,357 @@
package main
import (
"bufio"
"bytes"
"context"
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"time"
"github.com/memohai/memoh/internal/chat"
"github.com/memohai/memoh/internal/config"
)
type cliOptions struct {
configPath string
username string
password string
timeout time.Duration
apiBaseURL string
jwtToken string
}
func main() {
opts := parseFlags()
ctx := context.Background()
cfg, err := config.Load(opts.configPath)
if err != nil {
log.Fatalf("load config: %v", err)
}
if strings.TrimSpace(opts.apiBaseURL) == "" {
opts.apiBaseURL = defaultAPIBaseURL(cfg.Server.Addr)
}
if strings.TrimSpace(opts.apiBaseURL) == "" {
log.Fatalf("api url is required")
}
opts.apiBaseURL = normalizeBaseURL(opts.apiBaseURL)
jwtToken := strings.TrimSpace(opts.jwtToken)
client := &http.Client{Timeout: opts.timeout}
if jwtToken == "" {
username, password, err := resolveLoginCredentials(opts, cfg)
if err != nil {
log.Fatalf("resolve login: %v", err)
}
loginCtx := ctx
if opts.timeout > 0 {
var cancel context.CancelFunc
loginCtx, cancel = context.WithTimeout(ctx, opts.timeout)
defer cancel()
}
jwtToken, err = resolveJWTToken(loginCtx, client, opts.apiBaseURL, username, password)
if err != nil {
log.Fatalf("resolve jwt: %v", err)
}
}
query := strings.TrimSpace(strings.Join(flag.Args(), " "))
if query != "" {
if err := sendChat(ctx, client, opts.apiBaseURL, jwtToken, query); err != nil {
log.Fatalf("chat failed: %v", err)
}
return
}
if err := runInteractive(ctx, client, opts.apiBaseURL, jwtToken); err != nil {
log.Fatalf("chat failed: %v", err)
}
}
func parseFlags() cliOptions {
var opts cliOptions
defaultConfig := os.Getenv("CONFIG_PATH")
if strings.TrimSpace(defaultConfig) == "" {
defaultConfig = config.DefaultConfigPath
}
flag.StringVar(&opts.configPath, "config", defaultConfig, "Path to config.toml")
flag.StringVar(&opts.username, "username", "", "Username for login")
flag.StringVar(&opts.password, "password", "", "Password for login (or set MEMOH_PASSWORD)")
flag.StringVar(&opts.jwtToken, "jwt", "", "JWT token (optional)")
flag.StringVar(&opts.apiBaseURL, "api-url", "", "API server base URL (e.g. http://127.0.0.1:8080)")
flag.DurationVar(&opts.timeout, "timeout", 30*time.Second, "Request timeout")
flag.Parse()
return opts
}
func normalizeBaseURL(value string) string {
return strings.TrimRight(strings.TrimSpace(value), "/")
}
func defaultAPIBaseURL(addr string) string {
trimmed := strings.TrimSpace(addr)
if trimmed == "" {
return ""
}
if strings.HasPrefix(trimmed, "http://") || strings.HasPrefix(trimmed, "https://") {
return normalizeBaseURL(trimmed)
}
if strings.HasPrefix(trimmed, ":") {
return "http://127.0.0.1" + trimmed
}
return "http://" + trimmed
}
func resolveLoginCredentials(opts cliOptions, cfg config.Config) (string, string, error) {
username := strings.TrimSpace(opts.username)
if username == "" {
username = strings.TrimSpace(cfg.Admin.Username)
}
if username == "" {
return "", "", fmt.Errorf("username is required for login")
}
password := strings.TrimSpace(opts.password)
if password == "" {
password = strings.TrimSpace(os.Getenv("MEMOH_PASSWORD"))
}
if password == "" {
if candidate := strings.TrimSpace(cfg.Admin.Password); candidate != "" && candidate != "change-your-password-here" {
password = candidate
}
}
if password == "" {
return "", "", fmt.Errorf("password is required; pass --password or set MEMOH_PASSWORD")
}
return username, password, nil
}
func resolveJWTToken(ctx context.Context, client *http.Client, baseURL, username, password string) (string, error) {
resp, err := loginForToken(ctx, client, baseURL, username, password)
if err != nil {
return "", err
}
if strings.TrimSpace(resp.AccessToken) == "" {
return "", fmt.Errorf("login succeeded but token missing")
}
return resp.AccessToken, nil
}
type loginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
type loginResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresAt string `json:"expires_at"`
UserID string `json:"user_id"`
Username string `json:"username"`
}
func loginForToken(ctx context.Context, client *http.Client, baseURL, username, password string) (loginResponse, error) {
body, err := json.Marshal(loginRequest{
Username: username,
Password: password,
})
if err != nil {
return loginResponse{}, err
}
url := normalizeBaseURL(baseURL) + "/auth/login"
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil {
return loginResponse{}, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return loginResponse{}, err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
payload, _ := io.ReadAll(resp.Body)
return loginResponse{}, fmt.Errorf("login failed: %s", strings.TrimSpace(string(payload)))
}
var parsed loginResponse
if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil {
return loginResponse{}, err
}
return parsed, nil
}
func runInteractive(ctx context.Context, client *http.Client, baseURL, jwtToken string) error {
reader := bufio.NewScanner(os.Stdin)
reader.Buffer(make([]byte, 0, 64*1024), 2*1024*1024)
fmt.Fprint(os.Stdout, "You: ")
for reader.Scan() {
line := strings.TrimSpace(reader.Text())
if line == "" {
fmt.Fprint(os.Stdout, "You: ")
continue
}
lower := strings.ToLower(line)
if lower == "exit" || lower == "quit" {
return nil
}
if err := sendChat(ctx, client, baseURL, jwtToken, line); err != nil {
return err
}
fmt.Fprint(os.Stdout, "You: ")
}
return reader.Err()
}
func sendChat(ctx context.Context, client *http.Client, baseURL, jwtToken, query string) error {
return streamAPIChat(ctx, client, baseURL, jwtToken, chat.ChatRequest{Query: query})
}
func streamAPIChat(ctx context.Context, client *http.Client, baseURL, jwtToken string, req chat.ChatRequest) error {
body, err := json.Marshal(req)
if err != nil {
return err
}
url := baseURL + "/chat/stream"
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil {
return err
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Accept", "text/event-stream")
httpReq.Header.Set("Authorization", "Bearer "+jwtToken)
resp, err := client.Do(httpReq)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
payload, _ := io.ReadAll(resp.Body)
return fmt.Errorf("api server error: %s", strings.TrimSpace(string(payload)))
}
scanner := bufio.NewScanner(resp.Body)
scanner.Buffer(make([]byte, 0, 64*1024), 2*1024*1024)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "event:") {
continue
}
if !strings.HasPrefix(line, "data:") {
continue
}
data := strings.TrimSpace(strings.TrimPrefix(line, "data:"))
if data == "" || data == "[DONE]" {
break
}
if text, ok := extractStreamText([]byte(data)); ok {
fmt.Fprint(os.Stdout, text)
}
}
if err := scanner.Err(); err != nil {
return err
}
fmt.Fprintln(os.Stdout)
return nil
}
func renderMessageContent(raw interface{}) (string, bool) {
switch v := raw.(type) {
case string:
return v, true
case []interface{}:
parts := make([]string, 0, len(v))
for _, item := range v {
if text, ok := renderMessageContent(item); ok && strings.TrimSpace(text) != "" {
parts = append(parts, text)
}
}
if len(parts) > 0 {
return strings.Join(parts, ""), true
}
case map[string]interface{}:
if text, ok := v["text"].(string); ok && strings.TrimSpace(text) != "" {
return text, true
}
if text, ok := v["content"].(string); ok && strings.TrimSpace(text) != "" {
return text, true
}
if kind, ok := v["type"].(string); ok && kind == "text" {
if text, ok := v["text"].(string); ok {
return text, true
}
}
}
return "", false
}
func extractStreamText(raw []byte) (string, bool) {
var payload interface{}
if err := json.Unmarshal(raw, &payload); err != nil {
return "", false
}
return extractTextFromPayload(payload)
}
func extractTextFromPayload(payload interface{}) (string, bool) {
switch v := payload.(type) {
case string:
if strings.TrimSpace(v) == "" {
return "", false
}
return v, true
case []interface{}:
parts := make([]string, 0, len(v))
for _, item := range v {
if text, ok := extractTextFromPayload(item); ok && strings.TrimSpace(text) != "" {
parts = append(parts, text)
}
}
if len(parts) > 0 {
return strings.Join(parts, ""), true
}
case map[string]interface{}:
if text, ok := v["textDelta"].(string); ok && strings.TrimSpace(text) != "" {
return text, true
}
if text, ok := v["text"].(string); ok && strings.TrimSpace(text) != "" {
return text, true
}
if content, ok := v["content"]; ok {
if text, ok := renderMessageContent(content); ok {
return text, true
}
}
if delta, ok := v["delta"]; ok {
if text, ok := extractTextFromPayload(delta); ok {
return text, true
}
}
if data, ok := v["data"]; ok {
if text, ok := extractTextFromPayload(data); ok {
return text, true
}
}
if msg, ok := v["message"]; ok {
if text, ok := extractTextFromPayload(msg); ok {
return text, true
}
}
}
return "", false
}
+4 -12
View File
@@ -959,20 +959,12 @@ const docTemplate = `{
},
"/models/enable-as/{enableAs}": {
"get": {
"description": "Get the model that is enabled for a specific purpose (chat, memory, embedding)\nGet the default model configured for a specific purpose (chat, memory, or embedding)",
"description": "Get the model that is enabled for a specific purpose (chat, memory, embedding)",
"tags": [
"models",
"models"
],
"summary": "Get default model by enable_as",
"summary": "Get model by enable_as",
"parameters": [
{
"type": "string",
"description": "Enable as value (chat, memory, embedding)",
"name": "enableAs",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Enable as value (chat, memory, embedding)",
@@ -2499,11 +2491,11 @@ const docTemplate = `{
// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{
Version: "",
Version: "1.0.0",
Host: "",
BasePath: "",
Schemes: []string{},
Title: "",
Title: "Memoh API",
Description: "",
InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate,
+5 -11
View File
@@ -1,7 +1,9 @@
{
"swagger": "2.0",
"info": {
"contact": {}
"title": "Memoh API",
"contact": {},
"version": "1.0.0"
},
"paths": {
"/auth/login": {
@@ -948,20 +950,12 @@
},
"/models/enable-as/{enableAs}": {
"get": {
"description": "Get the model that is enabled for a specific purpose (chat, memory, embedding)\nGet the default model configured for a specific purpose (chat, memory, or embedding)",
"description": "Get the model that is enabled for a specific purpose (chat, memory, embedding)",
"tags": [
"models",
"models"
],
"summary": "Get default model by enable_as",
"summary": "Get model by enable_as",
"parameters": [
{
"type": "string",
"description": "Enable as value (chat, memory, embedding)",
"name": "enableAs",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Enable as value (chat, memory, embedding)",
+5 -10
View File
@@ -499,6 +499,8 @@ definitions:
type: object
info:
contact: {}
title: Memoh API
version: 1.0.0
paths:
/auth/login:
post:
@@ -1210,15 +1212,9 @@ paths:
- models
/models/enable-as/{enableAs}:
get:
description: |-
Get the model that is enabled for a specific purpose (chat, memory, embedding)
Get the default model configured for a specific purpose (chat, memory, or embedding)
description: Get the model that is enabled for a specific purpose (chat, memory,
embedding)
parameters:
- description: Enable as value (chat, memory, embedding)
in: path
name: enableAs
required: true
type: string
- description: Enable as value (chat, memory, embedding)
in: path
name: enableAs
@@ -1241,10 +1237,9 @@ paths:
description: Internal Server Error
schema:
$ref: '#/definitions/handlers.ErrorResponse'
summary: Get default model by enable_as
summary: Get model by enable_as
tags:
- models
- models
/models/model/{modelId}:
delete:
description: Delete a model configuration by its model_id field (e.g., gpt-4)
-13
View File
@@ -7,7 +7,6 @@ require (
github.com/containerd/containerd/api v1.10.0
github.com/containerd/containerd/v2 v2.2.1
github.com/containerd/errdefs v1.0.0
github.com/firebase/genkit/go v1.4.0
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.8.0
@@ -25,8 +24,6 @@ require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/Microsoft/hcsshim v0.14.0-rc.1 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/containerd/cgroups/v3 v3.1.2 // indirect
github.com/containerd/continuity v0.4.5 // indirect
@@ -53,22 +50,17 @@ require (
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
github.com/goccy/go-yaml v1.17.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/dotprompt/go v0.0.0-20251014011017-8d056e027254 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/jsonschema-go v0.3.0 // indirect
github.com/invopop/jsonschema v0.13.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/klauspost/compress v1.18.3 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mbleigh/raymond v0.0.0-20250414171441-6b3a58ab9e0a // indirect
github.com/moby/locker v1.0.1 // indirect
github.com/moby/sys/mountinfo v0.7.2 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
@@ -83,17 +75,12 @@ require (
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.32.0 // indirect
+1 -28
View File
@@ -10,10 +10,6 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/Microsoft/hcsshim v0.14.0-rc.1 h1:qAPXKwGOkVn8LlqgBN8GS0bxZ83hOJpcjxzmlQKxKsQ=
github.com/Microsoft/hcsshim v0.14.0-rc.1/go.mod h1:hTKFGbnDtQb1wHiOWv4v0eN+7boSWAHyK/tNAaYZL0c=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -57,8 +53,6 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/firebase/genkit/go v1.4.0 h1:CP1hNWk7z0hosyY53zMH6MFKFO1fMLtj58jGPllQo6I=
github.com/firebase/genkit/go v1.4.0/go.mod h1:HX6m7QOaGc3MDNr/DrpQZrzPLzxeuLxrkTvfFtCYlGw=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -70,7 +64,7 @@ github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmG
github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4=
github.com/go-openapi/spec v0.22.3 h1:qRSmj6Smz2rEBxMnLRBMeBWxbbOvuOoElvSvObIgwQc=
github.com/go-openapi/spec v0.22.3/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
@@ -91,8 +85,6 @@ github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxE
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY=
github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
@@ -113,8 +105,6 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/dotprompt/go v0.0.0-20251014011017-8d056e027254 h1:okN800+zMJOGHLJCgry+OGzhhtH6YrjQh1rluHmOacE=
github.com/google/dotprompt/go v0.0.0-20251014011017-8d056e027254/go.mod h1:k8cjJAQWc//ac/bMnzItyOFbfT01tgRTZGgxELCuxEQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -128,8 +118,6 @@ github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
@@ -152,14 +140,10 @@ github.com/labstack/echo/v4 v4.15.0 h1:hoRTKWcnR5STXZFe9BmYun9AMTNeSbjHi2vtDuADJ
github.com/labstack/echo/v4 v4.15.0/go.mod h1:xmw1clThob0BSVRX1CRQkGQ/vjwcpOMjQZSZa9fKA/c=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mbleigh/raymond v0.0.0-20250414171441-6b3a58ab9e0a h1:v2cBA3xWKv2cIOVhnzX/gNgkNXqiHfUgJtA3r61Hf7A=
github.com/mbleigh/raymond v0.0.0-20250414171441-6b3a58ab9e0a/go.mod h1:Y6ghKH+ZijXn5d9E7qGGZBmjitx7iitZdQiIW97EpTU=
github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
@@ -212,15 +196,6 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -241,8 +216,6 @@ go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2W
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+16 -7
View File
@@ -62,6 +62,7 @@ func (r *Resolver) Chat(ctx context.Context, req ChatRequest) (ChatResponse, err
if strings.TrimSpace(req.UserID) == "" {
return ChatResponse{}, fmt.Errorf("user id is required")
}
skipHistory := req.MaxContextLoadTime < 0
chatModel, provider, err := r.selectChatModel(ctx, req)
if err != nil {
@@ -83,9 +84,12 @@ func (r *Resolver) Chat(ctx context.Context, req ChatRequest) (ChatResponse, err
language = req.Language
}
messages, err := r.loadHistoryMessages(ctx, req.UserID, maxContextLoadTime)
if err != nil {
return ChatResponse{}, err
var messages []GatewayMessage
if !skipHistory {
messages, err = r.loadHistoryMessages(ctx, req.UserID, maxContextLoadTime)
if err != nil {
return ChatResponse{}, err
}
}
if len(req.Messages) > 0 {
messages = append(messages, req.Messages...)
@@ -142,6 +146,7 @@ func (r *Resolver) StreamChat(ctx context.Context, req ChatRequest) (<-chan Stre
errChan <- fmt.Errorf("user id is required")
return
}
skipHistory := req.MaxContextLoadTime < 0
chatModel, provider, err := r.selectChatModel(ctx, req)
if err != nil {
@@ -166,10 +171,13 @@ func (r *Resolver) StreamChat(ctx context.Context, req ChatRequest) (<-chan Stre
language = req.Language
}
messages, err := r.loadHistoryMessages(ctx, req.UserID, maxContextLoadTime)
if err != nil {
errChan <- err
return
var messages []GatewayMessage
if !skipHistory {
messages, err = r.loadHistoryMessages(ctx, req.UserID, maxContextLoadTime)
if err != nil {
errChan <- err
return
}
}
if len(req.Messages) > 0 {
messages = append(messages, req.Messages...)
@@ -225,6 +233,7 @@ func (r *Resolver) postChat(ctx context.Context, payload agentGatewayRequest) (a
return agentGatewayResponse{}, err
}
url := r.gatewayBaseURL + "/chat"
fmt.Println("url", url)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil {
return agentGatewayResponse{}, err
-9
View File
@@ -257,15 +257,6 @@ func (h *ModelsHandler) DeleteByModelID(c echo.Context) error {
// @Failure 404 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /models/enable-as/{enableAs} [get]
// GetByEnableAs godoc
// @Summary Get default model by enable_as
// @Description Get the default model configured for a specific purpose (chat, memory, or embedding)
// @Tags models
// @Param enableAs path string true "Enable as value (chat, memory, embedding)"
// @Success 200 {object} models.GetResponse
// @Failure 400 {object} ErrorResponse
// @Failure 404 {object} ErrorResponse
// @Router /models/enable-as/{enableAs} [get]
func (h *ModelsHandler) GetByEnableAs(c echo.Context) error {
enableAs := c.Param("enableAs")
if enableAs == "" {
+3
View File
@@ -1,5 +1,8 @@
package handlers
// @title Memoh API
// @version 1.0.0
import (
"net/http"
"os"
-1
View File
@@ -18,7 +18,6 @@
},
"dependencies": {
"@elysiajs/eden": "^1.4.6",
"@memoh/api": "workspace:*",
"@memoh/shared": "workspace:*",
"elysia": "latest",
"commander": "^12.1.0",
+16 -3290
View File
File diff suppressed because it is too large Load Diff