2026-03-25 21:39:14 +08:00
|
|
|
package cli
|
|
|
|
|
|
2026-03-26 21:07:06 +08:00
|
|
|
import (
|
2026-06-18 18:07:27 +08:00
|
|
|
"encoding/json"
|
2026-03-26 21:07:06 +08:00
|
|
|
"fmt"
|
2026-06-15 10:10:14 +08:00
|
|
|
"ragflow/internal/common"
|
2026-06-18 18:07:27 +08:00
|
|
|
"regexp"
|
2026-03-26 21:07:06 +08:00
|
|
|
"strconv"
|
2026-04-02 20:20:35 +08:00
|
|
|
"strings"
|
2026-03-26 21:07:06 +08:00
|
|
|
)
|
2026-03-25 21:39:14 +08:00
|
|
|
|
2026-06-08 11:49:37 +08:00
|
|
|
func tokenTypeDescription(t int, tok Token) string {
|
|
|
|
|
if tok.Type == t && tok.Value != "" {
|
|
|
|
|
return fmt.Sprintf("%s %q", tokenTypeToString(t), tok.Value)
|
|
|
|
|
}
|
|
|
|
|
return tokenTypeToString(t)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-25 21:39:14 +08:00
|
|
|
// Command parsers
|
2026-06-24 16:50:40 +08:00
|
|
|
func (p *Parser) parseAPILogout() (*Command, error) {
|
|
|
|
|
cmd := NewCommand("api_logout")
|
2026-03-26 11:54:23 +08:00
|
|
|
p.nextToken()
|
|
|
|
|
// Semicolon is optional for UNSET TOKEN
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-24 16:50:40 +08:00
|
|
|
func (p *Parser) parseAPILoginUser() (*Command, error) {
|
|
|
|
|
cmd := NewCommand("api_login_user")
|
2026-03-25 21:39:14 +08:00
|
|
|
|
|
|
|
|
p.nextToken() // consume LOGIN
|
|
|
|
|
if p.curToken.Type != TokenUser {
|
|
|
|
|
return nil, fmt.Errorf("expected USER after LOGIN")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
email, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["email"] = email
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
2026-04-07 09:44:51 +08:00
|
|
|
// Optional: PASSWORD 'password'
|
|
|
|
|
if p.curToken.Type == TokenPassword {
|
2026-04-01 16:16:25 +08:00
|
|
|
p.nextToken()
|
2026-06-24 16:50:40 +08:00
|
|
|
var password string
|
|
|
|
|
password, err = p.parseQuotedString()
|
2026-04-01 16:16:25 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["password"] = password
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-25 21:39:14 +08:00
|
|
|
// Semicolon is optional for UNSET TOKEN
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-24 16:50:40 +08:00
|
|
|
func (p *Parser) parseAPIPingServer() (*Command, error) {
|
|
|
|
|
cmd := NewCommand("api_ping_server")
|
2026-03-25 21:39:14 +08:00
|
|
|
p.nextToken()
|
|
|
|
|
// Semicolon is optional for UNSET TOKEN
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-24 16:50:40 +08:00
|
|
|
func (p *Parser) parseAPIRegisterCommand() (*Command, error) {
|
|
|
|
|
cmd := NewCommand("api_register_user")
|
2026-03-25 21:39:14 +08:00
|
|
|
|
|
|
|
|
if err := p.expectPeek(TokenUser); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-05-08 13:56:19 +08:00
|
|
|
p.nextToken() // consume USER
|
2026-03-25 21:39:14 +08:00
|
|
|
|
|
|
|
|
userName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["user_name"] = userName
|
2026-05-08 13:56:19 +08:00
|
|
|
p.nextToken() // consume Email
|
2026-03-25 21:39:14 +08:00
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenAs {
|
|
|
|
|
return nil, fmt.Errorf("expected AS")
|
|
|
|
|
}
|
2026-05-08 13:56:19 +08:00
|
|
|
p.nextToken() // consume AS
|
2026-03-25 21:39:14 +08:00
|
|
|
|
|
|
|
|
nickname, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["nickname"] = nickname
|
2026-05-08 13:56:19 +08:00
|
|
|
p.nextToken() // consume nickname
|
2026-03-25 21:39:14 +08:00
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenPassword {
|
|
|
|
|
return nil, fmt.Errorf("expected PASSWORD")
|
|
|
|
|
}
|
2026-05-08 13:56:19 +08:00
|
|
|
p.nextToken() // consume PASSWORD
|
2026-03-25 21:39:14 +08:00
|
|
|
|
|
|
|
|
password, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["password"] = password
|
2026-05-08 13:56:19 +08:00
|
|
|
p.nextToken() // consume 'password'
|
2026-03-25 21:39:14 +08:00
|
|
|
|
|
|
|
|
// Semicolon is optional for UNSET TOKEN
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-24 16:50:40 +08:00
|
|
|
// LIST CONFIGS;
|
2026-06-21 16:50:02 +08:00
|
|
|
// LIST PROVIDER 'provider_name' MODELS;
|
|
|
|
|
// LIST PROVIDER 'provider_name' INSTANCE 'instance_name' MODELS
|
|
|
|
|
// LIST MODELS;
|
2026-06-24 16:50:40 +08:00
|
|
|
func (p *Parser) parseAPIListCommands() (*Command, error) {
|
2026-03-25 21:39:14 +08:00
|
|
|
p.nextToken() // consume LIST
|
|
|
|
|
|
|
|
|
|
switch p.curToken.Type {
|
|
|
|
|
case TokenConfigs:
|
2026-06-24 16:50:40 +08:00
|
|
|
return p.parseAPIListConfigs()
|
2026-03-25 21:39:14 +08:00
|
|
|
case TokenDatasets:
|
2026-06-24 16:50:40 +08:00
|
|
|
return p.parseAPIListDatasets()
|
|
|
|
|
case TokenDataset:
|
2026-06-25 10:01:21 +08:00
|
|
|
return p.parseAPIListDatasetCommands()
|
2026-03-25 21:39:14 +08:00
|
|
|
case TokenAgents:
|
2026-06-24 16:50:40 +08:00
|
|
|
return p.parseAPIListAgents()
|
|
|
|
|
case TokenChats:
|
|
|
|
|
return p.parseAPIListChats()
|
|
|
|
|
case TokenSearches:
|
|
|
|
|
return p.parseAPIListSearches()
|
|
|
|
|
case TokenKeys:
|
|
|
|
|
return p.parseAPIListAPIKeys()
|
2026-04-21 16:52:32 +08:00
|
|
|
case TokenSupported:
|
|
|
|
|
return p.parseListModelsOfProvider()
|
2026-03-31 18:42:12 +08:00
|
|
|
case TokenModels:
|
|
|
|
|
return p.parseListModelsOfProvider()
|
|
|
|
|
case TokenProviders:
|
2026-06-24 18:48:09 +08:00
|
|
|
return p.parseAPIListProviders()
|
|
|
|
|
case TokenProvider:
|
|
|
|
|
return p.parseAPIListProviderCommands()
|
2026-06-12 14:56:44 +08:00
|
|
|
case TokenIngestion:
|
2026-06-25 10:01:21 +08:00
|
|
|
return p.parseAPIListIngestionTasks()
|
2026-03-25 21:39:14 +08:00
|
|
|
case TokenDefault:
|
2026-06-25 10:01:21 +08:00
|
|
|
return p.parseAPIListDefaultModels()
|
2026-03-31 18:42:12 +08:00
|
|
|
case TokenAvailable:
|
2026-06-24 18:48:09 +08:00
|
|
|
return p.parseAPIListAvailableProviders()
|
2026-03-25 21:39:14 +08:00
|
|
|
case TokenFiles:
|
|
|
|
|
return p.parseListFiles()
|
2026-05-15 12:29:52 +08:00
|
|
|
case TokenQuotedString:
|
|
|
|
|
return p.parseListQuotedStringCommand()
|
2026-06-09 15:22:50 +08:00
|
|
|
case TokenAPI:
|
|
|
|
|
return p.parseListApiCommand()
|
2026-03-25 21:39:14 +08:00
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("unknown LIST target: %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-24 16:50:40 +08:00
|
|
|
// LIST CONFIGS;
|
|
|
|
|
func (p *Parser) parseAPIListConfigs() (*Command, error) {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return NewCommand("api_list_configs"), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) parseAPIListDatasets() (*Command, error) {
|
|
|
|
|
cmd := NewCommand("api_list_datasets")
|
2026-03-25 21:39:14 +08:00
|
|
|
p.nextToken() // consume DATASETS
|
|
|
|
|
|
|
|
|
|
// Semicolon is optional for UNSET TOKEN
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-24 16:50:40 +08:00
|
|
|
// LIST DATASET 'dataset_name' DOCUMENTS;
|
2026-06-25 10:01:21 +08:00
|
|
|
func (p *Parser) parseAPIListDatasetCommands() (*Command, error) {
|
2026-06-24 16:50:40 +08:00
|
|
|
p.nextToken() // consume DATASET
|
2026-05-15 14:00:45 +08:00
|
|
|
|
|
|
|
|
datasetID, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
2026-06-25 10:01:21 +08:00
|
|
|
switch p.curToken.Type {
|
|
|
|
|
case TokenDocuments:
|
|
|
|
|
return p.parseAPIListDatasetDocuments(datasetID)
|
|
|
|
|
case TokenFiles:
|
|
|
|
|
return p.parseAPIListDatasetFiles(datasetID)
|
|
|
|
|
case TokenIngestion:
|
|
|
|
|
return p.parseAPIListDatasetIngestionTasks(datasetID)
|
|
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("unknown LIST target: %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) parseAPIListDatasetDocuments(datasetID string) (*Command, error) {
|
|
|
|
|
p.nextToken() // consume DOCUMENTS
|
|
|
|
|
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
2026-06-24 16:50:40 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("api_list_dataset_documents")
|
2026-05-15 14:00:45 +08:00
|
|
|
cmd.Params["dataset_id"] = datasetID
|
2026-06-25 10:01:21 +08:00
|
|
|
return cmd, nil
|
|
|
|
|
}
|
2026-05-15 14:00:45 +08:00
|
|
|
|
2026-06-25 10:01:21 +08:00
|
|
|
func (p *Parser) parseAPIListDatasetFiles(datasetName string) (*Command, error) {
|
|
|
|
|
p.nextToken() // consume FILES
|
|
|
|
|
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("api_list_dataset_files")
|
|
|
|
|
cmd.Params["dataset_name"] = datasetName
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) parseAPIListDatasetIngestionTasks(datasetName string) (*Command, error) {
|
|
|
|
|
p.nextToken() // consume INGESTION
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenTasks {
|
|
|
|
|
return nil, fmt.Errorf("expected TASKS")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Semicolon is optional
|
2026-05-15 14:00:45 +08:00
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
2026-06-25 10:01:21 +08:00
|
|
|
|
|
|
|
|
cmd := NewCommand("api_list_ingestion_tasks")
|
|
|
|
|
cmd.Params["dataset_name"] = datasetName
|
2026-05-15 14:00:45 +08:00
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-24 16:50:40 +08:00
|
|
|
func (p *Parser) parseAPIListAgents() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume AGENTS
|
|
|
|
|
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return NewCommand("api_list_agents"), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) parseAPIListChats() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume CHATS
|
|
|
|
|
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return NewCommand("api_list_chats"), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) parseAPIListSearches() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume SEARCHES
|
|
|
|
|
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return NewCommand("api_list_searches"), nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-08 11:49:37 +08:00
|
|
|
func (p *Parser) parseGetMetadata() (*Command, error) {
|
2026-05-20 20:32:06 +08:00
|
|
|
p.nextToken() // consume METADATA
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenOf {
|
|
|
|
|
return nil, fmt.Errorf("expected OF after METADATA")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenDataset {
|
|
|
|
|
return nil, fmt.Errorf("expected DATASET after OF")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Parse dataset names (space-separated)
|
|
|
|
|
var datasetNames []string
|
|
|
|
|
for {
|
|
|
|
|
name, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected dataset name: %w", err)
|
|
|
|
|
}
|
|
|
|
|
datasetNames = append(datasetNames, name)
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
2026-06-08 11:49:37 +08:00
|
|
|
|
|
|
|
|
if p.curToken.Type == TokenComma {
|
|
|
|
|
return nil, fmt.Errorf("syntax error: dataset names must be space-separated, not comma-separated (got %q after %q)", "'", name)
|
|
|
|
|
}
|
2026-05-20 20:32:06 +08:00
|
|
|
// Stop at semicolon or non-quoted (dataset name must be quoted)
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
// If next token is not a quoted string, stop parsing dataset names
|
|
|
|
|
if p.curToken.Type != TokenQuotedString {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-08 11:49:37 +08:00
|
|
|
cmd := NewCommand("get_metadata")
|
2026-05-20 20:32:06 +08:00
|
|
|
cmd.Params["dataset_names"] = datasetNames
|
|
|
|
|
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-24 16:50:40 +08:00
|
|
|
func (p *Parser) parseAPIListAPIKeys() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume KEYS
|
|
|
|
|
cmd := NewCommand("api_list_api_keys")
|
|
|
|
|
// Semicolon is optional
|
2026-03-25 21:39:14 +08:00
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-24 18:48:09 +08:00
|
|
|
// parseAPIListProviders parses LIST PROVIDERS command
|
|
|
|
|
func (p *Parser) parseAPIListProviders() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume PROVIDERS
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
2026-06-25 10:01:21 +08:00
|
|
|
return NewCommand("api_list_providers"), nil
|
2026-06-24 18:48:09 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// LIST PROVIDER 'provider_name' INSTANCES
|
|
|
|
|
func (p *Parser) parseAPIListProviderCommands() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume PROVIDER
|
|
|
|
|
|
|
|
|
|
providerName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
2026-03-25 21:39:14 +08:00
|
|
|
}
|
|
|
|
|
p.nextToken()
|
2026-06-24 18:48:09 +08:00
|
|
|
|
|
|
|
|
switch p.curToken.Type {
|
|
|
|
|
case TokenInstances:
|
|
|
|
|
return p.parseListProviderInstances(providerName)
|
|
|
|
|
case TokenInstance:
|
|
|
|
|
return p.parseListProviderInstanceModels(providerName)
|
|
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("unknown LIST target: %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// LIST PROVIDER 'provider_name' INSTANCES
|
|
|
|
|
func (p *Parser) parseListProviderInstances(providerName string) (*Command, error) {
|
|
|
|
|
p.nextToken() // consume INSTANCES
|
|
|
|
|
|
|
|
|
|
// Semicolon is optional
|
2026-03-25 21:39:14 +08:00
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
2026-06-24 18:48:09 +08:00
|
|
|
cmd := NewCommand("api_list_provider_instances")
|
|
|
|
|
cmd.Params["provider_name"] = providerName
|
|
|
|
|
return cmd, nil
|
2026-03-25 21:39:14 +08:00
|
|
|
}
|
|
|
|
|
|
2026-06-24 18:48:09 +08:00
|
|
|
// LIST PROVIDER 'provider_name' INSTANCE 'instance_name' MODELS
|
2026-06-25 10:01:21 +08:00
|
|
|
// LIST PROVIDER 'provider_name' INSTANCE 'instance_name' MODELS SYNC, get model list by API from remote server
|
2026-06-24 18:48:09 +08:00
|
|
|
func (p *Parser) parseListProviderInstanceModels(providerName string) (*Command, error) {
|
|
|
|
|
p.nextToken() // consume INSTANCE
|
|
|
|
|
|
|
|
|
|
instanceName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenModels {
|
|
|
|
|
return nil, fmt.Errorf("expected MODELS")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
2026-06-25 10:01:21 +08:00
|
|
|
cmd := NewCommand("api_list_provider_instance_models")
|
|
|
|
|
if p.curToken.Type == TokenSync {
|
|
|
|
|
cmd = NewCommand("api_list_provider_instance_models_sync")
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 18:42:12 +08:00
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
2026-06-24 18:48:09 +08:00
|
|
|
cmd.Params["provider_name"] = providerName
|
|
|
|
|
cmd.Params["instance_name"] = instanceName
|
|
|
|
|
return cmd, nil
|
2026-03-31 18:42:12 +08:00
|
|
|
}
|
|
|
|
|
|
2026-06-25 10:01:21 +08:00
|
|
|
func (p *Parser) parseAPIListDefaultModels() (*Command, error) {
|
2026-03-25 21:39:14 +08:00
|
|
|
p.nextToken() // consume DEFAULT
|
|
|
|
|
if p.curToken.Type != TokenModels {
|
|
|
|
|
return nil, fmt.Errorf("expected MODELS")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
// Semicolon is optional for UNSET TOKEN
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
2026-06-25 10:01:21 +08:00
|
|
|
return NewCommand("api_list_default_models"), nil
|
2026-03-25 21:39:14 +08:00
|
|
|
}
|
|
|
|
|
|
2026-06-24 18:48:09 +08:00
|
|
|
func (p *Parser) parseAPIListAvailableProviders() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume AVAILABLE
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenProviders {
|
|
|
|
|
return nil, fmt.Errorf("expected PROVIDERS")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return NewCommand("api_list_available_providers"), nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-25 21:39:14 +08:00
|
|
|
func (p *Parser) parseListFiles() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume FILES
|
|
|
|
|
if p.curToken.Type != TokenOf {
|
|
|
|
|
return nil, fmt.Errorf("expected OF")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
if p.curToken.Type != TokenDataset {
|
|
|
|
|
return nil, fmt.Errorf("expected DATASET")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
datasetName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("list_user_dataset_files")
|
|
|
|
|
cmd.Params["dataset_name"] = datasetName
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
// Semicolon is optional for UNSET TOKEN
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 12:29:52 +08:00
|
|
|
func (p *Parser) parseListQuotedStringCommand() (*Command, error) {
|
|
|
|
|
str, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
p.nextToken() // consume str
|
|
|
|
|
switch p.curToken.Type {
|
|
|
|
|
case TokenTasks:
|
|
|
|
|
p.nextToken() // consume TASKS
|
|
|
|
|
cmd := NewCommand("list_tasks_user_command")
|
|
|
|
|
cmd.Params["composite_instance_name"] = str
|
|
|
|
|
return cmd, nil
|
|
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("unknown command: %s", str)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) parseShowQuotedStringCommand() (*Command, error) {
|
|
|
|
|
str, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
p.nextToken() // consume str
|
|
|
|
|
switch p.curToken.Type {
|
|
|
|
|
case TokenTask:
|
|
|
|
|
p.nextToken() // consume TASK
|
|
|
|
|
|
|
|
|
|
var taskID string
|
|
|
|
|
taskID, err = p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected string: %w", err)
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("show_task_user_command")
|
|
|
|
|
cmd.Params["task_id"] = taskID
|
|
|
|
|
cmd.Params["composite_instance_name"] = str
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("unknown command: %s", str)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-25 21:39:14 +08:00
|
|
|
func (p *Parser) parseShowCommand() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume SHOW
|
|
|
|
|
switch p.curToken.Type {
|
|
|
|
|
case TokenVersion:
|
|
|
|
|
p.nextToken()
|
|
|
|
|
// Semicolon is optional for SHOW TOKEN
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return NewCommand("show_version"), nil
|
|
|
|
|
case TokenToken:
|
|
|
|
|
p.nextToken()
|
|
|
|
|
// Semicolon is optional for SHOW TOKEN
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return NewCommand("show_token"), nil
|
|
|
|
|
case TokenCurrent:
|
|
|
|
|
p.nextToken()
|
2026-06-09 15:22:50 +08:00
|
|
|
|
|
|
|
|
// Semicolon is optional for SHOW TOKEN
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
2026-03-25 21:39:14 +08:00
|
|
|
p.nextToken()
|
|
|
|
|
}
|
2026-06-09 15:22:50 +08:00
|
|
|
return NewCommand("show_current"), nil
|
2026-03-25 21:39:14 +08:00
|
|
|
case TokenVar:
|
|
|
|
|
return p.parseShowVariable()
|
2026-03-31 18:42:12 +08:00
|
|
|
case TokenProvider:
|
|
|
|
|
return p.parseShowProvider()
|
|
|
|
|
case TokenModel:
|
|
|
|
|
return p.parseShowModel()
|
2026-04-02 20:20:35 +08:00
|
|
|
case TokenInstance:
|
|
|
|
|
return p.parseShowInstance()
|
2026-04-21 21:31:50 +08:00
|
|
|
case TokenBalance:
|
|
|
|
|
return p.parseShowBalance()
|
2026-05-15 12:29:52 +08:00
|
|
|
case TokenTask:
|
|
|
|
|
return p.parseShowTask()
|
|
|
|
|
case TokenQuotedString:
|
|
|
|
|
return p.parseShowQuotedStringCommand()
|
2026-06-09 15:22:50 +08:00
|
|
|
case TokenAdmin:
|
|
|
|
|
return p.parseUserShowAdmin()
|
|
|
|
|
case TokenAPI:
|
|
|
|
|
return p.parseUserShowAPI()
|
2026-03-25 21:39:14 +08:00
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("unknown SHOW target: %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) parseShowVariable() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume VAR
|
|
|
|
|
varName, err := p.parseIdentifier()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("show_variable")
|
|
|
|
|
cmd.Params["var_name"] = varName
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
// Semicolon is optional for UNSET TOKEN
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 18:42:12 +08:00
|
|
|
func (p *Parser) parseShowModel() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume model
|
|
|
|
|
|
|
|
|
|
modelName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected model name: %w", err)
|
|
|
|
|
}
|
|
|
|
|
p.nextToken() // consume model_name
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenFrom {
|
2026-06-08 21:38:15 +08:00
|
|
|
// SHOW MODEL 'model_name'
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
cmd := NewCommand("show_model")
|
|
|
|
|
cmd.Params["model_name"] = modelName
|
|
|
|
|
return cmd, nil
|
2026-03-31 18:42:12 +08:00
|
|
|
}
|
|
|
|
|
p.nextToken() // consume from
|
2026-06-08 21:38:15 +08:00
|
|
|
|
|
|
|
|
cmd := NewCommand("show_provider_model")
|
|
|
|
|
cmd.Params["model_name"] = modelName
|
|
|
|
|
|
2026-03-31 18:42:12 +08:00
|
|
|
providerName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected provider name: %w", err)
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["provider_name"] = providerName
|
|
|
|
|
p.nextToken() // consume provider name
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// parseShowProvider parses SHOW PROVIDER <name> command
|
|
|
|
|
func (p *Parser) parseShowProvider() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume PROVIDER
|
|
|
|
|
|
|
|
|
|
providerName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected provider name: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("show_provider")
|
|
|
|
|
cmd.Params["provider_name"] = providerName
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-08 21:38:15 +08:00
|
|
|
// parseListModels parses LIST MODELS
|
|
|
|
|
func (p *Parser) parseListAllModels() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume models
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("list_all_models")
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-25 21:39:14 +08:00
|
|
|
func (p *Parser) parseCreateCommand() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume CREATE
|
|
|
|
|
|
|
|
|
|
switch p.curToken.Type {
|
|
|
|
|
case TokenModel:
|
|
|
|
|
return p.parseCreateModelProvider()
|
|
|
|
|
case TokenDataset:
|
|
|
|
|
return p.parseCreateDataset()
|
|
|
|
|
case TokenChat:
|
|
|
|
|
return p.parseCreateChat()
|
2026-06-24 16:50:40 +08:00
|
|
|
case TokenKey:
|
|
|
|
|
return p.parseAPICreateKey()
|
2026-05-25 19:15:07 +08:00
|
|
|
case TokenChunkStore:
|
|
|
|
|
return p.parseCreateChunkStore()
|
2026-04-09 09:52:31 +08:00
|
|
|
case TokenMetadata:
|
2026-05-25 19:15:07 +08:00
|
|
|
return p.parseCreateMetadataStore()
|
2026-04-02 20:20:35 +08:00
|
|
|
case TokenProvider:
|
|
|
|
|
return p.parseCreateProviderInstance()
|
2026-03-25 21:39:14 +08:00
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("unknown CREATE target: %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 20:20:35 +08:00
|
|
|
func (p *Parser) parseAddCommand() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume ADD
|
|
|
|
|
switch p.curToken.Type {
|
|
|
|
|
case TokenProvider:
|
|
|
|
|
return p.parseAddProvider()
|
2026-04-29 17:05:08 +08:00
|
|
|
case TokenModel:
|
|
|
|
|
return p.parseAddModel()
|
2026-06-09 15:22:50 +08:00
|
|
|
case TokenAPI:
|
|
|
|
|
return p.parseAddAPIServer()
|
|
|
|
|
case TokenAdmin:
|
|
|
|
|
return p.parseAddAdminServer()
|
2026-04-02 20:20:35 +08:00
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("unknown ADD target: %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-24 16:50:40 +08:00
|
|
|
func (p *Parser) parseAPICreateKey() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume KEY
|
2026-03-25 21:39:14 +08:00
|
|
|
|
2026-06-24 16:50:40 +08:00
|
|
|
// Semicolon is optional for UNSET KEY
|
2026-03-25 21:39:14 +08:00
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-24 16:50:40 +08:00
|
|
|
return NewCommand("api_create_api_key"), nil
|
2026-03-25 21:39:14 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-09 09:52:31 +08:00
|
|
|
// Internal CLI for GO
|
2026-05-25 19:15:07 +08:00
|
|
|
// parseCreateChunkStore parses: CREATE CHUNK STORE for Dataset 'name' VECTOR SIZE N
|
|
|
|
|
func (p *Parser) parseCreateChunkStore() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume CHUNK STORE compound token
|
|
|
|
|
|
|
|
|
|
// Expect FOR
|
|
|
|
|
if p.curToken.Type != TokenFor {
|
|
|
|
|
return nil, fmt.Errorf("expected FOR after CHUNK STORE, got %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Expect Dataset
|
|
|
|
|
if p.curToken.Type != TokenDataset {
|
|
|
|
|
return nil, fmt.Errorf("expected Dataset after FOR, got %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
2026-03-26 11:54:10 +08:00
|
|
|
|
|
|
|
|
datasetName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected dataset name, got %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
2026-04-09 09:52:31 +08:00
|
|
|
if p.curToken.Type != TokenVector {
|
|
|
|
|
return nil, fmt.Errorf("expected VECTOR after dataset name, got %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
if p.curToken.Type != TokenSize {
|
|
|
|
|
return nil, fmt.Errorf("expected SIZE after VECTOR, got %s", p.curToken.Value)
|
2026-03-26 11:54:10 +08:00
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
2026-04-07 15:09:45 +08:00
|
|
|
if p.curToken.Type != TokenInteger {
|
2026-03-26 11:54:10 +08:00
|
|
|
return nil, fmt.Errorf("expected vector size number, got %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
vectorSize, err := strconv.Atoi(p.curToken.Value)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("invalid vector size: %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 19:15:07 +08:00
|
|
|
cmd := NewCommand("create_chunk_store")
|
2026-03-26 11:54:10 +08:00
|
|
|
cmd.Params["dataset_name"] = datasetName
|
|
|
|
|
cmd.Params["vector_size"] = vectorSize
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-09 09:52:31 +08:00
|
|
|
// Internal CLI for GO
|
2026-05-25 19:15:07 +08:00
|
|
|
// parseCreateMetadataStore parses: CREATE METADATA STORE
|
|
|
|
|
func (p *Parser) parseCreateMetadataStore() (*Command, error) {
|
|
|
|
|
// CREATE METADATA STORE
|
2026-04-09 09:52:31 +08:00
|
|
|
p.nextToken() // consume METADATA
|
|
|
|
|
|
2026-05-25 19:15:07 +08:00
|
|
|
if p.curToken.Type != TokenStore {
|
|
|
|
|
return nil, fmt.Errorf("expected STORE after METADATA, got %s", p.curToken.Value)
|
2026-04-09 09:52:31 +08:00
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 19:15:07 +08:00
|
|
|
return NewCommand("create_metadata_store"), nil
|
2026-04-09 09:52:31 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-25 21:39:14 +08:00
|
|
|
func (p *Parser) parseCreateRole() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume ROLE
|
|
|
|
|
roleName, err := p.parseIdentifier()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("create_role")
|
|
|
|
|
cmd.Params["role_name"] = roleName
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
if p.curToken.Type == TokenDescription {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
description, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["description"] = description
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Semicolon is optional for UNSET TOKEN
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) parseCreateModelProvider() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume MODEL
|
|
|
|
|
if p.curToken.Type != TokenProvider {
|
|
|
|
|
return nil, fmt.Errorf("expected PROVIDER")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
providerName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
providerKey, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("create_model_provider")
|
|
|
|
|
cmd.Params["provider_name"] = providerName
|
|
|
|
|
cmd.Params["provider_key"] = providerKey
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
// Semicolon is optional for UNSET TOKEN
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 20:20:35 +08:00
|
|
|
// parseAddProvider parses ADD PROVIDER commands
|
|
|
|
|
// ADD PROVIDER <name>
|
|
|
|
|
func (p *Parser) parseAddProvider() (*Command, error) {
|
2026-03-31 18:42:12 +08:00
|
|
|
p.nextToken() // consume PROVIDER
|
|
|
|
|
|
|
|
|
|
providerName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected provider name: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 20:20:35 +08:00
|
|
|
cmd := NewCommand("add_provider")
|
2026-03-31 18:42:12 +08:00
|
|
|
cmd.Params["provider_name"] = providerName
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
feat(go-cli): support batch model add/remove and optional embedding dimension (#15631)
## Summary
This PR improves the Go CLI in two areas:
1. It adds batch model management support, allowing multiple models to
be added or removed in a single command.
2. It makes the `dimension` argument optional for the `embed text`
command.
These changes keep the existing single-model and explicit-dimension
behaviors compatible while making the CLI more convenient for common
workflows.
## What Changed
### 1. Batch model add/remove support
The CLI now supports operating on multiple model names provided in a
single quoted string.
Supported commands include:
```
add model 'x1 x2 x3' to provider 'vllm' instance 'test' with tokens 1024 chat think vision, token 2048 chat, token 1024 think vision;
drop model 'x1 x2 x3' from 'vllm' 'test';
remove model 'x1 x2 x3' from 'vllm' 'test';
```
For add model, each config segment after with is matched to the
corresponding model name by position.
Example mapping:
- x1 -> tokens 1024, chat + vision, thinking=true
- x2 -> tokens 2048, chat
- x3 -> tokens 1024, vision, thinking=true
The existing single-model syntax remains supported.
### 2. Optional embedding dimension
Previously, the Go CLI required dimension to be explicitly provided for
embed text.
Before:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
Now both forms are supported:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
embed text 'what is rag' 'who are you' with 'model@test@provider';
When omitted, the CLI leaves dimension unset and relies on
provider/backend behavior.
## Tests
Added parser tests covering:
- Multiple models with multiple config segments
- Model type deduplication
- Model/config count mismatch
- Drop/remove multiple models
- Optional embedding dimension parsing
2026-06-05 19:31:06 +08:00
|
|
|
func (p *Parser) parseModelNames(raw string) ([]string, error) {
|
|
|
|
|
modelNames := strings.Fields(raw)
|
|
|
|
|
|
|
|
|
|
if len(modelNames) == 0 {
|
|
|
|
|
return nil, fmt.Errorf("model name is required")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
seen := make(map[string]struct{}, len(modelNames))
|
|
|
|
|
for _, modelName := range modelNames {
|
|
|
|
|
if _, ok := seen[modelName]; ok {
|
|
|
|
|
return nil, fmt.Errorf("duplicate model name: %s", modelName)
|
|
|
|
|
}
|
|
|
|
|
seen[modelName] = struct{}{}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return modelNames, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 17:05:08 +08:00
|
|
|
// syntax: add model 'xxx' to provider 'vllm' instance 'test' with tokens 1024 chat think vision;
|
|
|
|
|
func (p *Parser) parseAddModel() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume MODEL
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenQuotedString {
|
|
|
|
|
return nil, fmt.Errorf("expected model name")
|
|
|
|
|
}
|
|
|
|
|
|
feat(go-cli): support batch model add/remove and optional embedding dimension (#15631)
## Summary
This PR improves the Go CLI in two areas:
1. It adds batch model management support, allowing multiple models to
be added or removed in a single command.
2. It makes the `dimension` argument optional for the `embed text`
command.
These changes keep the existing single-model and explicit-dimension
behaviors compatible while making the CLI more convenient for common
workflows.
## What Changed
### 1. Batch model add/remove support
The CLI now supports operating on multiple model names provided in a
single quoted string.
Supported commands include:
```
add model 'x1 x2 x3' to provider 'vllm' instance 'test' with tokens 1024 chat think vision, token 2048 chat, token 1024 think vision;
drop model 'x1 x2 x3' from 'vllm' 'test';
remove model 'x1 x2 x3' from 'vllm' 'test';
```
For add model, each config segment after with is matched to the
corresponding model name by position.
Example mapping:
- x1 -> tokens 1024, chat + vision, thinking=true
- x2 -> tokens 2048, chat
- x3 -> tokens 1024, vision, thinking=true
The existing single-model syntax remains supported.
### 2. Optional embedding dimension
Previously, the Go CLI required dimension to be explicitly provided for
embed text.
Before:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
Now both forms are supported:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
embed text 'what is rag' 'who are you' with 'model@test@provider';
When omitted, the CLI leaves dimension unset and relies on
provider/backend behavior.
## Tests
Added parser tests covering:
- Multiple models with multiple config segments
- Model type deduplication
- Model/config count mismatch
- Drop/remove multiple models
- Optional embedding dimension parsing
2026-06-05 19:31:06 +08:00
|
|
|
rawModelNames, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
modelNames, err := p.parseModelNames(rawModelNames)
|
2026-04-29 17:05:08 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
p.nextToken() // consume model name
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenTo {
|
|
|
|
|
return nil, fmt.Errorf("expected TO")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenProvider {
|
|
|
|
|
return nil, fmt.Errorf("expected PROVIDER")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// provider name
|
|
|
|
|
if p.curToken.Type != TokenQuotedString {
|
|
|
|
|
return nil, fmt.Errorf("expected provider name")
|
|
|
|
|
}
|
|
|
|
|
providerName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenInstance {
|
|
|
|
|
return nil, fmt.Errorf("expected INSTANCE")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// instance name
|
|
|
|
|
if p.curToken.Type != TokenQuotedString {
|
|
|
|
|
return nil, fmt.Errorf("expected provider name")
|
|
|
|
|
}
|
|
|
|
|
instanceName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
2026-06-17 20:21:42 +08:00
|
|
|
modelIndex := 0
|
2026-04-29 19:18:49 +08:00
|
|
|
var modelTypes []string
|
2026-04-29 17:05:08 +08:00
|
|
|
var supportThink *bool = nil
|
|
|
|
|
maxTokens := 0
|
2026-06-16 12:53:43 +08:00
|
|
|
var maxDimension *int = nil
|
|
|
|
|
var dimensions []int = nil
|
feat(go-cli): support batch model add/remove and optional embedding dimension (#15631)
## Summary
This PR improves the Go CLI in two areas:
1. It adds batch model management support, allowing multiple models to
be added or removed in a single command.
2. It makes the `dimension` argument optional for the `embed text`
command.
These changes keep the existing single-model and explicit-dimension
behaviors compatible while making the CLI more convenient for common
workflows.
## What Changed
### 1. Batch model add/remove support
The CLI now supports operating on multiple model names provided in a
single quoted string.
Supported commands include:
```
add model 'x1 x2 x3' to provider 'vllm' instance 'test' with tokens 1024 chat think vision, token 2048 chat, token 1024 think vision;
drop model 'x1 x2 x3' from 'vllm' 'test';
remove model 'x1 x2 x3' from 'vllm' 'test';
```
For add model, each config segment after with is matched to the
corresponding model name by position.
Example mapping:
- x1 -> tokens 1024, chat + vision, thinking=true
- x2 -> tokens 2048, chat
- x3 -> tokens 1024, vision, thinking=true
The existing single-model syntax remains supported.
### 2. Optional embedding dimension
Previously, the Go CLI required dimension to be explicitly provided for
embed text.
Before:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
Now both forms are supported:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
embed text 'what is rag' 'who are you' with 'model@test@provider';
When omitted, the CLI leaves dimension unset and relies on
provider/backend behavior.
## Tests
Added parser tests covering:
- Multiple models with multiple config segments
- Model type deduplication
- Model/config count mismatch
- Drop/remove multiple models
- Optional embedding dimension parsing
2026-06-05 19:31:06 +08:00
|
|
|
|
|
|
|
|
models := make([]map[string]any, 0, len(modelNames))
|
|
|
|
|
if p.curToken.Type != TokenWith {
|
|
|
|
|
return nil, fmt.Errorf("expected with")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
2026-06-17 20:21:42 +08:00
|
|
|
optionsLoop:
|
feat(go-cli): support batch model add/remove and optional embedding dimension (#15631)
## Summary
This PR improves the Go CLI in two areas:
1. It adds batch model management support, allowing multiple models to
be added or removed in a single command.
2. It makes the `dimension` argument optional for the `embed text`
command.
These changes keep the existing single-model and explicit-dimension
behaviors compatible while making the CLI more convenient for common
workflows.
## What Changed
### 1. Batch model add/remove support
The CLI now supports operating on multiple model names provided in a
single quoted string.
Supported commands include:
```
add model 'x1 x2 x3' to provider 'vllm' instance 'test' with tokens 1024 chat think vision, token 2048 chat, token 1024 think vision;
drop model 'x1 x2 x3' from 'vllm' 'test';
remove model 'x1 x2 x3' from 'vllm' 'test';
```
For add model, each config segment after with is matched to the
corresponding model name by position.
Example mapping:
- x1 -> tokens 1024, chat + vision, thinking=true
- x2 -> tokens 2048, chat
- x3 -> tokens 1024, vision, thinking=true
The existing single-model syntax remains supported.
### 2. Optional embedding dimension
Previously, the Go CLI required dimension to be explicitly provided for
embed text.
Before:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
Now both forms are supported:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
embed text 'what is rag' 'who are you' with 'model@test@provider';
When omitted, the CLI leaves dimension unset and relies on
provider/backend behavior.
## Tests
Added parser tests covering:
- Multiple models with multiple config segments
- Model type deduplication
- Model/config count mismatch
- Drop/remove multiple models
- Optional embedding dimension parsing
2026-06-05 19:31:06 +08:00
|
|
|
for {
|
2026-06-17 20:21:42 +08:00
|
|
|
if modelIndex >= len(modelNames) {
|
feat(go-cli): support batch model add/remove and optional embedding dimension (#15631)
## Summary
This PR improves the Go CLI in two areas:
1. It adds batch model management support, allowing multiple models to
be added or removed in a single command.
2. It makes the `dimension` argument optional for the `embed text`
command.
These changes keep the existing single-model and explicit-dimension
behaviors compatible while making the CLI more convenient for common
workflows.
## What Changed
### 1. Batch model add/remove support
The CLI now supports operating on multiple model names provided in a
single quoted string.
Supported commands include:
```
add model 'x1 x2 x3' to provider 'vllm' instance 'test' with tokens 1024 chat think vision, token 2048 chat, token 1024 think vision;
drop model 'x1 x2 x3' from 'vllm' 'test';
remove model 'x1 x2 x3' from 'vllm' 'test';
```
For add model, each config segment after with is matched to the
corresponding model name by position.
Example mapping:
- x1 -> tokens 1024, chat + vision, thinking=true
- x2 -> tokens 2048, chat
- x3 -> tokens 1024, vision, thinking=true
The existing single-model syntax remains supported.
### 2. Optional embedding dimension
Previously, the Go CLI required dimension to be explicitly provided for
embed text.
Before:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
Now both forms are supported:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
embed text 'what is rag' 'who are you' with 'model@test@provider';
When omitted, the CLI leaves dimension unset and relies on
provider/backend behavior.
## Tests
Added parser tests covering:
- Multiple models with multiple config segments
- Model type deduplication
- Model/config count mismatch
- Drop/remove multiple models
- Optional embedding dimension parsing
2026-06-05 19:31:06 +08:00
|
|
|
return nil, fmt.Errorf("too many model configs: got more configs than model names")
|
|
|
|
|
}
|
2026-06-17 20:21:42 +08:00
|
|
|
currentModelName := modelNames[modelIndex]
|
feat(go-cli): support batch model add/remove and optional embedding dimension (#15631)
## Summary
This PR improves the Go CLI in two areas:
1. It adds batch model management support, allowing multiple models to
be added or removed in a single command.
2. It makes the `dimension` argument optional for the `embed text`
command.
These changes keep the existing single-model and explicit-dimension
behaviors compatible while making the CLI more convenient for common
workflows.
## What Changed
### 1. Batch model add/remove support
The CLI now supports operating on multiple model names provided in a
single quoted string.
Supported commands include:
```
add model 'x1 x2 x3' to provider 'vllm' instance 'test' with tokens 1024 chat think vision, token 2048 chat, token 1024 think vision;
drop model 'x1 x2 x3' from 'vllm' 'test';
remove model 'x1 x2 x3' from 'vllm' 'test';
```
For add model, each config segment after with is matched to the
corresponding model name by position.
Example mapping:
- x1 -> tokens 1024, chat + vision, thinking=true
- x2 -> tokens 2048, chat
- x3 -> tokens 1024, vision, thinking=true
The existing single-model syntax remains supported.
### 2. Optional embedding dimension
Previously, the Go CLI required dimension to be explicitly provided for
embed text.
Before:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
Now both forms are supported:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
embed text 'what is rag' 'who are you' with 'model@test@provider';
When omitted, the CLI leaves dimension unset and relies on
provider/backend behavior.
## Tests
Added parser tests covering:
- Multiple models with multiple config segments
- Model type deduplication
- Model/config count mismatch
- Drop/remove multiple models
- Optional embedding dimension parsing
2026-06-05 19:31:06 +08:00
|
|
|
switch p.curToken.Type {
|
|
|
|
|
case TokenThink:
|
|
|
|
|
if supportThink != nil {
|
2026-06-17 20:21:42 +08:00
|
|
|
return nil, fmt.Errorf("think model is already set for model %s", currentModelName)
|
feat(go-cli): support batch model add/remove and optional embedding dimension (#15631)
## Summary
This PR improves the Go CLI in two areas:
1. It adds batch model management support, allowing multiple models to
be added or removed in a single command.
2. It makes the `dimension` argument optional for the `embed text`
command.
These changes keep the existing single-model and explicit-dimension
behaviors compatible while making the CLI more convenient for common
workflows.
## What Changed
### 1. Batch model add/remove support
The CLI now supports operating on multiple model names provided in a
single quoted string.
Supported commands include:
```
add model 'x1 x2 x3' to provider 'vllm' instance 'test' with tokens 1024 chat think vision, token 2048 chat, token 1024 think vision;
drop model 'x1 x2 x3' from 'vllm' 'test';
remove model 'x1 x2 x3' from 'vllm' 'test';
```
For add model, each config segment after with is matched to the
corresponding model name by position.
Example mapping:
- x1 -> tokens 1024, chat + vision, thinking=true
- x2 -> tokens 2048, chat
- x3 -> tokens 1024, vision, thinking=true
The existing single-model syntax remains supported.
### 2. Optional embedding dimension
Previously, the Go CLI required dimension to be explicitly provided for
embed text.
Before:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
Now both forms are supported:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
embed text 'what is rag' 'who are you' with 'model@test@provider';
When omitted, the CLI leaves dimension unset and relies on
provider/backend behavior.
## Tests
Added parser tests covering:
- Multiple models with multiple config segments
- Model type deduplication
- Model/config count mismatch
- Drop/remove multiple models
- Optional embedding dimension parsing
2026-06-05 19:31:06 +08:00
|
|
|
}
|
|
|
|
|
value := true
|
|
|
|
|
supportThink = &value
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
case TokenVision:
|
|
|
|
|
modelTypes = append(modelTypes, "vision")
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
case TokenChat:
|
|
|
|
|
modelTypes = append(modelTypes, "chat")
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
case TokenEmbedding:
|
|
|
|
|
modelTypes = append(modelTypes, "embedding")
|
|
|
|
|
p.nextToken()
|
2026-06-16 12:53:43 +08:00
|
|
|
if p.curToken.Type == TokenInteger {
|
|
|
|
|
val, err := p.parseNumber()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
maxDimension = &val
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
dimensions = make([]int, 0)
|
|
|
|
|
for p.curToken.Type == TokenInteger {
|
|
|
|
|
dim, err := p.parseNumber()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
dimensions = append(dimensions, int(dim))
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
}
|
feat(go-cli): support batch model add/remove and optional embedding dimension (#15631)
## Summary
This PR improves the Go CLI in two areas:
1. It adds batch model management support, allowing multiple models to
be added or removed in a single command.
2. It makes the `dimension` argument optional for the `embed text`
command.
These changes keep the existing single-model and explicit-dimension
behaviors compatible while making the CLI more convenient for common
workflows.
## What Changed
### 1. Batch model add/remove support
The CLI now supports operating on multiple model names provided in a
single quoted string.
Supported commands include:
```
add model 'x1 x2 x3' to provider 'vllm' instance 'test' with tokens 1024 chat think vision, token 2048 chat, token 1024 think vision;
drop model 'x1 x2 x3' from 'vllm' 'test';
remove model 'x1 x2 x3' from 'vllm' 'test';
```
For add model, each config segment after with is matched to the
corresponding model name by position.
Example mapping:
- x1 -> tokens 1024, chat + vision, thinking=true
- x2 -> tokens 2048, chat
- x3 -> tokens 1024, vision, thinking=true
The existing single-model syntax remains supported.
### 2. Optional embedding dimension
Previously, the Go CLI required dimension to be explicitly provided for
embed text.
Before:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
Now both forms are supported:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
embed text 'what is rag' 'who are you' with 'model@test@provider';
When omitted, the CLI leaves dimension unset and relies on
provider/backend behavior.
## Tests
Added parser tests covering:
- Multiple models with multiple config segments
- Model type deduplication
- Model/config count mismatch
- Drop/remove multiple models
- Optional embedding dimension parsing
2026-06-05 19:31:06 +08:00
|
|
|
|
|
|
|
|
case TokenRerank:
|
|
|
|
|
modelTypes = append(modelTypes, "rerank")
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
case TokenOCR:
|
|
|
|
|
modelTypes = append(modelTypes, "ocr")
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
case TokenDocParse:
|
|
|
|
|
modelTypes = append(modelTypes, "doc_parse")
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
case TokenTTS:
|
|
|
|
|
modelTypes = append(modelTypes, "tts")
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
case TokenASR:
|
|
|
|
|
modelTypes = append(modelTypes, "asr")
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
case TokenToken, TokenTokens:
|
|
|
|
|
p.nextToken()
|
|
|
|
|
if maxTokens != 0 {
|
2026-06-17 20:21:42 +08:00
|
|
|
return nil, fmt.Errorf("max tokens is already given %d for model %s", maxTokens, currentModelName)
|
feat(go-cli): support batch model add/remove and optional embedding dimension (#15631)
## Summary
This PR improves the Go CLI in two areas:
1. It adds batch model management support, allowing multiple models to
be added or removed in a single command.
2. It makes the `dimension` argument optional for the `embed text`
command.
These changes keep the existing single-model and explicit-dimension
behaviors compatible while making the CLI more convenient for common
workflows.
## What Changed
### 1. Batch model add/remove support
The CLI now supports operating on multiple model names provided in a
single quoted string.
Supported commands include:
```
add model 'x1 x2 x3' to provider 'vllm' instance 'test' with tokens 1024 chat think vision, token 2048 chat, token 1024 think vision;
drop model 'x1 x2 x3' from 'vllm' 'test';
remove model 'x1 x2 x3' from 'vllm' 'test';
```
For add model, each config segment after with is matched to the
corresponding model name by position.
Example mapping:
- x1 -> tokens 1024, chat + vision, thinking=true
- x2 -> tokens 2048, chat
- x3 -> tokens 1024, vision, thinking=true
The existing single-model syntax remains supported.
### 2. Optional embedding dimension
Previously, the Go CLI required dimension to be explicitly provided for
embed text.
Before:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
Now both forms are supported:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
embed text 'what is rag' 'who are you' with 'model@test@provider';
When omitted, the CLI leaves dimension unset and relies on
provider/backend behavior.
## Tests
Added parser tests covering:
- Multiple models with multiple config segments
- Model type deduplication
- Model/config count mismatch
- Drop/remove multiple models
- Optional embedding dimension parsing
2026-06-05 19:31:06 +08:00
|
|
|
}
|
|
|
|
|
if p.curToken.Type != TokenInteger {
|
|
|
|
|
return nil, fmt.Errorf("expected integer")
|
|
|
|
|
}
|
|
|
|
|
var err error
|
|
|
|
|
maxTokens, err = p.parseNumber()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
p.nextToken() // consume number
|
|
|
|
|
|
|
|
|
|
case TokenComma, TokenSemicolon, TokenEOF:
|
|
|
|
|
if len(modelTypes) == 0 {
|
2026-06-17 20:21:42 +08:00
|
|
|
return nil, fmt.Errorf("model type is required for model %s", currentModelName)
|
feat(go-cli): support batch model add/remove and optional embedding dimension (#15631)
## Summary
This PR improves the Go CLI in two areas:
1. It adds batch model management support, allowing multiple models to
be added or removed in a single command.
2. It makes the `dimension` argument optional for the `embed text`
command.
These changes keep the existing single-model and explicit-dimension
behaviors compatible while making the CLI more convenient for common
workflows.
## What Changed
### 1. Batch model add/remove support
The CLI now supports operating on multiple model names provided in a
single quoted string.
Supported commands include:
```
add model 'x1 x2 x3' to provider 'vllm' instance 'test' with tokens 1024 chat think vision, token 2048 chat, token 1024 think vision;
drop model 'x1 x2 x3' from 'vllm' 'test';
remove model 'x1 x2 x3' from 'vllm' 'test';
```
For add model, each config segment after with is matched to the
corresponding model name by position.
Example mapping:
- x1 -> tokens 1024, chat + vision, thinking=true
- x2 -> tokens 2048, chat
- x3 -> tokens 1024, vision, thinking=true
The existing single-model syntax remains supported.
### 2. Optional embedding dimension
Previously, the Go CLI required dimension to be explicitly provided for
embed text.
Before:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
Now both forms are supported:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
embed text 'what is rag' 'who are you' with 'model@test@provider';
When omitted, the CLI leaves dimension unset and relies on
provider/backend behavior.
## Tests
Added parser tests covering:
- Multiple models with multiple config segments
- Model type deduplication
- Model/config count mismatch
- Drop/remove multiple models
- Optional embedding dimension parsing
2026-06-05 19:31:06 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
seenTypes := make(map[string]struct{}, len(modelTypes))
|
|
|
|
|
dedupedModelTypes := make([]string, 0, len(modelTypes))
|
|
|
|
|
|
|
|
|
|
for _, modelType := range modelTypes {
|
|
|
|
|
modelType = strings.TrimSpace(modelType)
|
|
|
|
|
if modelType == "" {
|
|
|
|
|
continue
|
2026-04-29 17:05:08 +08:00
|
|
|
}
|
feat(go-cli): support batch model add/remove and optional embedding dimension (#15631)
## Summary
This PR improves the Go CLI in two areas:
1. It adds batch model management support, allowing multiple models to
be added or removed in a single command.
2. It makes the `dimension` argument optional for the `embed text`
command.
These changes keep the existing single-model and explicit-dimension
behaviors compatible while making the CLI more convenient for common
workflows.
## What Changed
### 1. Batch model add/remove support
The CLI now supports operating on multiple model names provided in a
single quoted string.
Supported commands include:
```
add model 'x1 x2 x3' to provider 'vllm' instance 'test' with tokens 1024 chat think vision, token 2048 chat, token 1024 think vision;
drop model 'x1 x2 x3' from 'vllm' 'test';
remove model 'x1 x2 x3' from 'vllm' 'test';
```
For add model, each config segment after with is matched to the
corresponding model name by position.
Example mapping:
- x1 -> tokens 1024, chat + vision, thinking=true
- x2 -> tokens 2048, chat
- x3 -> tokens 1024, vision, thinking=true
The existing single-model syntax remains supported.
### 2. Optional embedding dimension
Previously, the Go CLI required dimension to be explicitly provided for
embed text.
Before:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
Now both forms are supported:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
embed text 'what is rag' 'who are you' with 'model@test@provider';
When omitted, the CLI leaves dimension unset and relies on
provider/backend behavior.
## Tests
Added parser tests covering:
- Multiple models with multiple config segments
- Model type deduplication
- Model/config count mismatch
- Drop/remove multiple models
- Optional embedding dimension parsing
2026-06-05 19:31:06 +08:00
|
|
|
|
|
|
|
|
if _, ok := seenTypes[modelType]; ok {
|
|
|
|
|
continue
|
2026-04-29 17:05:08 +08:00
|
|
|
}
|
feat(go-cli): support batch model add/remove and optional embedding dimension (#15631)
## Summary
This PR improves the Go CLI in two areas:
1. It adds batch model management support, allowing multiple models to
be added or removed in a single command.
2. It makes the `dimension` argument optional for the `embed text`
command.
These changes keep the existing single-model and explicit-dimension
behaviors compatible while making the CLI more convenient for common
workflows.
## What Changed
### 1. Batch model add/remove support
The CLI now supports operating on multiple model names provided in a
single quoted string.
Supported commands include:
```
add model 'x1 x2 x3' to provider 'vllm' instance 'test' with tokens 1024 chat think vision, token 2048 chat, token 1024 think vision;
drop model 'x1 x2 x3' from 'vllm' 'test';
remove model 'x1 x2 x3' from 'vllm' 'test';
```
For add model, each config segment after with is matched to the
corresponding model name by position.
Example mapping:
- x1 -> tokens 1024, chat + vision, thinking=true
- x2 -> tokens 2048, chat
- x3 -> tokens 1024, vision, thinking=true
The existing single-model syntax remains supported.
### 2. Optional embedding dimension
Previously, the Go CLI required dimension to be explicitly provided for
embed text.
Before:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
Now both forms are supported:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
embed text 'what is rag' 'who are you' with 'model@test@provider';
When omitted, the CLI leaves dimension unset and relies on
provider/backend behavior.
## Tests
Added parser tests covering:
- Multiple models with multiple config segments
- Model type deduplication
- Model/config count mismatch
- Drop/remove multiple models
- Optional embedding dimension parsing
2026-06-05 19:31:06 +08:00
|
|
|
|
|
|
|
|
seenTypes[modelType] = struct{}{}
|
|
|
|
|
dedupedModelTypes = append(dedupedModelTypes, modelType)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
modelTypes = dedupedModelTypes
|
|
|
|
|
if len(modelTypes) == 0 {
|
2026-06-17 20:21:42 +08:00
|
|
|
return nil, fmt.Errorf("model type is required for model %s", currentModelName)
|
feat(go-cli): support batch model add/remove and optional embedding dimension (#15631)
## Summary
This PR improves the Go CLI in two areas:
1. It adds batch model management support, allowing multiple models to
be added or removed in a single command.
2. It makes the `dimension` argument optional for the `embed text`
command.
These changes keep the existing single-model and explicit-dimension
behaviors compatible while making the CLI more convenient for common
workflows.
## What Changed
### 1. Batch model add/remove support
The CLI now supports operating on multiple model names provided in a
single quoted string.
Supported commands include:
```
add model 'x1 x2 x3' to provider 'vllm' instance 'test' with tokens 1024 chat think vision, token 2048 chat, token 1024 think vision;
drop model 'x1 x2 x3' from 'vllm' 'test';
remove model 'x1 x2 x3' from 'vllm' 'test';
```
For add model, each config segment after with is matched to the
corresponding model name by position.
Example mapping:
- x1 -> tokens 1024, chat + vision, thinking=true
- x2 -> tokens 2048, chat
- x3 -> tokens 1024, vision, thinking=true
The existing single-model syntax remains supported.
### 2. Optional embedding dimension
Previously, the Go CLI required dimension to be explicitly provided for
embed text.
Before:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
Now both forms are supported:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
embed text 'what is rag' 'who are you' with 'model@test@provider';
When omitted, the CLI leaves dimension unset and relies on
provider/backend behavior.
## Tests
Added parser tests covering:
- Multiple models with multiple config segments
- Model type deduplication
- Model/config count mismatch
- Drop/remove multiple models
- Optional embedding dimension parsing
2026-06-05 19:31:06 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
model := map[string]any{
|
2026-06-17 20:21:42 +08:00
|
|
|
"model_name": currentModelName,
|
feat(go-cli): support batch model add/remove and optional embedding dimension (#15631)
## Summary
This PR improves the Go CLI in two areas:
1. It adds batch model management support, allowing multiple models to
be added or removed in a single command.
2. It makes the `dimension` argument optional for the `embed text`
command.
These changes keep the existing single-model and explicit-dimension
behaviors compatible while making the CLI more convenient for common
workflows.
## What Changed
### 1. Batch model add/remove support
The CLI now supports operating on multiple model names provided in a
single quoted string.
Supported commands include:
```
add model 'x1 x2 x3' to provider 'vllm' instance 'test' with tokens 1024 chat think vision, token 2048 chat, token 1024 think vision;
drop model 'x1 x2 x3' from 'vllm' 'test';
remove model 'x1 x2 x3' from 'vllm' 'test';
```
For add model, each config segment after with is matched to the
corresponding model name by position.
Example mapping:
- x1 -> tokens 1024, chat + vision, thinking=true
- x2 -> tokens 2048, chat
- x3 -> tokens 1024, vision, thinking=true
The existing single-model syntax remains supported.
### 2. Optional embedding dimension
Previously, the Go CLI required dimension to be explicitly provided for
embed text.
Before:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
Now both forms are supported:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
embed text 'what is rag' 'who are you' with 'model@test@provider';
When omitted, the CLI leaves dimension unset and relies on
provider/backend behavior.
## Tests
Added parser tests covering:
- Multiple models with multiple config segments
- Model type deduplication
- Model/config count mismatch
- Drop/remove multiple models
- Optional embedding dimension parsing
2026-06-05 19:31:06 +08:00
|
|
|
"model_types": modelTypes,
|
|
|
|
|
"max_tokens": maxTokens,
|
|
|
|
|
}
|
|
|
|
|
if supportThink != nil {
|
|
|
|
|
model["thinking"] = *supportThink
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 12:53:43 +08:00
|
|
|
if maxDimension != nil {
|
|
|
|
|
model["max_dimension"] = *maxDimension
|
|
|
|
|
model["dimensions"] = dimensions
|
|
|
|
|
}
|
|
|
|
|
|
feat(go-cli): support batch model add/remove and optional embedding dimension (#15631)
## Summary
This PR improves the Go CLI in two areas:
1. It adds batch model management support, allowing multiple models to
be added or removed in a single command.
2. It makes the `dimension` argument optional for the `embed text`
command.
These changes keep the existing single-model and explicit-dimension
behaviors compatible while making the CLI more convenient for common
workflows.
## What Changed
### 1. Batch model add/remove support
The CLI now supports operating on multiple model names provided in a
single quoted string.
Supported commands include:
```
add model 'x1 x2 x3' to provider 'vllm' instance 'test' with tokens 1024 chat think vision, token 2048 chat, token 1024 think vision;
drop model 'x1 x2 x3' from 'vllm' 'test';
remove model 'x1 x2 x3' from 'vllm' 'test';
```
For add model, each config segment after with is matched to the
corresponding model name by position.
Example mapping:
- x1 -> tokens 1024, chat + vision, thinking=true
- x2 -> tokens 2048, chat
- x3 -> tokens 1024, vision, thinking=true
The existing single-model syntax remains supported.
### 2. Optional embedding dimension
Previously, the Go CLI required dimension to be explicitly provided for
embed text.
Before:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
Now both forms are supported:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
embed text 'what is rag' 'who are you' with 'model@test@provider';
When omitted, the CLI leaves dimension unset and relies on
provider/backend behavior.
## Tests
Added parser tests covering:
- Multiple models with multiple config segments
- Model type deduplication
- Model/config count mismatch
- Drop/remove multiple models
- Optional embedding dimension parsing
2026-06-05 19:31:06 +08:00
|
|
|
models = append(models, model)
|
|
|
|
|
|
2026-06-17 20:21:42 +08:00
|
|
|
modelIndex++
|
feat(go-cli): support batch model add/remove and optional embedding dimension (#15631)
## Summary
This PR improves the Go CLI in two areas:
1. It adds batch model management support, allowing multiple models to
be added or removed in a single command.
2. It makes the `dimension` argument optional for the `embed text`
command.
These changes keep the existing single-model and explicit-dimension
behaviors compatible while making the CLI more convenient for common
workflows.
## What Changed
### 1. Batch model add/remove support
The CLI now supports operating on multiple model names provided in a
single quoted string.
Supported commands include:
```
add model 'x1 x2 x3' to provider 'vllm' instance 'test' with tokens 1024 chat think vision, token 2048 chat, token 1024 think vision;
drop model 'x1 x2 x3' from 'vllm' 'test';
remove model 'x1 x2 x3' from 'vllm' 'test';
```
For add model, each config segment after with is matched to the
corresponding model name by position.
Example mapping:
- x1 -> tokens 1024, chat + vision, thinking=true
- x2 -> tokens 2048, chat
- x3 -> tokens 1024, vision, thinking=true
The existing single-model syntax remains supported.
### 2. Optional embedding dimension
Previously, the Go CLI required dimension to be explicitly provided for
embed text.
Before:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
Now both forms are supported:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
embed text 'what is rag' 'who are you' with 'model@test@provider';
When omitted, the CLI leaves dimension unset and relies on
provider/backend behavior.
## Tests
Added parser tests covering:
- Multiple models with multiple config segments
- Model type deduplication
- Model/config count mismatch
- Drop/remove multiple models
- Optional embedding dimension parsing
2026-06-05 19:31:06 +08:00
|
|
|
modelTypes = nil
|
|
|
|
|
supportThink = nil
|
|
|
|
|
maxTokens = 0
|
2026-06-16 12:53:43 +08:00
|
|
|
maxDimension = nil
|
|
|
|
|
dimensions = nil
|
feat(go-cli): support batch model add/remove and optional embedding dimension (#15631)
## Summary
This PR improves the Go CLI in two areas:
1. It adds batch model management support, allowing multiple models to
be added or removed in a single command.
2. It makes the `dimension` argument optional for the `embed text`
command.
These changes keep the existing single-model and explicit-dimension
behaviors compatible while making the CLI more convenient for common
workflows.
## What Changed
### 1. Batch model add/remove support
The CLI now supports operating on multiple model names provided in a
single quoted string.
Supported commands include:
```
add model 'x1 x2 x3' to provider 'vllm' instance 'test' with tokens 1024 chat think vision, token 2048 chat, token 1024 think vision;
drop model 'x1 x2 x3' from 'vllm' 'test';
remove model 'x1 x2 x3' from 'vllm' 'test';
```
For add model, each config segment after with is matched to the
corresponding model name by position.
Example mapping:
- x1 -> tokens 1024, chat + vision, thinking=true
- x2 -> tokens 2048, chat
- x3 -> tokens 1024, vision, thinking=true
The existing single-model syntax remains supported.
### 2. Optional embedding dimension
Previously, the Go CLI required dimension to be explicitly provided for
embed text.
Before:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
Now both forms are supported:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
embed text 'what is rag' 'who are you' with 'model@test@provider';
When omitted, the CLI leaves dimension unset and relies on
provider/backend behavior.
## Tests
Added parser tests covering:
- Multiple models with multiple config segments
- Model type deduplication
- Model/config count mismatch
- Drop/remove multiple models
- Optional embedding dimension parsing
2026-06-05 19:31:06 +08:00
|
|
|
|
|
|
|
|
if p.curToken.Type == TokenComma {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
2026-04-29 17:05:08 +08:00
|
|
|
p.nextToken()
|
|
|
|
|
}
|
2026-06-17 20:21:42 +08:00
|
|
|
break optionsLoop
|
feat(go-cli): support batch model add/remove and optional embedding dimension (#15631)
## Summary
This PR improves the Go CLI in two areas:
1. It adds batch model management support, allowing multiple models to
be added or removed in a single command.
2. It makes the `dimension` argument optional for the `embed text`
command.
These changes keep the existing single-model and explicit-dimension
behaviors compatible while making the CLI more convenient for common
workflows.
## What Changed
### 1. Batch model add/remove support
The CLI now supports operating on multiple model names provided in a
single quoted string.
Supported commands include:
```
add model 'x1 x2 x3' to provider 'vllm' instance 'test' with tokens 1024 chat think vision, token 2048 chat, token 1024 think vision;
drop model 'x1 x2 x3' from 'vllm' 'test';
remove model 'x1 x2 x3' from 'vllm' 'test';
```
For add model, each config segment after with is matched to the
corresponding model name by position.
Example mapping:
- x1 -> tokens 1024, chat + vision, thinking=true
- x2 -> tokens 2048, chat
- x3 -> tokens 1024, vision, thinking=true
The existing single-model syntax remains supported.
### 2. Optional embedding dimension
Previously, the Go CLI required dimension to be explicitly provided for
embed text.
Before:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
Now both forms are supported:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
embed text 'what is rag' 'who are you' with 'model@test@provider';
When omitted, the CLI leaves dimension unset and relies on
provider/backend behavior.
## Tests
Added parser tests covering:
- Multiple models with multiple config segments
- Model type deduplication
- Model/config count mismatch
- Drop/remove multiple models
- Optional embedding dimension parsing
2026-06-05 19:31:06 +08:00
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("unexpected token type: %s", p.curToken.Value)
|
2026-04-29 17:05:08 +08:00
|
|
|
}
|
feat(go-cli): support batch model add/remove and optional embedding dimension (#15631)
## Summary
This PR improves the Go CLI in two areas:
1. It adds batch model management support, allowing multiple models to
be added or removed in a single command.
2. It makes the `dimension` argument optional for the `embed text`
command.
These changes keep the existing single-model and explicit-dimension
behaviors compatible while making the CLI more convenient for common
workflows.
## What Changed
### 1. Batch model add/remove support
The CLI now supports operating on multiple model names provided in a
single quoted string.
Supported commands include:
```
add model 'x1 x2 x3' to provider 'vllm' instance 'test' with tokens 1024 chat think vision, token 2048 chat, token 1024 think vision;
drop model 'x1 x2 x3' from 'vllm' 'test';
remove model 'x1 x2 x3' from 'vllm' 'test';
```
For add model, each config segment after with is matched to the
corresponding model name by position.
Example mapping:
- x1 -> tokens 1024, chat + vision, thinking=true
- x2 -> tokens 2048, chat
- x3 -> tokens 1024, vision, thinking=true
The existing single-model syntax remains supported.
### 2. Optional embedding dimension
Previously, the Go CLI required dimension to be explicitly provided for
embed text.
Before:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
Now both forms are supported:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
embed text 'what is rag' 'who are you' with 'model@test@provider';
When omitted, the CLI leaves dimension unset and relies on
provider/backend behavior.
## Tests
Added parser tests covering:
- Multiple models with multiple config segments
- Model type deduplication
- Model/config count mismatch
- Drop/remove multiple models
- Optional embedding dimension parsing
2026-06-05 19:31:06 +08:00
|
|
|
|
|
|
|
|
}
|
|
|
|
|
if len(models) != len(modelNames) {
|
|
|
|
|
return nil, fmt.Errorf("model config count %d does not match model name count %d", len(models), len(modelNames))
|
2026-04-29 17:05:08 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("add_custom_model")
|
|
|
|
|
cmd.Params["provider_name"] = providerName
|
|
|
|
|
cmd.Params["instance_name"] = instanceName
|
feat(go-cli): support batch model add/remove and optional embedding dimension (#15631)
## Summary
This PR improves the Go CLI in two areas:
1. It adds batch model management support, allowing multiple models to
be added or removed in a single command.
2. It makes the `dimension` argument optional for the `embed text`
command.
These changes keep the existing single-model and explicit-dimension
behaviors compatible while making the CLI more convenient for common
workflows.
## What Changed
### 1. Batch model add/remove support
The CLI now supports operating on multiple model names provided in a
single quoted string.
Supported commands include:
```
add model 'x1 x2 x3' to provider 'vllm' instance 'test' with tokens 1024 chat think vision, token 2048 chat, token 1024 think vision;
drop model 'x1 x2 x3' from 'vllm' 'test';
remove model 'x1 x2 x3' from 'vllm' 'test';
```
For add model, each config segment after with is matched to the
corresponding model name by position.
Example mapping:
- x1 -> tokens 1024, chat + vision, thinking=true
- x2 -> tokens 2048, chat
- x3 -> tokens 1024, vision, thinking=true
The existing single-model syntax remains supported.
### 2. Optional embedding dimension
Previously, the Go CLI required dimension to be explicitly provided for
embed text.
Before:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
Now both forms are supported:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
embed text 'what is rag' 'who are you' with 'model@test@provider';
When omitted, the CLI leaves dimension unset and relies on
provider/backend behavior.
## Tests
Added parser tests covering:
- Multiple models with multiple config segments
- Model type deduplication
- Model/config count mismatch
- Drop/remove multiple models
- Optional embedding dimension parsing
2026-06-05 19:31:06 +08:00
|
|
|
|
|
|
|
|
cmd.Params["models"] = models
|
2026-04-29 17:05:08 +08:00
|
|
|
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-22 17:33:47 +08:00
|
|
|
// syntax: add admin host '127.0.0.1:9333' user 'ccc' password 'ppp'
|
2026-06-09 15:22:50 +08:00
|
|
|
func (p *Parser) parseAddAdminServer() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume ADMIN
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenHost {
|
|
|
|
|
return nil, fmt.Errorf("expected HOST")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
host, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
ip, port, err := parseHostPort(host)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("add_admin_server")
|
|
|
|
|
cmd.Params["server_ip"] = ip
|
|
|
|
|
cmd.Params["server_port"] = port
|
|
|
|
|
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-24 13:41:01 +08:00
|
|
|
// syntax: ADD API 'abc' HOST '127.0.0.1:9333';
|
2026-06-09 15:22:50 +08:00
|
|
|
func (p *Parser) parseAddAPIServer() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume API
|
|
|
|
|
|
|
|
|
|
serverName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
p.nextToken() // consume model name
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenHost {
|
2026-06-24 13:41:01 +08:00
|
|
|
return nil, fmt.Errorf("expected HOST")
|
2026-06-09 15:22:50 +08:00
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
host, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
ip, port, err := parseHostPort(host)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("add_api_server")
|
|
|
|
|
cmd.Params["server_name"] = serverName
|
|
|
|
|
cmd.Params["server_ip"] = ip
|
|
|
|
|
cmd.Params["server_port"] = port
|
|
|
|
|
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-22 17:33:47 +08:00
|
|
|
// syntax: delete api 'abc'
|
2026-06-09 15:22:50 +08:00
|
|
|
func (p *Parser) parseDeleteAPIServer() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume API
|
|
|
|
|
|
|
|
|
|
serverName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
p.nextToken() // consume model name
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("delete_api_server")
|
|
|
|
|
cmd.Params["server_name"] = serverName
|
|
|
|
|
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// syntax: delete admin server 'abc'
|
|
|
|
|
func (p *Parser) parseDeleteAdminServer() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume ADMIN
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("delete_admin_server")
|
|
|
|
|
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) parseUserSaveCommand() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume SAVE
|
|
|
|
|
switch p.curToken.Type {
|
|
|
|
|
case TokenConfig:
|
|
|
|
|
return p.parseSaveConfig()
|
|
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("unknown ADD target: %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// syntax: save config as 'path'
|
|
|
|
|
func (p *Parser) parseSaveConfig() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume CONFIG
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenAs {
|
|
|
|
|
return nil, fmt.Errorf("expected AS after CONFIG")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken() // consume AS
|
|
|
|
|
|
|
|
|
|
path, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("save_config_command")
|
|
|
|
|
cmd.Params["path"] = path
|
|
|
|
|
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-25 21:39:14 +08:00
|
|
|
func (p *Parser) parseCreateDataset() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume DATASET
|
|
|
|
|
datasetName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
if p.curToken.Type != TokenWith {
|
|
|
|
|
return nil, fmt.Errorf("expected WITH")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
if p.curToken.Type != TokenEmbedding {
|
|
|
|
|
return nil, fmt.Errorf("expected EMBEDDING")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
embedding, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
cmd := NewCommand("create_user_dataset")
|
|
|
|
|
cmd.Params["dataset_name"] = datasetName
|
|
|
|
|
cmd.Params["embedding"] = embedding
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type == TokenParser {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
parserType, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["parser_type"] = parserType
|
|
|
|
|
p.nextToken()
|
|
|
|
|
} else if p.curToken.Type == TokenPipeline {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
pipeline, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["pipeline"] = pipeline
|
|
|
|
|
p.nextToken()
|
|
|
|
|
} else {
|
|
|
|
|
return nil, fmt.Errorf("expected PARSER or PIPELINE")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Semicolon is optional for UNSET TOKEN
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) parseCreateChat() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume CHAT
|
|
|
|
|
chatName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("create_user_chat")
|
|
|
|
|
cmd.Params["chat_name"] = chatName
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
// Semicolon is optional for UNSET TOKEN
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) parseDropCommand() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume DROP
|
|
|
|
|
|
|
|
|
|
switch p.curToken.Type {
|
|
|
|
|
case TokenUser:
|
|
|
|
|
return p.parseDropUser()
|
|
|
|
|
case TokenRole:
|
|
|
|
|
return p.parseDropRole()
|
|
|
|
|
case TokenDataset:
|
|
|
|
|
return p.parseDropDataset()
|
|
|
|
|
case TokenChat:
|
|
|
|
|
return p.parseDropChat()
|
2026-06-24 16:50:40 +08:00
|
|
|
case TokenKey:
|
|
|
|
|
return p.parseAPIDeleteAPIKey()
|
2026-05-25 19:15:07 +08:00
|
|
|
case TokenChunkStore:
|
|
|
|
|
return p.parseDropChunkStore()
|
2026-04-09 09:52:31 +08:00
|
|
|
case TokenMetadata:
|
2026-05-25 19:15:07 +08:00
|
|
|
return p.parseDropMetadataStore()
|
2026-04-02 20:20:35 +08:00
|
|
|
case TokenInstance:
|
|
|
|
|
return p.parseDropInstance()
|
2026-04-29 19:18:49 +08:00
|
|
|
case TokenModel:
|
|
|
|
|
return p.parseDropInstanceModel()
|
2026-04-02 20:20:35 +08:00
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("unknown DROP target: %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) parseDeleteCommand() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume DELETE
|
|
|
|
|
|
|
|
|
|
switch p.curToken.Type {
|
|
|
|
|
case TokenProvider:
|
|
|
|
|
return p.parseDeleteProvider()
|
2026-05-25 19:15:07 +08:00
|
|
|
case TokenMetadata:
|
|
|
|
|
return p.parseDeleteMeta()
|
2026-06-09 15:22:50 +08:00
|
|
|
case TokenAdmin:
|
|
|
|
|
return p.parseDeleteAdminServer()
|
|
|
|
|
case TokenAPI:
|
|
|
|
|
return p.parseDeleteAPIServer()
|
2026-03-25 21:39:14 +08:00
|
|
|
default:
|
2026-05-25 19:15:07 +08:00
|
|
|
return nil, fmt.Errorf("unknown DELETE target: %s", p.curToken.Value)
|
2026-03-25 21:39:14 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 09:44:51 +08:00
|
|
|
func (p *Parser) parseRemoveCommand() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume RM
|
|
|
|
|
|
|
|
|
|
switch p.curToken.Type {
|
|
|
|
|
case TokenTag:
|
|
|
|
|
return p.parseRemoveTags()
|
2026-04-09 09:52:31 +08:00
|
|
|
case TokenChunks, TokenAll:
|
|
|
|
|
return p.parseRemoveChunk()
|
2026-06-12 14:56:44 +08:00
|
|
|
case TokenIngestion:
|
|
|
|
|
return p.parseUserRemoveTask()
|
feat(go-cli): support batch model add/remove and optional embedding dimension (#15631)
## Summary
This PR improves the Go CLI in two areas:
1. It adds batch model management support, allowing multiple models to
be added or removed in a single command.
2. It makes the `dimension` argument optional for the `embed text`
command.
These changes keep the existing single-model and explicit-dimension
behaviors compatible while making the CLI more convenient for common
workflows.
## What Changed
### 1. Batch model add/remove support
The CLI now supports operating on multiple model names provided in a
single quoted string.
Supported commands include:
```
add model 'x1 x2 x3' to provider 'vllm' instance 'test' with tokens 1024 chat think vision, token 2048 chat, token 1024 think vision;
drop model 'x1 x2 x3' from 'vllm' 'test';
remove model 'x1 x2 x3' from 'vllm' 'test';
```
For add model, each config segment after with is matched to the
corresponding model name by position.
Example mapping:
- x1 -> tokens 1024, chat + vision, thinking=true
- x2 -> tokens 2048, chat
- x3 -> tokens 1024, vision, thinking=true
The existing single-model syntax remains supported.
### 2. Optional embedding dimension
Previously, the Go CLI required dimension to be explicitly provided for
embed text.
Before:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
Now both forms are supported:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
embed text 'what is rag' 'who are you' with 'model@test@provider';
When omitted, the CLI leaves dimension unset and relies on
provider/backend behavior.
## Tests
Added parser tests covering:
- Multiple models with multiple config segments
- Model type deduplication
- Model/config count mismatch
- Drop/remove multiple models
- Optional embedding dimension parsing
2026-06-05 19:31:06 +08:00
|
|
|
case TokenModel:
|
|
|
|
|
return p.parseRemoveInstanceModel()
|
2026-04-07 09:44:51 +08:00
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("unknown REMOVE target: %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-24 16:50:40 +08:00
|
|
|
func (p *Parser) parseAPIDeleteAPIKey() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume KEY
|
2026-03-25 21:39:14 +08:00
|
|
|
|
2026-06-24 16:50:40 +08:00
|
|
|
apiKey, err := p.parseQuotedString()
|
2026-03-25 21:39:14 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
2026-06-24 16:50:40 +08:00
|
|
|
cmd := NewCommand("api_delete_api_key")
|
|
|
|
|
cmd.Params["api_key"] = apiKey
|
2026-03-25 21:39:14 +08:00
|
|
|
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-09 09:52:31 +08:00
|
|
|
// Internal CLI for GO
|
2026-05-25 19:15:07 +08:00
|
|
|
// parseDropChunkStore parses: DROP CHUNK STORE for Dataset 'name'
|
|
|
|
|
func (p *Parser) parseDropChunkStore() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume CHUNK STORE
|
|
|
|
|
|
|
|
|
|
// Expect FOR
|
|
|
|
|
if p.curToken.Type != TokenFor {
|
|
|
|
|
return nil, fmt.Errorf("expected FOR after CHUNK STORE, got %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Expect Dataset
|
|
|
|
|
if p.curToken.Type != TokenDataset {
|
|
|
|
|
return nil, fmt.Errorf("expected Dataset after FOR, got %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
2026-03-26 11:54:10 +08:00
|
|
|
|
|
|
|
|
datasetName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected dataset name, got %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 19:15:07 +08:00
|
|
|
cmd := NewCommand("drop_chunk_store")
|
2026-03-26 11:54:10 +08:00
|
|
|
cmd.Params["dataset_name"] = datasetName
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 19:15:07 +08:00
|
|
|
// parseDropMetadataStore parses: DROP METADATA STORE
|
|
|
|
|
func (p *Parser) parseDropMetadataStore() (*Command, error) {
|
|
|
|
|
// DROP METADATA STORE
|
2026-04-09 09:52:31 +08:00
|
|
|
p.nextToken() // consume METADATA
|
|
|
|
|
|
2026-05-25 19:15:07 +08:00
|
|
|
if p.curToken.Type != TokenStore {
|
|
|
|
|
return nil, fmt.Errorf("expected STORE after METADATA, got %s", p.curToken.Value)
|
2026-04-09 09:52:31 +08:00
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 19:15:07 +08:00
|
|
|
cmd := NewCommand("drop_metadata_store")
|
2026-04-09 09:52:31 +08:00
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-25 21:39:14 +08:00
|
|
|
func (p *Parser) parseDropUser() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume USER
|
|
|
|
|
userName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("drop_user")
|
|
|
|
|
cmd.Params["user_name"] = userName
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
// Semicolon is optional for UNSET TOKEN
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) parseDropRole() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume ROLE
|
|
|
|
|
roleName, err := p.parseIdentifier()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("drop_role")
|
|
|
|
|
cmd.Params["role_name"] = roleName
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
// Semicolon is optional for UNSET TOKEN
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 20:20:35 +08:00
|
|
|
// parseDeleteProvider parses DELETE PROVIDER <name> command
|
|
|
|
|
func (p *Parser) parseDeleteProvider() (*Command, error) {
|
2026-03-31 18:42:12 +08:00
|
|
|
p.nextToken() // consume PROVIDER
|
|
|
|
|
|
|
|
|
|
providerName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected provider name: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 20:20:35 +08:00
|
|
|
cmd := NewCommand("delete_provider")
|
2026-03-31 18:42:12 +08:00
|
|
|
cmd.Params["provider_name"] = providerName
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-25 21:39:14 +08:00
|
|
|
func (p *Parser) parseDropDataset() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume DATASET
|
|
|
|
|
datasetName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("drop_user_dataset")
|
|
|
|
|
cmd.Params["dataset_name"] = datasetName
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
// Semicolon is optional for UNSET TOKEN
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) parseDropChat() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume CHAT
|
|
|
|
|
chatName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("drop_user_chat")
|
|
|
|
|
cmd.Params["chat_name"] = chatName
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
// Semicolon is optional for UNSET TOKEN
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) parseAlterCommand() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume ALTER
|
|
|
|
|
|
|
|
|
|
switch p.curToken.Type {
|
2026-03-31 18:42:12 +08:00
|
|
|
case TokenProvider:
|
|
|
|
|
return p.parseAlterProvider()
|
2026-04-02 20:20:35 +08:00
|
|
|
case TokenInstance:
|
|
|
|
|
return p.parseAlterInstance()
|
2026-03-25 21:39:14 +08:00
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("unknown ALTER target: %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 18:42:12 +08:00
|
|
|
// parseAlterProvider parses ALTER PROVIDER <name> NAME <new_name> command
|
|
|
|
|
func (p *Parser) parseAlterProvider() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume PROVIDER
|
|
|
|
|
|
|
|
|
|
providerName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected provider name: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
if p.curToken.Type != TokenName {
|
|
|
|
|
return nil, fmt.Errorf("expected NAME")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
newName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected new provider name: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("alter_provider")
|
|
|
|
|
cmd.Params["provider_name"] = providerName
|
|
|
|
|
cmd.Params["new_name"] = newName
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 13:23:20 +08:00
|
|
|
// parseCreateProviderInstance parses CREATE PROVIDER <name> INSTANCE <instance_name> KEY <api_key> URL <base_url> REGION <region> command
|
2026-04-02 20:20:35 +08:00
|
|
|
// instance_name cannot be "default"
|
|
|
|
|
func (p *Parser) parseCreateProviderInstance() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume PROVIDER
|
|
|
|
|
|
|
|
|
|
providerName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected provider name: %w", err)
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
2026-06-03 13:23:20 +08:00
|
|
|
|
2026-04-02 20:20:35 +08:00
|
|
|
if p.curToken.Type != TokenInstance {
|
|
|
|
|
return nil, fmt.Errorf("expected INSTANCE after provider name")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
instanceName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected instance name: %w", err)
|
|
|
|
|
}
|
2026-04-29 17:05:08 +08:00
|
|
|
p.nextToken()
|
2026-04-02 20:20:35 +08:00
|
|
|
|
2026-04-29 17:05:08 +08:00
|
|
|
if p.curToken.Type != TokenKey {
|
|
|
|
|
return nil, fmt.Errorf("expected KEY after instance name")
|
|
|
|
|
}
|
2026-04-02 20:20:35 +08:00
|
|
|
p.nextToken()
|
2026-04-29 17:05:08 +08:00
|
|
|
|
2026-04-02 20:20:35 +08:00
|
|
|
apiKey, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected API key: %w", err)
|
|
|
|
|
}
|
2026-04-29 17:05:08 +08:00
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
baseURL := ""
|
|
|
|
|
region := ""
|
2026-06-03 13:23:20 +08:00
|
|
|
optionsLoop:
|
|
|
|
|
for {
|
|
|
|
|
switch p.curToken.Type {
|
|
|
|
|
case TokenRegion:
|
|
|
|
|
p.nextToken()
|
|
|
|
|
region, err = p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected region: %w", err)
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
case TokenURL:
|
|
|
|
|
p.nextToken()
|
|
|
|
|
baseURL, err = p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected base URL: %w", err)
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
default:
|
|
|
|
|
break optionsLoop
|
2026-04-29 17:05:08 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-02 20:20:35 +08:00
|
|
|
|
|
|
|
|
cmd := NewCommand("create_provider_instance")
|
|
|
|
|
cmd.Params["provider_name"] = providerName
|
|
|
|
|
cmd.Params["instance_name"] = instanceName
|
|
|
|
|
cmd.Params["api_key"] = apiKey
|
2026-04-29 17:05:08 +08:00
|
|
|
if baseURL != "" {
|
|
|
|
|
// Only local model provider need to set URL
|
|
|
|
|
cmd.Params["base_url"] = baseURL
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if region != "" {
|
|
|
|
|
cmd.Params["region"] = region
|
|
|
|
|
}
|
2026-04-02 20:20:35 +08:00
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// parseShowInstance parses SHOW INSTANCE <name> FROM PROVIDER <name> command
|
|
|
|
|
func (p *Parser) parseShowInstance() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume INSTANCE
|
|
|
|
|
|
|
|
|
|
instanceName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected instance name: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
if p.curToken.Type != TokenFrom {
|
|
|
|
|
return nil, fmt.Errorf("expected FROM")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
providerName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected provider name after FROM PROVIDER: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("show_provider_instance")
|
|
|
|
|
cmd.Params["instance_name"] = instanceName
|
|
|
|
|
cmd.Params["provider_name"] = providerName
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 19:15:07 +08:00
|
|
|
// parseShowBalance parses SHOW BALANCE FROM <provider_name> <instance_name>
|
2026-04-21 21:31:50 +08:00
|
|
|
func (p *Parser) parseShowBalance() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume INSTANCE
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenFrom {
|
|
|
|
|
return nil, fmt.Errorf("expected FROM")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenQuotedString {
|
|
|
|
|
return nil, fmt.Errorf("expected provider name after FROM PROVIDER")
|
|
|
|
|
}
|
|
|
|
|
providerName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected provider name after FROM PROVIDER: %w", err)
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenQuotedString {
|
|
|
|
|
return nil, fmt.Errorf("expected instance name")
|
|
|
|
|
}
|
|
|
|
|
instanceName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected instance name: %w", err)
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("show_instance_balance")
|
|
|
|
|
cmd.Params["instance_name"] = instanceName
|
|
|
|
|
cmd.Params["provider_name"] = providerName
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 12:29:52 +08:00
|
|
|
// parseShowTask parses SHOW TASK <task>
|
|
|
|
|
func (p *Parser) parseShowTask() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume TASK
|
|
|
|
|
|
|
|
|
|
taskID, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected string: %w", err)
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("show_task_user_command")
|
|
|
|
|
cmd.Params["task_id"] = taskID
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 20:20:35 +08:00
|
|
|
// parseAlterInstance parses ALTER INSTANCE <name> NAME <new_name> FROM PROVIDER <name> command
|
|
|
|
|
func (p *Parser) parseAlterInstance() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume INSTANCE
|
|
|
|
|
|
|
|
|
|
instanceName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected instance name: %w", err)
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
2026-06-23 16:57:05 +08:00
|
|
|
|
|
|
|
|
newModelName := ""
|
|
|
|
|
newAPIKey := ""
|
|
|
|
|
optionsLoop:
|
|
|
|
|
for {
|
|
|
|
|
switch p.curToken.Type {
|
|
|
|
|
case TokenName:
|
|
|
|
|
p.nextToken()
|
|
|
|
|
newModelName, err = p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected model name: %w", err)
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
case TokenKey:
|
|
|
|
|
p.nextToken()
|
|
|
|
|
newAPIKey, err = p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected API key: %w", err)
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
default:
|
|
|
|
|
break optionsLoop
|
|
|
|
|
}
|
2026-04-02 20:20:35 +08:00
|
|
|
}
|
|
|
|
|
|
2026-06-23 16:57:05 +08:00
|
|
|
if newModelName == "" && newAPIKey == "" {
|
|
|
|
|
return nil, fmt.Errorf("expected NAME or KEY after INSTANCE")
|
2026-04-02 20:20:35 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenFrom {
|
|
|
|
|
return nil, fmt.Errorf("expected FROM")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenProvider {
|
|
|
|
|
return nil, fmt.Errorf("expected PROVIDER after FROM")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
providerName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected provider name after FROM PROVIDER: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("alter_provider_instance")
|
|
|
|
|
cmd.Params["provider_name"] = providerName
|
2026-06-23 16:57:05 +08:00
|
|
|
cmd.Params["instance_name"] = instanceName
|
|
|
|
|
cmd.Params["new_model_name"] = newModelName
|
|
|
|
|
cmd.Params["new_api_key"] = newAPIKey
|
2026-04-02 20:20:35 +08:00
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// parseDropInstance parses DROP INSTANCE <name> FROM PROVIDER <name> command
|
|
|
|
|
func (p *Parser) parseDropInstance() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume INSTANCE
|
|
|
|
|
|
|
|
|
|
instanceName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected instance name: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
if p.curToken.Type != TokenFrom {
|
|
|
|
|
return nil, fmt.Errorf("expected FROM")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
providerName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected provider name after FROM PROVIDER: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("drop_provider_instance")
|
|
|
|
|
cmd.Params["instance_name"] = instanceName
|
|
|
|
|
cmd.Params["provider_name"] = providerName
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
feat(go-cli): support batch model add/remove and optional embedding dimension (#15631)
## Summary
This PR improves the Go CLI in two areas:
1. It adds batch model management support, allowing multiple models to
be added or removed in a single command.
2. It makes the `dimension` argument optional for the `embed text`
command.
These changes keep the existing single-model and explicit-dimension
behaviors compatible while making the CLI more convenient for common
workflows.
## What Changed
### 1. Batch model add/remove support
The CLI now supports operating on multiple model names provided in a
single quoted string.
Supported commands include:
```
add model 'x1 x2 x3' to provider 'vllm' instance 'test' with tokens 1024 chat think vision, token 2048 chat, token 1024 think vision;
drop model 'x1 x2 x3' from 'vllm' 'test';
remove model 'x1 x2 x3' from 'vllm' 'test';
```
For add model, each config segment after with is matched to the
corresponding model name by position.
Example mapping:
- x1 -> tokens 1024, chat + vision, thinking=true
- x2 -> tokens 2048, chat
- x3 -> tokens 1024, vision, thinking=true
The existing single-model syntax remains supported.
### 2. Optional embedding dimension
Previously, the Go CLI required dimension to be explicitly provided for
embed text.
Before:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
Now both forms are supported:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
embed text 'what is rag' 'who are you' with 'model@test@provider';
When omitted, the CLI leaves dimension unset and relies on
provider/backend behavior.
## Tests
Added parser tests covering:
- Multiple models with multiple config segments
- Model type deduplication
- Model/config count mismatch
- Drop/remove multiple models
- Optional embedding dimension parsing
2026-06-05 19:31:06 +08:00
|
|
|
func (p *Parser) parseRemoveInstanceModel() (*Command, error) {
|
|
|
|
|
return p.parseDropInstanceModel()
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 19:18:49 +08:00
|
|
|
// parseDropInstanceModel parses DROP MODEL <name> FROM <provider_name> <instance_name> command
|
|
|
|
|
// Only works for local deployed model
|
|
|
|
|
func (p *Parser) parseDropInstanceModel() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume MODEL
|
|
|
|
|
|
feat(go-cli): support batch model add/remove and optional embedding dimension (#15631)
## Summary
This PR improves the Go CLI in two areas:
1. It adds batch model management support, allowing multiple models to
be added or removed in a single command.
2. It makes the `dimension` argument optional for the `embed text`
command.
These changes keep the existing single-model and explicit-dimension
behaviors compatible while making the CLI more convenient for common
workflows.
## What Changed
### 1. Batch model add/remove support
The CLI now supports operating on multiple model names provided in a
single quoted string.
Supported commands include:
```
add model 'x1 x2 x3' to provider 'vllm' instance 'test' with tokens 1024 chat think vision, token 2048 chat, token 1024 think vision;
drop model 'x1 x2 x3' from 'vllm' 'test';
remove model 'x1 x2 x3' from 'vllm' 'test';
```
For add model, each config segment after with is matched to the
corresponding model name by position.
Example mapping:
- x1 -> tokens 1024, chat + vision, thinking=true
- x2 -> tokens 2048, chat
- x3 -> tokens 1024, vision, thinking=true
The existing single-model syntax remains supported.
### 2. Optional embedding dimension
Previously, the Go CLI required dimension to be explicitly provided for
embed text.
Before:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
Now both forms are supported:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
embed text 'what is rag' 'who are you' with 'model@test@provider';
When omitted, the CLI leaves dimension unset and relies on
provider/backend behavior.
## Tests
Added parser tests covering:
- Multiple models with multiple config segments
- Model type deduplication
- Model/config count mismatch
- Drop/remove multiple models
- Optional embedding dimension parsing
2026-06-05 19:31:06 +08:00
|
|
|
rawModelNames, err := p.parseQuotedString()
|
2026-04-29 19:18:49 +08:00
|
|
|
if err != nil {
|
feat(go-cli): support batch model add/remove and optional embedding dimension (#15631)
## Summary
This PR improves the Go CLI in two areas:
1. It adds batch model management support, allowing multiple models to
be added or removed in a single command.
2. It makes the `dimension` argument optional for the `embed text`
command.
These changes keep the existing single-model and explicit-dimension
behaviors compatible while making the CLI more convenient for common
workflows.
## What Changed
### 1. Batch model add/remove support
The CLI now supports operating on multiple model names provided in a
single quoted string.
Supported commands include:
```
add model 'x1 x2 x3' to provider 'vllm' instance 'test' with tokens 1024 chat think vision, token 2048 chat, token 1024 think vision;
drop model 'x1 x2 x3' from 'vllm' 'test';
remove model 'x1 x2 x3' from 'vllm' 'test';
```
For add model, each config segment after with is matched to the
corresponding model name by position.
Example mapping:
- x1 -> tokens 1024, chat + vision, thinking=true
- x2 -> tokens 2048, chat
- x3 -> tokens 1024, vision, thinking=true
The existing single-model syntax remains supported.
### 2. Optional embedding dimension
Previously, the Go CLI required dimension to be explicitly provided for
embed text.
Before:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
Now both forms are supported:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
embed text 'what is rag' 'who are you' with 'model@test@provider';
When omitted, the CLI leaves dimension unset and relies on
provider/backend behavior.
## Tests
Added parser tests covering:
- Multiple models with multiple config segments
- Model type deduplication
- Model/config count mismatch
- Drop/remove multiple models
- Optional embedding dimension parsing
2026-06-05 19:31:06 +08:00
|
|
|
return nil, err
|
2026-04-29 19:18:49 +08:00
|
|
|
}
|
feat(go-cli): support batch model add/remove and optional embedding dimension (#15631)
## Summary
This PR improves the Go CLI in two areas:
1. It adds batch model management support, allowing multiple models to
be added or removed in a single command.
2. It makes the `dimension` argument optional for the `embed text`
command.
These changes keep the existing single-model and explicit-dimension
behaviors compatible while making the CLI more convenient for common
workflows.
## What Changed
### 1. Batch model add/remove support
The CLI now supports operating on multiple model names provided in a
single quoted string.
Supported commands include:
```
add model 'x1 x2 x3' to provider 'vllm' instance 'test' with tokens 1024 chat think vision, token 2048 chat, token 1024 think vision;
drop model 'x1 x2 x3' from 'vllm' 'test';
remove model 'x1 x2 x3' from 'vllm' 'test';
```
For add model, each config segment after with is matched to the
corresponding model name by position.
Example mapping:
- x1 -> tokens 1024, chat + vision, thinking=true
- x2 -> tokens 2048, chat
- x3 -> tokens 1024, vision, thinking=true
The existing single-model syntax remains supported.
### 2. Optional embedding dimension
Previously, the Go CLI required dimension to be explicitly provided for
embed text.
Before:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
Now both forms are supported:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
embed text 'what is rag' 'who are you' with 'model@test@provider';
When omitted, the CLI leaves dimension unset and relies on
provider/backend behavior.
## Tests
Added parser tests covering:
- Multiple models with multiple config segments
- Model type deduplication
- Model/config count mismatch
- Drop/remove multiple models
- Optional embedding dimension parsing
2026-06-05 19:31:06 +08:00
|
|
|
modelNames, err := p.parseModelNames(rawModelNames)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
p.nextToken() // consume model name
|
2026-04-29 19:18:49 +08:00
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenFrom {
|
|
|
|
|
return nil, fmt.Errorf("expected FROM")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
providerName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected provider name after FROM PROVIDER: %w", err)
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
instanceName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected instance name after provider name: %w", err)
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("drop_instance_model")
|
|
|
|
|
cmd.Params["instance_name"] = instanceName
|
|
|
|
|
cmd.Params["provider_name"] = providerName
|
feat(go-cli): support batch model add/remove and optional embedding dimension (#15631)
## Summary
This PR improves the Go CLI in two areas:
1. It adds batch model management support, allowing multiple models to
be added or removed in a single command.
2. It makes the `dimension` argument optional for the `embed text`
command.
These changes keep the existing single-model and explicit-dimension
behaviors compatible while making the CLI more convenient for common
workflows.
## What Changed
### 1. Batch model add/remove support
The CLI now supports operating on multiple model names provided in a
single quoted string.
Supported commands include:
```
add model 'x1 x2 x3' to provider 'vllm' instance 'test' with tokens 1024 chat think vision, token 2048 chat, token 1024 think vision;
drop model 'x1 x2 x3' from 'vllm' 'test';
remove model 'x1 x2 x3' from 'vllm' 'test';
```
For add model, each config segment after with is matched to the
corresponding model name by position.
Example mapping:
- x1 -> tokens 1024, chat + vision, thinking=true
- x2 -> tokens 2048, chat
- x3 -> tokens 1024, vision, thinking=true
The existing single-model syntax remains supported.
### 2. Optional embedding dimension
Previously, the Go CLI required dimension to be explicitly provided for
embed text.
Before:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
Now both forms are supported:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
embed text 'what is rag' 'who are you' with 'model@test@provider';
When omitted, the CLI leaves dimension unset and relies on
provider/backend behavior.
## Tests
Added parser tests covering:
- Multiple models with multiple config segments
- Model type deduplication
- Model/config count mismatch
- Drop/remove multiple models
- Optional embedding dimension parsing
2026-06-05 19:31:06 +08:00
|
|
|
cmd.Params["model_names"] = modelNames
|
2026-04-29 19:18:49 +08:00
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-25 21:39:14 +08:00
|
|
|
func (p *Parser) parseIdentifierList() ([]string, error) {
|
|
|
|
|
var list []string
|
|
|
|
|
|
|
|
|
|
ident, err := p.parseIdentifier()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
list = append(list, ident)
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
for p.curToken.Type == TokenComma {
|
|
|
|
|
p.nextToken()
|
2026-06-22 17:33:47 +08:00
|
|
|
ident, err = p.parseIdentifier()
|
2026-03-25 21:39:14 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
list = append(list, ident)
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return list, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-24 18:48:09 +08:00
|
|
|
func (p *Parser) parseAPISetCommands() (*Command, error) {
|
2026-03-25 21:39:14 +08:00
|
|
|
p.nextToken() // consume SET
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type == TokenVar {
|
|
|
|
|
return p.parseSetVariable()
|
|
|
|
|
}
|
|
|
|
|
if p.curToken.Type == TokenDefault {
|
|
|
|
|
return p.parseSetDefault()
|
|
|
|
|
}
|
2026-06-24 18:48:09 +08:00
|
|
|
if p.curToken.Type == TokenKey {
|
|
|
|
|
return p.parseAPISetAPIKey()
|
2026-03-25 21:39:14 +08:00
|
|
|
}
|
2026-04-07 09:44:51 +08:00
|
|
|
if p.curToken.Type == TokenMetadata {
|
|
|
|
|
return p.parseSetMeta()
|
|
|
|
|
}
|
2026-04-08 19:32:53 +08:00
|
|
|
if p.curToken.Type == TokenLog {
|
|
|
|
|
return p.parseSetLog()
|
|
|
|
|
}
|
2026-03-25 21:39:14 +08:00
|
|
|
|
|
|
|
|
return nil, fmt.Errorf("unknown SET target: %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) parseSetVariable() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume VAR
|
|
|
|
|
varName, err := p.parseIdentifier()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
2026-05-18 01:08:45 -10:00
|
|
|
varValue, err := p.parseVariableValue()
|
2026-03-25 21:39:14 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("set_variable")
|
|
|
|
|
cmd.Params["var_name"] = varName
|
|
|
|
|
cmd.Params["var_value"] = varValue
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
// Semicolon is optional for UNSET TOKEN
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) parseSetDefault() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume DEFAULT
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
var modelType, modelNameOrID string
|
2026-04-17 18:05:33 +08:00
|
|
|
var err error
|
2026-03-25 21:39:14 +08:00
|
|
|
|
|
|
|
|
switch p.curToken.Type {
|
2026-04-20 15:31:12 +08:00
|
|
|
case TokenChat:
|
2026-04-17 18:05:33 +08:00
|
|
|
modelType = "chat"
|
2026-04-20 15:31:12 +08:00
|
|
|
case TokenVision:
|
|
|
|
|
modelType = "vision"
|
2026-03-25 21:39:14 +08:00
|
|
|
case TokenEmbedding:
|
2026-04-17 18:05:33 +08:00
|
|
|
modelType = "embedding"
|
2026-04-20 15:31:12 +08:00
|
|
|
case TokenRerank:
|
2026-04-17 18:05:33 +08:00
|
|
|
modelType = "rerank"
|
2026-03-25 21:39:14 +08:00
|
|
|
case TokenASR:
|
2026-04-17 18:05:33 +08:00
|
|
|
modelType = "asr"
|
2026-03-25 21:39:14 +08:00
|
|
|
case TokenTTS:
|
2026-04-17 18:05:33 +08:00
|
|
|
modelType = "tts"
|
2026-04-20 15:31:12 +08:00
|
|
|
case TokenOCR:
|
|
|
|
|
modelType = "ocr"
|
2026-03-25 21:39:14 +08:00
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("unknown model type: %s", p.curToken.Value)
|
|
|
|
|
}
|
2026-04-20 15:31:12 +08:00
|
|
|
p.nextToken() // pass model type
|
2026-03-25 21:39:14 +08:00
|
|
|
|
2026-04-20 15:31:12 +08:00
|
|
|
if p.curToken.Type != TokenModel {
|
|
|
|
|
return nil, fmt.Errorf("expected MODEL")
|
2026-04-17 18:05:33 +08:00
|
|
|
}
|
2026-04-20 15:31:12 +08:00
|
|
|
p.nextToken() // pass MODEL
|
2026-04-17 18:05:33 +08:00
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
// Format: 'model@instance@provider' or just 'message'
|
2026-04-20 15:31:12 +08:00
|
|
|
if p.curToken.Type != TokenQuotedString {
|
2026-06-15 10:10:14 +08:00
|
|
|
return nil, fmt.Errorf("expected quoted string with format model@instance@provider")
|
2026-04-17 18:05:33 +08:00
|
|
|
}
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
modelNameOrID, err = p.parseQuotedString()
|
2026-03-25 21:39:14 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-04-20 15:31:12 +08:00
|
|
|
p.nextToken()
|
2026-03-25 21:39:14 +08:00
|
|
|
|
|
|
|
|
cmd := NewCommand("set_default_model")
|
|
|
|
|
cmd.Params["model_type"] = modelType
|
2026-06-15 10:10:14 +08:00
|
|
|
|
|
|
|
|
if common.IsCompositeModelName(modelNameOrID) {
|
|
|
|
|
cmd.Params["composite_model_name"] = modelNameOrID
|
|
|
|
|
} else if common.IsUUID(modelNameOrID) {
|
|
|
|
|
cmd.Params["model_id"] = modelNameOrID
|
|
|
|
|
} else {
|
|
|
|
|
return nil, fmt.Errorf("invalid format of model name or ID: %s", modelNameOrID)
|
|
|
|
|
}
|
2026-03-25 21:39:14 +08:00
|
|
|
|
|
|
|
|
p.nextToken()
|
2026-06-24 18:48:09 +08:00
|
|
|
// Semicolon is optional
|
2026-03-25 21:39:14 +08:00
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-24 18:48:09 +08:00
|
|
|
func (p *Parser) parseAPISetAPIKey() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume KEY
|
2026-03-25 21:39:14 +08:00
|
|
|
|
2026-06-24 18:48:09 +08:00
|
|
|
apiKey, err := p.parseQuotedString()
|
2026-03-25 21:39:14 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-06-24 18:48:09 +08:00
|
|
|
p.nextToken()
|
2026-03-25 21:39:14 +08:00
|
|
|
|
2026-06-24 18:48:09 +08:00
|
|
|
cmd := NewCommand("api_set_api_key")
|
|
|
|
|
cmd.Params["api_key"] = apiKey
|
2026-03-25 21:39:14 +08:00
|
|
|
|
2026-06-24 18:48:09 +08:00
|
|
|
// Semicolon is optional
|
2026-03-25 21:39:14 +08:00
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 19:32:53 +08:00
|
|
|
func (p *Parser) parseSetLog() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume LOG
|
|
|
|
|
|
|
|
|
|
switch p.curToken.Type {
|
|
|
|
|
case TokenLevel:
|
|
|
|
|
return p.parseSetLogLevel()
|
|
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("unknown log target: %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) parseSetLogLevel() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume LEVEL
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("set_log_level")
|
|
|
|
|
switch p.curToken.Type {
|
|
|
|
|
case TokenDebug:
|
|
|
|
|
cmd.Params["level"] = "debug"
|
|
|
|
|
case TokenInfo:
|
|
|
|
|
cmd.Params["level"] = "info"
|
|
|
|
|
case TokenWarn:
|
|
|
|
|
cmd.Params["level"] = "warn"
|
|
|
|
|
case TokenError:
|
|
|
|
|
cmd.Params["level"] = "error"
|
|
|
|
|
case TokenFatal:
|
|
|
|
|
cmd.Params["level"] = "fatal"
|
|
|
|
|
case TokenPanic:
|
|
|
|
|
cmd.Params["level"] = "panic"
|
|
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("unknown log target: %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
// Semicolon is optional for UNSET TOKEN
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-25 21:39:14 +08:00
|
|
|
func (p *Parser) parseResetCommand() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume RESET
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenDefault {
|
|
|
|
|
return nil, fmt.Errorf("expected DEFAULT")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
var modelType string
|
|
|
|
|
switch p.curToken.Type {
|
2026-04-20 15:31:12 +08:00
|
|
|
case TokenChat:
|
|
|
|
|
modelType = "chat"
|
|
|
|
|
case TokenVision:
|
|
|
|
|
modelType = "vision"
|
2026-03-25 21:39:14 +08:00
|
|
|
case TokenEmbedding:
|
2026-04-20 15:31:12 +08:00
|
|
|
modelType = "embedding"
|
|
|
|
|
case TokenRerank:
|
|
|
|
|
modelType = "rerank"
|
2026-03-25 21:39:14 +08:00
|
|
|
case TokenASR:
|
2026-04-20 15:31:12 +08:00
|
|
|
modelType = "asr"
|
2026-03-25 21:39:14 +08:00
|
|
|
case TokenTTS:
|
2026-04-20 15:31:12 +08:00
|
|
|
modelType = "tts"
|
|
|
|
|
case TokenOCR:
|
|
|
|
|
modelType = "ocr"
|
2026-03-25 21:39:14 +08:00
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("unknown model type: %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("reset_default_model")
|
|
|
|
|
cmd.Params["model_type"] = modelType
|
|
|
|
|
p.nextToken()
|
2026-04-20 15:31:12 +08:00
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenModel {
|
|
|
|
|
return nil, fmt.Errorf("expected MODEL")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken() // pass MODEL
|
|
|
|
|
|
2026-03-25 21:39:14 +08:00
|
|
|
// Semicolon is optional for UNSET TOKEN
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) parseImportCommand() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume IMPORT
|
|
|
|
|
documentPaths, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
if p.curToken.Type != TokenInto {
|
|
|
|
|
return nil, fmt.Errorf("expected INTO")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
if p.curToken.Type != TokenDataset {
|
|
|
|
|
return nil, fmt.Errorf("expected DATASET")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
datasetName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("import_docs_into_dataset")
|
|
|
|
|
cmd.Params["document_paths"] = documentPaths
|
|
|
|
|
cmd.Params["dataset_name"] = datasetName
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
// Semicolon is optional for UNSET TOKEN
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 16:16:25 +08:00
|
|
|
// parseInsertCommand parses INSERT command and dispatches to specific handler
|
|
|
|
|
func (p *Parser) parseInsertCommand() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume INSERT
|
|
|
|
|
|
2026-05-25 19:15:07 +08:00
|
|
|
// Expect CHUNKS or METADATA
|
|
|
|
|
if p.curToken.Type == TokenChunks {
|
|
|
|
|
return p.parseInsertChunksFromFile()
|
2026-04-01 16:16:25 +08:00
|
|
|
}
|
|
|
|
|
if p.curToken.Type == TokenMetadata {
|
|
|
|
|
return p.parseInsertMetadataFromFile()
|
|
|
|
|
}
|
2026-05-25 19:15:07 +08:00
|
|
|
return nil, fmt.Errorf("expected CHUNKS or METADATA after INSERT, got %s", p.curToken.Value)
|
2026-04-01 16:16:25 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Internal CLI for GO
|
2026-05-25 19:15:07 +08:00
|
|
|
// parseInsertChunksFromFile parses: INSERT CHUNKS FROM FILE "file_path"
|
|
|
|
|
func (p *Parser) parseInsertChunksFromFile() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume CHUNKS
|
2026-04-01 16:16:25 +08:00
|
|
|
|
|
|
|
|
// Expect FROM
|
|
|
|
|
if p.curToken.Type != TokenFrom {
|
|
|
|
|
return nil, fmt.Errorf("expected FROM, got %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Expect FILE
|
|
|
|
|
if p.curToken.Type != TokenFile {
|
|
|
|
|
return nil, fmt.Errorf("expected FILE, got %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Get file path (quoted string)
|
|
|
|
|
filePath, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 19:15:07 +08:00
|
|
|
cmd := NewCommand("insert_chunks_from_file")
|
2026-04-01 16:16:25 +08:00
|
|
|
cmd.Params["file_path"] = filePath
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Internal CLI for GO
|
2026-04-24 15:30:14 +08:00
|
|
|
// parseInsertMetadataFromFile parses: INSERT METADATA FROM FILE "file_path"
|
2026-04-01 16:16:25 +08:00
|
|
|
func (p *Parser) parseInsertMetadataFromFile() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume METADATA
|
|
|
|
|
|
|
|
|
|
// Expect FROM
|
|
|
|
|
if p.curToken.Type != TokenFrom {
|
|
|
|
|
return nil, fmt.Errorf("expected FROM, got %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Expect FILE
|
|
|
|
|
if p.curToken.Type != TokenFile {
|
|
|
|
|
return nil, fmt.Errorf("expected FILE, got %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Get file path (quoted string)
|
|
|
|
|
filePath, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("insert_metadata_from_file")
|
|
|
|
|
cmd.Params["file_path"] = filePath
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-09 15:22:50 +08:00
|
|
|
func (p *Parser) parseRetrieveCommand() (*Command, error) {
|
2026-03-25 21:39:14 +08:00
|
|
|
p.nextToken() // consume SEARCH
|
2026-04-07 13:59:27 +08:00
|
|
|
|
2026-06-08 11:49:37 +08:00
|
|
|
// Handle help flag: -h / --help. The lexer tokenizes each leading
|
|
|
|
|
// `-` as a separate `TokenDash` and then the rest of the flag name
|
|
|
|
|
// (e.g. "help") as a `TokenIdentifier`. We collect any leading
|
|
|
|
|
// dashes before checking the identifier value. Short-circuit with
|
|
|
|
|
// a dedicated command type so the dispatcher can print the search
|
|
|
|
|
// usage instead of erroring out on the missing question.
|
|
|
|
|
if p.curToken.Type == TokenDash {
|
|
|
|
|
dashCount := 0
|
|
|
|
|
for p.curToken.Type == TokenDash {
|
|
|
|
|
dashCount++
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
if dashCount > 0 && p.curToken.Type == TokenIdentifier {
|
|
|
|
|
switch strings.ToLower(p.curToken.Value) {
|
|
|
|
|
case "h", "help":
|
|
|
|
|
return NewCommand("search_help"), nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil, fmt.Errorf("expected quoted string or identifier")
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 13:59:27 +08:00
|
|
|
var err error
|
|
|
|
|
var question string
|
|
|
|
|
if p.curToken.Type == TokenQuotedString {
|
|
|
|
|
question, err = p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
} else if p.curToken.Type == TokenIdentifier {
|
|
|
|
|
question, err = p.parseIdentifier()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
return nil, fmt.Errorf("expected quoted string or identifier")
|
2026-03-25 21:39:14 +08:00
|
|
|
}
|
2026-04-07 13:59:27 +08:00
|
|
|
|
2026-03-25 21:39:14 +08:00
|
|
|
p.nextToken()
|
2026-04-07 11:30:09 +08:00
|
|
|
|
|
|
|
|
if p.curToken.Type == TokenOn {
|
|
|
|
|
p.nextToken() // skip on
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenDatasets {
|
|
|
|
|
return nil, fmt.Errorf("expected DATASETS")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
datasets, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("search_on_datasets")
|
|
|
|
|
cmd.Params["question"] = question
|
|
|
|
|
cmd.Params["datasets"] = datasets
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
2026-06-08 11:49:37 +08:00
|
|
|
|
|
|
|
|
// Parse optional WITH clause for additional parameters
|
|
|
|
|
if p.curToken.Type == TokenWith || (p.curToken.Type == TokenIdentifier && strings.ToLower(p.curToken.Value) == "with") {
|
|
|
|
|
if p.curToken.Type == TokenWith {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
} else {
|
|
|
|
|
p.nextToken() // skip "with" identifier
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for p.curToken.Type != TokenEOF && p.curToken.Type != TokenSemicolon {
|
|
|
|
|
if p.curToken.Type == TokenComma {
|
|
|
|
|
return nil, fmt.Errorf("syntax error: WITH options must be space-separated, not comma-separated")
|
|
|
|
|
}
|
|
|
|
|
// Parse parameter name
|
|
|
|
|
if p.curToken.Type != TokenIdentifier {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
paramName := strings.ToLower(p.curToken.Value)
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Parse parameter value
|
|
|
|
|
var paramValue interface{}
|
|
|
|
|
valueToken := p.curToken.Type
|
|
|
|
|
var valueErr error
|
|
|
|
|
switch p.curToken.Type {
|
|
|
|
|
case TokenInteger:
|
|
|
|
|
paramValue, valueErr = p.parseNumber()
|
|
|
|
|
if valueErr != nil {
|
|
|
|
|
return nil, valueErr
|
|
|
|
|
}
|
|
|
|
|
p.nextToken() // step past the integer
|
|
|
|
|
case TokenFloat:
|
|
|
|
|
paramValue, valueErr = p.parseFloat()
|
|
|
|
|
if valueErr != nil {
|
|
|
|
|
return nil, valueErr
|
|
|
|
|
}
|
|
|
|
|
p.nextToken() // step past the float
|
|
|
|
|
case TokenQuotedString:
|
|
|
|
|
paramValue, valueErr = p.parseQuotedString()
|
|
|
|
|
if valueErr != nil {
|
|
|
|
|
return nil, valueErr
|
|
|
|
|
}
|
|
|
|
|
p.nextToken() // step past the closing quote
|
|
|
|
|
case TokenIdentifier:
|
|
|
|
|
// Bare identifiers are only meaningful for the
|
|
|
|
|
// boolean keys (keyword / use_kg = true|false);
|
|
|
|
|
// everything else rejects them.
|
|
|
|
|
paramValue = p.curToken.Value
|
|
|
|
|
p.nextToken()
|
|
|
|
|
case TokenLBracket:
|
|
|
|
|
// List value: parsed inside the switch below by
|
|
|
|
|
// cross_languages / doc_ids. No value is captured here.
|
|
|
|
|
paramValue = nil
|
|
|
|
|
default:
|
|
|
|
|
// EOF, ';', or any other non-value token: the option
|
|
|
|
|
// is missing a value, which is a hard error rather
|
|
|
|
|
// than a silent drop.
|
|
|
|
|
return nil, fmt.Errorf("WITH option %q is missing a value", paramName)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch paramName {
|
|
|
|
|
case "top_k", "page_size", "page":
|
|
|
|
|
if valueToken != TokenInteger {
|
|
|
|
|
return nil, fmt.Errorf("WITH option %q must be an integer, got %s", paramName, tokenTypeDescription(valueToken, p.curToken))
|
|
|
|
|
}
|
|
|
|
|
cmd.Params[paramName] = paramValue
|
|
|
|
|
case "similarity_threshold", "vector_similarity_weight":
|
|
|
|
|
switch n := paramValue.(type) {
|
|
|
|
|
case int:
|
|
|
|
|
cmd.Params[paramName] = float64(n)
|
|
|
|
|
case float64:
|
|
|
|
|
cmd.Params[paramName] = n
|
|
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("WITH option %q must be a number, got %s", paramName, tokenTypeDescription(valueToken, p.curToken))
|
|
|
|
|
}
|
|
|
|
|
case "keyword", "use_kg":
|
|
|
|
|
s, ok := paramValue.(string)
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, fmt.Errorf("WITH option %q must be true or false, got %s", paramName, tokenTypeDescription(valueToken, p.curToken))
|
|
|
|
|
}
|
|
|
|
|
switch strings.ToLower(s) {
|
|
|
|
|
case "true":
|
|
|
|
|
cmd.Params[paramName] = true
|
|
|
|
|
case "false":
|
|
|
|
|
cmd.Params[paramName] = false
|
|
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("WITH option %q must be true or false, got %q", paramName, s)
|
|
|
|
|
}
|
|
|
|
|
case "rerank_id", "tenant_rerank_id", "search_id", "meta_data_filter":
|
|
|
|
|
if valueToken != TokenQuotedString {
|
|
|
|
|
return nil, fmt.Errorf("WITH option %q must be a quoted string, got %s", paramName, tokenTypeDescription(valueToken, p.curToken))
|
|
|
|
|
}
|
|
|
|
|
// meta_data_filter JSON string is decoded into a map in
|
|
|
|
|
// the SearchOnDatasets handler; parser stores the raw
|
|
|
|
|
// string so the handler can surface a clean error on
|
|
|
|
|
// invalid JSON.
|
|
|
|
|
cmd.Params[paramName] = paramValue
|
|
|
|
|
case "cross_languages", "doc_ids":
|
|
|
|
|
if p.curToken.Type != TokenLBracket {
|
|
|
|
|
return nil, fmt.Errorf("WITH option %q must be a list, e.g. %q ['a', 'b']", paramName, paramName)
|
|
|
|
|
}
|
|
|
|
|
list, err := p.parseQuotedStringList()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
cmd.Params[paramName] = list
|
|
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("unknown WITH option %q", paramName)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Semicolon is optional
|
2026-04-07 11:30:09 +08:00
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
2026-03-25 21:39:14 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-30 12:36:03 +08:00
|
|
|
cmd := NewCommand("ce_search")
|
2026-04-07 11:30:09 +08:00
|
|
|
|
|
|
|
|
cmd.Params["query"] = question
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type == TokenEOF {
|
|
|
|
|
cmd.Params["path"] = "."
|
|
|
|
|
return cmd, nil
|
2026-03-25 21:39:14 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-07 11:30:09 +08:00
|
|
|
for p.curToken.Type != TokenEOF {
|
|
|
|
|
if p.curToken.Type == TokenDash {
|
|
|
|
|
p.nextToken() // skip dash
|
|
|
|
|
if p.curToken.Type != TokenIdentifier {
|
|
|
|
|
return nil, fmt.Errorf("expect identifier")
|
|
|
|
|
}
|
2026-03-25 21:39:14 +08:00
|
|
|
|
2026-04-07 11:30:09 +08:00
|
|
|
if strings.ToLower(p.curToken.Value) == "n" {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
var err error
|
2026-04-07 15:09:45 +08:00
|
|
|
if p.curToken.Type != TokenInteger {
|
2026-04-07 11:30:09 +08:00
|
|
|
return nil, fmt.Errorf("expect number")
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["number"], err = p.parseNumber()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 13:59:27 +08:00
|
|
|
//if strings.ToLower(p.curToken.Value) == "t" {
|
|
|
|
|
// p.nextToken()
|
|
|
|
|
// var err error
|
2026-04-07 15:09:45 +08:00
|
|
|
// if p.curToken.Type != TokenInteger {
|
2026-04-07 13:59:27 +08:00
|
|
|
// return nil, fmt.Errorf("expect number")
|
|
|
|
|
// }
|
|
|
|
|
// cmd.Params["threshold"], err = p.parseFloat()
|
|
|
|
|
// if err != nil {
|
|
|
|
|
// return nil, err
|
|
|
|
|
// }
|
|
|
|
|
// p.nextToken()
|
|
|
|
|
// continue
|
|
|
|
|
//}
|
2026-04-07 11:30:09 +08:00
|
|
|
|
|
|
|
|
return nil, fmt.Errorf("unknow parameter: %s", p.curToken.Value)
|
|
|
|
|
} else if p.curToken.Type == TokenIdentifier {
|
|
|
|
|
if cmd.Params["path"] == nil {
|
|
|
|
|
cmd.Params["path"] = p.curToken.Value
|
|
|
|
|
} else {
|
2026-04-07 13:59:27 +08:00
|
|
|
cmd.Params["path"] = fmt.Sprintf("%s%s", cmd.Params["path"], p.curToken.Value)
|
2026-04-07 11:30:09 +08:00
|
|
|
}
|
|
|
|
|
p.nextToken() // skip path
|
|
|
|
|
continue
|
2026-04-07 13:59:27 +08:00
|
|
|
} else if p.curToken.Type == TokenSlash {
|
|
|
|
|
if cmd.Params["path"] == nil {
|
|
|
|
|
cmd.Params["path"] = "/"
|
|
|
|
|
} else {
|
|
|
|
|
cmd.Params["path"] = fmt.Sprintf("%s/", cmd.Params["path"])
|
|
|
|
|
}
|
|
|
|
|
p.nextToken() // skip slash
|
|
|
|
|
if p.curToken.Type == TokenIdentifier {
|
|
|
|
|
cmd.Params["path"] = fmt.Sprintf("%s%s", cmd.Params["path"], p.curToken.Value)
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
continue
|
2026-06-18 18:07:27 +08:00
|
|
|
} else if p.curToken.Type == TokenIllegal && p.curToken.Value == "." {
|
|
|
|
|
if cmd.Params["path"] == nil {
|
|
|
|
|
cmd.Params["path"] = "."
|
|
|
|
|
} else {
|
|
|
|
|
cmd.Params["path"] = fmt.Sprintf("%s.", cmd.Params["path"])
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
continue
|
|
|
|
|
} else {
|
|
|
|
|
return nil, fmt.Errorf("unexpected token %q in search path", p.curToken.Value)
|
2026-04-07 11:30:09 +08:00
|
|
|
}
|
2026-03-25 21:39:14 +08:00
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 20:20:35 +08:00
|
|
|
func (p *Parser) parseListModelsOfProvider() (*Command, error) {
|
2026-04-21 16:52:32 +08:00
|
|
|
|
|
|
|
|
if p.curToken.Type == TokenSupported {
|
|
|
|
|
// List supported models
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("list_supported_models")
|
|
|
|
|
if p.curToken.Type != TokenModels {
|
|
|
|
|
return nil, fmt.Errorf("expected MODELS")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenFrom {
|
|
|
|
|
return nil, fmt.Errorf("expected FROM")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenQuotedString {
|
|
|
|
|
return nil, fmt.Errorf("expected quoted string for provider name")
|
|
|
|
|
}
|
|
|
|
|
firstName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenQuotedString {
|
|
|
|
|
return nil, fmt.Errorf("expected quoted string for instance name")
|
|
|
|
|
}
|
|
|
|
|
secondName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
cmd.Params["provider_name"] = firstName
|
|
|
|
|
cmd.Params["instance_name"] = secondName
|
|
|
|
|
|
|
|
|
|
// Semicolon is optional for UNSET TOKEN
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 20:20:35 +08:00
|
|
|
if p.curToken.Type != TokenModels {
|
|
|
|
|
return nil, fmt.Errorf("expected MODELS")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
2026-04-21 16:52:32 +08:00
|
|
|
|
2026-04-02 20:20:35 +08:00
|
|
|
if p.curToken.Type != TokenFrom {
|
2026-06-08 21:38:15 +08:00
|
|
|
// LIST MODELS
|
|
|
|
|
cmd := NewCommand("list_all_models")
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
2026-04-02 20:20:35 +08:00
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Parse first quoted string (could be instance_name or provider_name)
|
|
|
|
|
firstName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Check if there's a second quoted string (provider_name)
|
|
|
|
|
// If so, format is: LIST MODELS FROM <instance_name> <provider_name>
|
|
|
|
|
// If not, format is: LIST MODELS FROM <provider_name>
|
|
|
|
|
if p.curToken.Type == TokenQuotedString {
|
2026-06-23 16:57:05 +08:00
|
|
|
var instanceName string
|
2026-04-02 20:20:35 +08:00
|
|
|
// Two arguments: instance_name and provider_name
|
2026-06-23 16:57:05 +08:00
|
|
|
instanceName, err = p.parseQuotedString()
|
2026-04-02 20:20:35 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
cmd := NewCommand("list_instance_models")
|
|
|
|
|
cmd.Params["instance_name"] = instanceName
|
|
|
|
|
cmd.Params["provider_name"] = firstName
|
|
|
|
|
p.nextToken()
|
|
|
|
|
// Semicolon is optional for UNSET TOKEN
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Only one argument: provider_name
|
|
|
|
|
cmd := NewCommand("list_provider_models")
|
|
|
|
|
cmd.Params["provider_name"] = firstName
|
|
|
|
|
// Semicolon is optional for UNSET TOKEN
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) parseEnableCommand() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume ENABLE
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenModel {
|
|
|
|
|
return nil, fmt.Errorf("expected MODEL")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
modelName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenFrom {
|
|
|
|
|
return nil, fmt.Errorf("expected FROM")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
modelProvider, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
modelInstance, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Semicolon is optional for UNSET TOKEN
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("enable_model")
|
|
|
|
|
cmd.Params["model_name"] = modelName
|
|
|
|
|
cmd.Params["instance_name"] = modelInstance
|
|
|
|
|
cmd.Params["provider_name"] = modelProvider
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) parseDisableCommand() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume DISABLE
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenModel {
|
|
|
|
|
return nil, fmt.Errorf("expected MODEL")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
modelName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenFrom {
|
|
|
|
|
return nil, fmt.Errorf("expected FROM")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
modelProvider, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
modelInstance, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Semicolon is optional for UNSET TOKEN
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("disable_model")
|
|
|
|
|
cmd.Params["model_name"] = modelName
|
|
|
|
|
cmd.Params["instance_name"] = modelInstance
|
|
|
|
|
cmd.Params["provider_name"] = modelProvider
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
Go: CLI chat with text, image, video (#14573)
### What problem does this PR solve?
```
RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the pics talk about?' image 'https://cdn.bigmodel.cn/static/logo/register.png' 'https://cdn.bigmodel.cn/static/logo/api-key.png'
Answer: The first picture shows a login/register modal with options for phone number login, account login, and WeChat QR code login, along with a prompt for new users to get a 20 million tokens experience package. The second picture displays the API keys management page of a platform, including a warning about API key security and a table listing existing API keys with details like creation time and usage history.
Time: 31.600545
RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the video talk about?' video 'https://cdn.bigmodel.cn/agent-demos/lark/113123.mov'
Answer: Based on the sequence of frames provided, the video is a demonstration of a web search and navigation process.
1. The video starts with a blank Google search page.
2. The user types "智谱" (which is the Chinese name for the company Zhipu AI) into the search box.
3. The search is initiated and the page shows "About 0 results".
4. The search results load, showing information about Zhipu AI, including its website.
5. The user clicks on the main website link (www.zhipuai.cn).
6. The video ends by showing the homepage of Zhipu AI's website, titled "Z.ai GLM Large Model Open Platform".
In summary, the video is about searching for the company "智谱" (Zhipu AI) on Google and then navigating to its official website.
Time: 76.582520
```
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-05-05 18:14:39 +08:00
|
|
|
// CHAT 'model@instance@provider' 'hello world'
|
|
|
|
|
// CHAT WITH 'model@instance@provider' MESSAGE 'hello world' 'who are you' IMAGE 'url1' 'file0' VIDEO "url2.mov" "file1" FILE "url" "path file2" AUDIO "file.wav"
|
2026-04-02 20:20:35 +08:00
|
|
|
func (p *Parser) parseChatCommand() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume CHAT
|
|
|
|
|
|
Go: CLI chat with text, image, video (#14573)
### What problem does this PR solve?
```
RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the pics talk about?' image 'https://cdn.bigmodel.cn/static/logo/register.png' 'https://cdn.bigmodel.cn/static/logo/api-key.png'
Answer: The first picture shows a login/register modal with options for phone number login, account login, and WeChat QR code login, along with a prompt for new users to get a 20 million tokens experience package. The second picture displays the API keys management page of a platform, including a warning about API key security and a table listing existing API keys with details like creation time and usage history.
Time: 31.600545
RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the video talk about?' video 'https://cdn.bigmodel.cn/agent-demos/lark/113123.mov'
Answer: Based on the sequence of frames provided, the video is a demonstration of a web search and navigation process.
1. The video starts with a blank Google search page.
2. The user types "智谱" (which is the Chinese name for the company Zhipu AI) into the search box.
3. The search is initiated and the page shows "About 0 results".
4. The search results load, showing information about Zhipu AI, including its website.
5. The user clicks on the main website link (www.zhipuai.cn).
6. The video ends by showing the homepage of Zhipu AI's website, titled "Z.ai GLM Large Model Open Platform".
In summary, the video is about searching for the company "智谱" (Zhipu AI) on Google and then navigating to its official website.
Time: 76.582520
```
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-05-05 18:14:39 +08:00
|
|
|
var err error
|
2026-06-15 10:10:14 +08:00
|
|
|
var modelNameOrID string = ""
|
Go: CLI chat with text, image, video (#14573)
### What problem does this PR solve?
```
RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the pics talk about?' image 'https://cdn.bigmodel.cn/static/logo/register.png' 'https://cdn.bigmodel.cn/static/logo/api-key.png'
Answer: The first picture shows a login/register modal with options for phone number login, account login, and WeChat QR code login, along with a prompt for new users to get a 20 million tokens experience package. The second picture displays the API keys management page of a platform, including a warning about API key security and a table listing existing API keys with details like creation time and usage history.
Time: 31.600545
RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the video talk about?' video 'https://cdn.bigmodel.cn/agent-demos/lark/113123.mov'
Answer: Based on the sequence of frames provided, the video is a demonstration of a web search and navigation process.
1. The video starts with a blank Google search page.
2. The user types "智谱" (which is the Chinese name for the company Zhipu AI) into the search box.
3. The search is initiated and the page shows "About 0 results".
4. The search results load, showing information about Zhipu AI, including its website.
5. The user clicks on the main website link (www.zhipuai.cn).
6. The video ends by showing the homepage of Zhipu AI's website, titled "Z.ai GLM Large Model Open Platform".
In summary, the video is about searching for the company "智谱" (Zhipu AI) on Google and then navigating to its official website.
Time: 76.582520
```
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-05-05 18:14:39 +08:00
|
|
|
var messages []string
|
|
|
|
|
var images []string
|
|
|
|
|
var videos []string
|
|
|
|
|
var audios []string
|
|
|
|
|
var files []string
|
|
|
|
|
effort := "default"
|
|
|
|
|
verbosity := "low"
|
2026-04-03 18:11:23 +08:00
|
|
|
|
Go: CLI chat with text, image, video (#14573)
### What problem does this PR solve?
```
RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the pics talk about?' image 'https://cdn.bigmodel.cn/static/logo/register.png' 'https://cdn.bigmodel.cn/static/logo/api-key.png'
Answer: The first picture shows a login/register modal with options for phone number login, account login, and WeChat QR code login, along with a prompt for new users to get a 20 million tokens experience package. The second picture displays the API keys management page of a platform, including a warning about API key security and a table listing existing API keys with details like creation time and usage history.
Time: 31.600545
RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the video talk about?' video 'https://cdn.bigmodel.cn/agent-demos/lark/113123.mov'
Answer: Based on the sequence of frames provided, the video is a demonstration of a web search and navigation process.
1. The video starts with a blank Google search page.
2. The user types "智谱" (which is the Chinese name for the company Zhipu AI) into the search box.
3. The search is initiated and the page shows "About 0 results".
4. The search results load, showing information about Zhipu AI, including its website.
5. The user clicks on the main website link (www.zhipuai.cn).
6. The video ends by showing the homepage of Zhipu AI's website, titled "Z.ai GLM Large Model Open Platform".
In summary, the video is about searching for the company "智谱" (Zhipu AI) on Google and then navigating to its official website.
Time: 76.582520
```
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-05-05 18:14:39 +08:00
|
|
|
optionsLoop:
|
|
|
|
|
for {
|
|
|
|
|
switch p.curToken.Type {
|
|
|
|
|
case TokenWith:
|
2026-04-02 20:20:35 +08:00
|
|
|
p.nextToken()
|
2026-06-15 10:10:14 +08:00
|
|
|
// 'model@instance@provider' or model ID
|
|
|
|
|
if modelNameOrID != "" {
|
|
|
|
|
return nil, fmt.Errorf("model name or ID is already set")
|
Go: CLI chat with text, image, video (#14573)
### What problem does this PR solve?
```
RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the pics talk about?' image 'https://cdn.bigmodel.cn/static/logo/register.png' 'https://cdn.bigmodel.cn/static/logo/api-key.png'
Answer: The first picture shows a login/register modal with options for phone number login, account login, and WeChat QR code login, along with a prompt for new users to get a 20 million tokens experience package. The second picture displays the API keys management page of a platform, including a warning about API key security and a table listing existing API keys with details like creation time and usage history.
Time: 31.600545
RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the video talk about?' video 'https://cdn.bigmodel.cn/agent-demos/lark/113123.mov'
Answer: Based on the sequence of frames provided, the video is a demonstration of a web search and navigation process.
1. The video starts with a blank Google search page.
2. The user types "智谱" (which is the Chinese name for the company Zhipu AI) into the search box.
3. The search is initiated and the page shows "About 0 results".
4. The search results load, showing information about Zhipu AI, including its website.
5. The user clicks on the main website link (www.zhipuai.cn).
6. The video ends by showing the homepage of Zhipu AI's website, titled "Z.ai GLM Large Model Open Platform".
In summary, the video is about searching for the company "智谱" (Zhipu AI) on Google and then navigating to its official website.
Time: 76.582520
```
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-05-05 18:14:39 +08:00
|
|
|
}
|
2026-06-15 10:10:14 +08:00
|
|
|
modelNameOrID, err = p.parseQuotedString()
|
Go: CLI chat with text, image, video (#14573)
### What problem does this PR solve?
```
RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the pics talk about?' image 'https://cdn.bigmodel.cn/static/logo/register.png' 'https://cdn.bigmodel.cn/static/logo/api-key.png'
Answer: The first picture shows a login/register modal with options for phone number login, account login, and WeChat QR code login, along with a prompt for new users to get a 20 million tokens experience package. The second picture displays the API keys management page of a platform, including a warning about API key security and a table listing existing API keys with details like creation time and usage history.
Time: 31.600545
RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the video talk about?' video 'https://cdn.bigmodel.cn/agent-demos/lark/113123.mov'
Answer: Based on the sequence of frames provided, the video is a demonstration of a web search and navigation process.
1. The video starts with a blank Google search page.
2. The user types "智谱" (which is the Chinese name for the company Zhipu AI) into the search box.
3. The search is initiated and the page shows "About 0 results".
4. The search results load, showing information about Zhipu AI, including its website.
5. The user clicks on the main website link (www.zhipuai.cn).
6. The video ends by showing the homepage of Zhipu AI's website, titled "Z.ai GLM Large Model Open Platform".
In summary, the video is about searching for the company "智谱" (Zhipu AI) on Google and then navigating to its official website.
Time: 76.582520
```
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-05-05 18:14:39 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
2026-04-02 20:20:35 +08:00
|
|
|
}
|
|
|
|
|
p.nextToken()
|
Go: CLI chat with text, image, video (#14573)
### What problem does this PR solve?
```
RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the pics talk about?' image 'https://cdn.bigmodel.cn/static/logo/register.png' 'https://cdn.bigmodel.cn/static/logo/api-key.png'
Answer: The first picture shows a login/register modal with options for phone number login, account login, and WeChat QR code login, along with a prompt for new users to get a 20 million tokens experience package. The second picture displays the API keys management page of a platform, including a warning about API key security and a table listing existing API keys with details like creation time and usage history.
Time: 31.600545
RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the video talk about?' video 'https://cdn.bigmodel.cn/agent-demos/lark/113123.mov'
Answer: Based on the sequence of frames provided, the video is a demonstration of a web search and navigation process.
1. The video starts with a blank Google search page.
2. The user types "智谱" (which is the Chinese name for the company Zhipu AI) into the search box.
3. The search is initiated and the page shows "About 0 results".
4. The search results load, showing information about Zhipu AI, including its website.
5. The user clicks on the main website link (www.zhipuai.cn).
6. The video ends by showing the homepage of Zhipu AI's website, titled "Z.ai GLM Large Model Open Platform".
In summary, the video is about searching for the company "智谱" (Zhipu AI) on Google and then navigating to its official website.
Time: 76.582520
```
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-05-05 18:14:39 +08:00
|
|
|
case TokenMessage:
|
2026-04-02 20:20:35 +08:00
|
|
|
p.nextToken()
|
Go: CLI chat with text, image, video (#14573)
### What problem does this PR solve?
```
RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the pics talk about?' image 'https://cdn.bigmodel.cn/static/logo/register.png' 'https://cdn.bigmodel.cn/static/logo/api-key.png'
Answer: The first picture shows a login/register modal with options for phone number login, account login, and WeChat QR code login, along with a prompt for new users to get a 20 million tokens experience package. The second picture displays the API keys management page of a platform, including a warning about API key security and a table listing existing API keys with details like creation time and usage history.
Time: 31.600545
RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the video talk about?' video 'https://cdn.bigmodel.cn/agent-demos/lark/113123.mov'
Answer: Based on the sequence of frames provided, the video is a demonstration of a web search and navigation process.
1. The video starts with a blank Google search page.
2. The user types "智谱" (which is the Chinese name for the company Zhipu AI) into the search box.
3. The search is initiated and the page shows "About 0 results".
4. The search results load, showing information about Zhipu AI, including its website.
5. The user clicks on the main website link (www.zhipuai.cn).
6. The video ends by showing the homepage of Zhipu AI's website, titled "Z.ai GLM Large Model Open Platform".
In summary, the video is about searching for the company "智谱" (Zhipu AI) on Google and then navigating to its official website.
Time: 76.582520
```
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-05-05 18:14:39 +08:00
|
|
|
if len(messages) != 0 {
|
|
|
|
|
return nil, fmt.Errorf("message is already set")
|
|
|
|
|
}
|
|
|
|
|
messageLoop:
|
|
|
|
|
for {
|
|
|
|
|
if p.curToken.Type != TokenQuotedString {
|
|
|
|
|
break messageLoop
|
2026-04-24 20:59:30 +08:00
|
|
|
}
|
Go: CLI chat with text, image, video (#14573)
### What problem does this PR solve?
```
RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the pics talk about?' image 'https://cdn.bigmodel.cn/static/logo/register.png' 'https://cdn.bigmodel.cn/static/logo/api-key.png'
Answer: The first picture shows a login/register modal with options for phone number login, account login, and WeChat QR code login, along with a prompt for new users to get a 20 million tokens experience package. The second picture displays the API keys management page of a platform, including a warning about API key security and a table listing existing API keys with details like creation time and usage history.
Time: 31.600545
RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the video talk about?' video 'https://cdn.bigmodel.cn/agent-demos/lark/113123.mov'
Answer: Based on the sequence of frames provided, the video is a demonstration of a web search and navigation process.
1. The video starts with a blank Google search page.
2. The user types "智谱" (which is the Chinese name for the company Zhipu AI) into the search box.
3. The search is initiated and the page shows "About 0 results".
4. The search results load, showing information about Zhipu AI, including its website.
5. The user clicks on the main website link (www.zhipuai.cn).
6. The video ends by showing the homepage of Zhipu AI's website, titled "Z.ai GLM Large Model Open Platform".
In summary, the video is about searching for the company "智谱" (Zhipu AI) on Google and then navigating to its official website.
Time: 76.582520
```
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-05-05 18:14:39 +08:00
|
|
|
var message string
|
|
|
|
|
message, err = p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
message = strings.TrimSpace(message)
|
|
|
|
|
messages = append(messages, message)
|
2026-04-24 20:59:30 +08:00
|
|
|
p.nextToken()
|
|
|
|
|
}
|
Go: CLI chat with text, image, video (#14573)
### What problem does this PR solve?
```
RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the pics talk about?' image 'https://cdn.bigmodel.cn/static/logo/register.png' 'https://cdn.bigmodel.cn/static/logo/api-key.png'
Answer: The first picture shows a login/register modal with options for phone number login, account login, and WeChat QR code login, along with a prompt for new users to get a 20 million tokens experience package. The second picture displays the API keys management page of a platform, including a warning about API key security and a table listing existing API keys with details like creation time and usage history.
Time: 31.600545
RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the video talk about?' video 'https://cdn.bigmodel.cn/agent-demos/lark/113123.mov'
Answer: Based on the sequence of frames provided, the video is a demonstration of a web search and navigation process.
1. The video starts with a blank Google search page.
2. The user types "智谱" (which is the Chinese name for the company Zhipu AI) into the search box.
3. The search is initiated and the page shows "About 0 results".
4. The search results load, showing information about Zhipu AI, including its website.
5. The user clicks on the main website link (www.zhipuai.cn).
6. The video ends by showing the homepage of Zhipu AI's website, titled "Z.ai GLM Large Model Open Platform".
In summary, the video is about searching for the company "智谱" (Zhipu AI) on Google and then navigating to its official website.
Time: 76.582520
```
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-05-05 18:14:39 +08:00
|
|
|
case TokenImage:
|
|
|
|
|
p.nextToken()
|
|
|
|
|
if len(images) != 0 {
|
|
|
|
|
return nil, fmt.Errorf("image is already set")
|
|
|
|
|
}
|
|
|
|
|
imageLoop:
|
|
|
|
|
for {
|
|
|
|
|
if p.curToken.Type != TokenQuotedString {
|
|
|
|
|
break imageLoop
|
|
|
|
|
}
|
|
|
|
|
var image string
|
|
|
|
|
image, err = p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
images = append(images, image)
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
case TokenVideo:
|
|
|
|
|
p.nextToken()
|
|
|
|
|
if len(videos) != 0 {
|
|
|
|
|
return nil, fmt.Errorf("video is already set")
|
|
|
|
|
}
|
|
|
|
|
videoLoop:
|
|
|
|
|
for {
|
|
|
|
|
if p.curToken.Type != TokenQuotedString {
|
|
|
|
|
break videoLoop
|
2026-04-24 20:59:30 +08:00
|
|
|
}
|
Go: CLI chat with text, image, video (#14573)
### What problem does this PR solve?
```
RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the pics talk about?' image 'https://cdn.bigmodel.cn/static/logo/register.png' 'https://cdn.bigmodel.cn/static/logo/api-key.png'
Answer: The first picture shows a login/register modal with options for phone number login, account login, and WeChat QR code login, along with a prompt for new users to get a 20 million tokens experience package. The second picture displays the API keys management page of a platform, including a warning about API key security and a table listing existing API keys with details like creation time and usage history.
Time: 31.600545
RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the video talk about?' video 'https://cdn.bigmodel.cn/agent-demos/lark/113123.mov'
Answer: Based on the sequence of frames provided, the video is a demonstration of a web search and navigation process.
1. The video starts with a blank Google search page.
2. The user types "智谱" (which is the Chinese name for the company Zhipu AI) into the search box.
3. The search is initiated and the page shows "About 0 results".
4. The search results load, showing information about Zhipu AI, including its website.
5. The user clicks on the main website link (www.zhipuai.cn).
6. The video ends by showing the homepage of Zhipu AI's website, titled "Z.ai GLM Large Model Open Platform".
In summary, the video is about searching for the company "智谱" (Zhipu AI) on Google and then navigating to its official website.
Time: 76.582520
```
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-05-05 18:14:39 +08:00
|
|
|
var video string
|
|
|
|
|
video, err = p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
videos = append(videos, video)
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
case TokenAudio:
|
|
|
|
|
p.nextToken()
|
|
|
|
|
if len(audios) != 0 {
|
|
|
|
|
return nil, fmt.Errorf("video is already set")
|
|
|
|
|
}
|
|
|
|
|
audioLoop:
|
|
|
|
|
for {
|
|
|
|
|
if p.curToken.Type != TokenQuotedString {
|
|
|
|
|
break audioLoop
|
|
|
|
|
}
|
|
|
|
|
var audio string
|
|
|
|
|
audio, err = p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
audios = append(audios, audio)
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
case TokenFile:
|
|
|
|
|
p.nextToken()
|
|
|
|
|
if len(files) != 0 {
|
|
|
|
|
return nil, fmt.Errorf("video is already set")
|
|
|
|
|
}
|
|
|
|
|
fileLoop:
|
|
|
|
|
for {
|
|
|
|
|
if p.curToken.Type != TokenQuotedString {
|
|
|
|
|
break fileLoop
|
|
|
|
|
}
|
|
|
|
|
var file string
|
|
|
|
|
file, err = p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
files = append(files, file)
|
2026-04-24 20:59:30 +08:00
|
|
|
p.nextToken()
|
|
|
|
|
}
|
Go: CLI chat with text, image, video (#14573)
### What problem does this PR solve?
```
RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the pics talk about?' image 'https://cdn.bigmodel.cn/static/logo/register.png' 'https://cdn.bigmodel.cn/static/logo/api-key.png'
Answer: The first picture shows a login/register modal with options for phone number login, account login, and WeChat QR code login, along with a prompt for new users to get a 20 million tokens experience package. The second picture displays the API keys management page of a platform, including a warning about API key security and a table listing existing API keys with details like creation time and usage history.
Time: 31.600545
RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the video talk about?' video 'https://cdn.bigmodel.cn/agent-demos/lark/113123.mov'
Answer: Based on the sequence of frames provided, the video is a demonstration of a web search and navigation process.
1. The video starts with a blank Google search page.
2. The user types "智谱" (which is the Chinese name for the company Zhipu AI) into the search box.
3. The search is initiated and the page shows "About 0 results".
4. The search results load, showing information about Zhipu AI, including its website.
5. The user clicks on the main website link (www.zhipuai.cn).
6. The video ends by showing the homepage of Zhipu AI's website, titled "Z.ai GLM Large Model Open Platform".
In summary, the video is about searching for the company "智谱" (Zhipu AI) on Google and then navigating to its official website.
Time: 76.582520
```
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-05-05 18:14:39 +08:00
|
|
|
case TokenEffort:
|
|
|
|
|
p.nextToken() // pass Effort
|
|
|
|
|
switch p.curToken.Type {
|
|
|
|
|
case TokenNone:
|
|
|
|
|
effort = "none"
|
|
|
|
|
case TokenMinimal:
|
|
|
|
|
effort = "minimal"
|
|
|
|
|
case TokenLow:
|
|
|
|
|
effort = "low"
|
|
|
|
|
case TokenMedium:
|
|
|
|
|
effort = "medium"
|
|
|
|
|
case TokenHigh:
|
|
|
|
|
effort = "high"
|
|
|
|
|
case TokenMax:
|
|
|
|
|
effort = "max"
|
|
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("invalid effort level")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
break optionsLoop
|
|
|
|
|
case TokenVerbosity:
|
|
|
|
|
p.nextToken() // pass VERBOSITY
|
|
|
|
|
switch p.curToken.Type {
|
|
|
|
|
case TokenLow:
|
|
|
|
|
verbosity = "low"
|
|
|
|
|
case TokenMedium:
|
|
|
|
|
verbosity = "median"
|
|
|
|
|
case TokenHigh:
|
|
|
|
|
verbosity = "high"
|
|
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("invalid verbosity level")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
break optionsLoop
|
|
|
|
|
case TokenSemicolon:
|
|
|
|
|
p.nextToken()
|
|
|
|
|
break optionsLoop // done
|
2026-04-24 20:59:30 +08:00
|
|
|
default:
|
Go: CLI chat with text, image, video (#14573)
### What problem does this PR solve?
```
RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the pics talk about?' image 'https://cdn.bigmodel.cn/static/logo/register.png' 'https://cdn.bigmodel.cn/static/logo/api-key.png'
Answer: The first picture shows a login/register modal with options for phone number login, account login, and WeChat QR code login, along with a prompt for new users to get a 20 million tokens experience package. The second picture displays the API keys management page of a platform, including a warning about API key security and a table listing existing API keys with details like creation time and usage history.
Time: 31.600545
RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the video talk about?' video 'https://cdn.bigmodel.cn/agent-demos/lark/113123.mov'
Answer: Based on the sequence of frames provided, the video is a demonstration of a web search and navigation process.
1. The video starts with a blank Google search page.
2. The user types "智谱" (which is the Chinese name for the company Zhipu AI) into the search box.
3. The search is initiated and the page shows "About 0 results".
4. The search results load, showing information about Zhipu AI, including its website.
5. The user clicks on the main website link (www.zhipuai.cn).
6. The video ends by showing the homepage of Zhipu AI's website, titled "Z.ai GLM Large Model Open Platform".
In summary, the video is about searching for the company "智谱" (Zhipu AI) on Google and then navigating to its official website.
Time: 76.582520
```
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-05-05 18:14:39 +08:00
|
|
|
// No more options to process
|
|
|
|
|
break optionsLoop
|
2026-04-24 20:59:30 +08:00
|
|
|
}
|
|
|
|
|
}
|
Go: CLI chat with text, image, video (#14573)
### What problem does this PR solve?
```
RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the pics talk about?' image 'https://cdn.bigmodel.cn/static/logo/register.png' 'https://cdn.bigmodel.cn/static/logo/api-key.png'
Answer: The first picture shows a login/register modal with options for phone number login, account login, and WeChat QR code login, along with a prompt for new users to get a 20 million tokens experience package. The second picture displays the API keys management page of a platform, including a warning about API key security and a table listing existing API keys with details like creation time and usage history.
Time: 31.600545
RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the video talk about?' video 'https://cdn.bigmodel.cn/agent-demos/lark/113123.mov'
Answer: Based on the sequence of frames provided, the video is a demonstration of a web search and navigation process.
1. The video starts with a blank Google search page.
2. The user types "智谱" (which is the Chinese name for the company Zhipu AI) into the search box.
3. The search is initiated and the page shows "About 0 results".
4. The search results load, showing information about Zhipu AI, including its website.
5. The user clicks on the main website link (www.zhipuai.cn).
6. The video ends by showing the homepage of Zhipu AI's website, titled "Z.ai GLM Large Model Open Platform".
In summary, the video is about searching for the company "智谱" (Zhipu AI) on Google and then navigating to its official website.
Time: 76.582520
```
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-05-05 18:14:39 +08:00
|
|
|
cmd := NewCommand("chat_to_model")
|
2026-04-24 20:59:30 +08:00
|
|
|
|
2026-06-22 18:14:58 +08:00
|
|
|
if modelNameOrID != "" {
|
|
|
|
|
if common.IsCompositeModelName(modelNameOrID) {
|
|
|
|
|
cmd.Params["composite_model_name"] = modelNameOrID
|
|
|
|
|
} else if common.IsUUID(modelNameOrID) {
|
|
|
|
|
cmd.Params["model_id"] = modelNameOrID
|
|
|
|
|
} else {
|
|
|
|
|
return nil, fmt.Errorf("invalid format of model name or ID: %s", modelNameOrID)
|
|
|
|
|
}
|
2026-06-15 10:10:14 +08:00
|
|
|
}
|
Go: CLI chat with text, image, video (#14573)
### What problem does this PR solve?
```
RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the pics talk about?' image 'https://cdn.bigmodel.cn/static/logo/register.png' 'https://cdn.bigmodel.cn/static/logo/api-key.png'
Answer: The first picture shows a login/register modal with options for phone number login, account login, and WeChat QR code login, along with a prompt for new users to get a 20 million tokens experience package. The second picture displays the API keys management page of a platform, including a warning about API key security and a table listing existing API keys with details like creation time and usage history.
Time: 31.600545
RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the video talk about?' video 'https://cdn.bigmodel.cn/agent-demos/lark/113123.mov'
Answer: Based on the sequence of frames provided, the video is a demonstration of a web search and navigation process.
1. The video starts with a blank Google search page.
2. The user types "智谱" (which is the Chinese name for the company Zhipu AI) into the search box.
3. The search is initiated and the page shows "About 0 results".
4. The search results load, showing information about Zhipu AI, including its website.
5. The user clicks on the main website link (www.zhipuai.cn).
6. The video ends by showing the homepage of Zhipu AI's website, titled "Z.ai GLM Large Model Open Platform".
In summary, the video is about searching for the company "智谱" (Zhipu AI) on Google and then navigating to its official website.
Time: 76.582520
```
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-05-05 18:14:39 +08:00
|
|
|
cmd.Params["messages"] = messages
|
|
|
|
|
cmd.Params["images"] = images
|
|
|
|
|
cmd.Params["videos"] = videos
|
|
|
|
|
cmd.Params["audios"] = audios
|
|
|
|
|
cmd.Params["files"] = files
|
2026-04-21 16:52:32 +08:00
|
|
|
cmd.Params["thinking"] = false
|
|
|
|
|
cmd.Params["stream"] = false
|
2026-04-24 20:59:30 +08:00
|
|
|
cmd.Params["effort"] = effort
|
|
|
|
|
cmd.Params["verbosity"] = verbosity
|
2026-04-02 20:20:35 +08:00
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 18:11:23 +08:00
|
|
|
func (p *Parser) parseThinkCommand() (*Command, error) {
|
|
|
|
|
|
|
|
|
|
p.nextToken() // consume THINK
|
2026-04-21 16:52:32 +08:00
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenChat {
|
|
|
|
|
return nil, fmt.Errorf("expected CHAT after THINK")
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 18:11:23 +08:00
|
|
|
command, err := p.parseChatCommand()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-04-21 16:52:32 +08:00
|
|
|
command.Params["thinking"] = true
|
|
|
|
|
return command, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) parseStreamCommand() (*Command, error) {
|
|
|
|
|
|
|
|
|
|
p.nextToken() // consume STREAM
|
|
|
|
|
|
|
|
|
|
var command *Command
|
|
|
|
|
var err error
|
|
|
|
|
|
2026-05-12 17:17:44 +08:00
|
|
|
switch p.curToken.Type {
|
|
|
|
|
case TokenChat:
|
2026-04-21 16:52:32 +08:00
|
|
|
command, err = p.parseChatCommand()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-05-12 17:17:44 +08:00
|
|
|
case TokenThink:
|
2026-04-21 16:52:32 +08:00
|
|
|
command, err = p.parseThinkCommand()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-05-12 17:17:44 +08:00
|
|
|
case TokenASR:
|
|
|
|
|
command, err = p.parseASRCommand()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
case TokenTTS:
|
|
|
|
|
command, err = p.parseTTSCommand()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("expected CHAT, THINK, ASR, or TTS after STREAM")
|
2026-04-21 16:52:32 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
command.Params["stream"] = true
|
2026-04-03 18:11:23 +08:00
|
|
|
return command, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-09 17:41:54 +08:00
|
|
|
func (p *Parser) parseEmbedCommand() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume EMBED
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenText {
|
|
|
|
|
return nil, fmt.Errorf("expected WITH after EMBED")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken() // consume TEXT
|
|
|
|
|
|
|
|
|
|
var texts []string
|
|
|
|
|
|
|
|
|
|
textLoop:
|
|
|
|
|
for {
|
|
|
|
|
if p.curToken.Type != TokenQuotedString {
|
|
|
|
|
break textLoop
|
|
|
|
|
}
|
|
|
|
|
text, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
text = strings.TrimSpace(text)
|
|
|
|
|
texts = append(texts, text)
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenWith {
|
|
|
|
|
return nil, fmt.Errorf("expected WITH after EMBED")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken() // consume WITH
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
modelNameOrID, err := p.parseQuotedString()
|
2026-05-09 17:41:54 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
feat(go-cli): support batch model add/remove and optional embedding dimension (#15631)
## Summary
This PR improves the Go CLI in two areas:
1. It adds batch model management support, allowing multiple models to
be added or removed in a single command.
2. It makes the `dimension` argument optional for the `embed text`
command.
These changes keep the existing single-model and explicit-dimension
behaviors compatible while making the CLI more convenient for common
workflows.
## What Changed
### 1. Batch model add/remove support
The CLI now supports operating on multiple model names provided in a
single quoted string.
Supported commands include:
```
add model 'x1 x2 x3' to provider 'vllm' instance 'test' with tokens 1024 chat think vision, token 2048 chat, token 1024 think vision;
drop model 'x1 x2 x3' from 'vllm' 'test';
remove model 'x1 x2 x3' from 'vllm' 'test';
```
For add model, each config segment after with is matched to the
corresponding model name by position.
Example mapping:
- x1 -> tokens 1024, chat + vision, thinking=true
- x2 -> tokens 2048, chat
- x3 -> tokens 1024, vision, thinking=true
The existing single-model syntax remains supported.
### 2. Optional embedding dimension
Previously, the Go CLI required dimension to be explicitly provided for
embed text.
Before:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
Now both forms are supported:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
embed text 'what is rag' 'who are you' with 'model@test@provider';
When omitted, the CLI leaves dimension unset and relies on
provider/backend behavior.
## Tests
Added parser tests covering:
- Multiple models with multiple config segments
- Model type deduplication
- Model/config count mismatch
- Drop/remove multiple models
- Optional embedding dimension parsing
2026-06-05 19:31:06 +08:00
|
|
|
dimension := 0
|
|
|
|
|
if p.curToken.Type == TokenDimension {
|
|
|
|
|
p.nextToken() // consume DIMENSION
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenInteger {
|
|
|
|
|
return nil, fmt.Errorf("expected integer after DIMENSION")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var err error
|
|
|
|
|
dimension, err = p.parseNumber()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
2026-05-09 17:41:54 +08:00
|
|
|
}
|
|
|
|
|
|
feat(go-cli): support batch model add/remove and optional embedding dimension (#15631)
## Summary
This PR improves the Go CLI in two areas:
1. It adds batch model management support, allowing multiple models to
be added or removed in a single command.
2. It makes the `dimension` argument optional for the `embed text`
command.
These changes keep the existing single-model and explicit-dimension
behaviors compatible while making the CLI more convenient for common
workflows.
## What Changed
### 1. Batch model add/remove support
The CLI now supports operating on multiple model names provided in a
single quoted string.
Supported commands include:
```
add model 'x1 x2 x3' to provider 'vllm' instance 'test' with tokens 1024 chat think vision, token 2048 chat, token 1024 think vision;
drop model 'x1 x2 x3' from 'vllm' 'test';
remove model 'x1 x2 x3' from 'vllm' 'test';
```
For add model, each config segment after with is matched to the
corresponding model name by position.
Example mapping:
- x1 -> tokens 1024, chat + vision, thinking=true
- x2 -> tokens 2048, chat
- x3 -> tokens 1024, vision, thinking=true
The existing single-model syntax remains supported.
### 2. Optional embedding dimension
Previously, the Go CLI required dimension to be explicitly provided for
embed text.
Before:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
Now both forms are supported:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
embed text 'what is rag' 'who are you' with 'model@test@provider';
When omitted, the CLI leaves dimension unset and relies on
provider/backend behavior.
## Tests
Added parser tests covering:
- Multiple models with multiple config segments
- Model type deduplication
- Model/config count mismatch
- Drop/remove multiple models
- Optional embedding dimension parsing
2026-06-05 19:31:06 +08:00
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
if p.curToken.Type != TokenEOF {
|
|
|
|
|
return nil, fmt.Errorf("unexpected token after embed command: %s", p.curToken.Value)
|
2026-05-09 17:41:54 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("embed_user_text")
|
2026-06-15 10:10:14 +08:00
|
|
|
if common.IsCompositeModelName(modelNameOrID) {
|
|
|
|
|
cmd.Params["composite_model_name"] = modelNameOrID
|
|
|
|
|
} else if common.IsUUID(modelNameOrID) {
|
|
|
|
|
cmd.Params["model_id"] = modelNameOrID
|
|
|
|
|
} else {
|
|
|
|
|
return nil, fmt.Errorf("invalid format of model name or ID: %s", modelNameOrID)
|
|
|
|
|
}
|
2026-05-09 17:41:54 +08:00
|
|
|
cmd.Params["texts"] = texts
|
feat(go-cli): support batch model add/remove and optional embedding dimension (#15631)
## Summary
This PR improves the Go CLI in two areas:
1. It adds batch model management support, allowing multiple models to
be added or removed in a single command.
2. It makes the `dimension` argument optional for the `embed text`
command.
These changes keep the existing single-model and explicit-dimension
behaviors compatible while making the CLI more convenient for common
workflows.
## What Changed
### 1. Batch model add/remove support
The CLI now supports operating on multiple model names provided in a
single quoted string.
Supported commands include:
```
add model 'x1 x2 x3' to provider 'vllm' instance 'test' with tokens 1024 chat think vision, token 2048 chat, token 1024 think vision;
drop model 'x1 x2 x3' from 'vllm' 'test';
remove model 'x1 x2 x3' from 'vllm' 'test';
```
For add model, each config segment after with is matched to the
corresponding model name by position.
Example mapping:
- x1 -> tokens 1024, chat + vision, thinking=true
- x2 -> tokens 2048, chat
- x3 -> tokens 1024, vision, thinking=true
The existing single-model syntax remains supported.
### 2. Optional embedding dimension
Previously, the Go CLI required dimension to be explicitly provided for
embed text.
Before:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
Now both forms are supported:
embed text 'what is rag' 'who are you' with 'model@test@provider'
dimension 8192;
embed text 'what is rag' 'who are you' with 'model@test@provider';
When omitted, the CLI leaves dimension unset and relies on
provider/backend behavior.
## Tests
Added parser tests covering:
- Multiple models with multiple config segments
- Model type deduplication
- Model/config count mismatch
- Drop/remove multiple models
- Optional embedding dimension parsing
2026-06-05 19:31:06 +08:00
|
|
|
if dimension > 0 {
|
|
|
|
|
cmd.Params["dimension"] = dimension
|
|
|
|
|
}
|
2026-05-09 17:41:54 +08:00
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) parseRerankCommand() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume RERANK
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenQuery {
|
|
|
|
|
return nil, fmt.Errorf("expected WITH after EMBED")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken() // consume QUERY
|
|
|
|
|
|
|
|
|
|
query, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
query = strings.TrimSpace(query)
|
|
|
|
|
p.nextToken() // consume query
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenDocument {
|
|
|
|
|
return nil, fmt.Errorf("expected DOCUMENT after query")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken() // consume DOCUMENT
|
|
|
|
|
|
|
|
|
|
var documents []string
|
|
|
|
|
|
|
|
|
|
documentLoop:
|
|
|
|
|
for {
|
|
|
|
|
if p.curToken.Type != TokenQuotedString {
|
|
|
|
|
break documentLoop
|
|
|
|
|
}
|
|
|
|
|
var document string
|
|
|
|
|
document, err = p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
document = strings.TrimSpace(document)
|
|
|
|
|
documents = append(documents, document)
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenWith {
|
|
|
|
|
return nil, fmt.Errorf("expected WITH after EMBED")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken() // consume WITH
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
modelNameOrID, err := p.parseQuotedString()
|
2026-05-09 17:41:54 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenTop {
|
|
|
|
|
return nil, fmt.Errorf("expected TOP after model")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
topN, err := p.parseNumber()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("rarank_user_document")
|
2026-06-15 10:10:14 +08:00
|
|
|
if common.IsCompositeModelName(modelNameOrID) {
|
|
|
|
|
cmd.Params["composite_model_name"] = modelNameOrID
|
|
|
|
|
} else if common.IsUUID(modelNameOrID) {
|
|
|
|
|
cmd.Params["model_id"] = modelNameOrID
|
|
|
|
|
} else {
|
|
|
|
|
return nil, fmt.Errorf("invalid format of model name or ID: %s", modelNameOrID)
|
|
|
|
|
}
|
2026-05-09 17:41:54 +08:00
|
|
|
cmd.Params["query"] = query
|
|
|
|
|
cmd.Params["documents"] = documents
|
|
|
|
|
cmd.Params["top_n"] = topN
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-12 17:17:44 +08:00
|
|
|
func (p *Parser) parseASRCommand() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume ASR
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenWith {
|
|
|
|
|
return nil, fmt.Errorf("expected WITH after ASR")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken() // consume WITH
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
modelNameOrID, err := p.parseQuotedString()
|
2026-05-12 17:17:44 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenAudio {
|
|
|
|
|
return nil, fmt.Errorf("expected AUDIO to ASR")
|
|
|
|
|
}
|
Go: implement TTS for fishaudio, openrouter and asr for fishaudio (#14926)
### What problem does this PR solve?
This PR implement TTS for FishAudio and MiniMax provider and ASR for
FishAudio
**The following functionalities are now supported:**
**FishAudio:**
- [x] Text To Speech
- [x] Stream Text To Speech
- [x] Audio To Text
**OpenRouter:**
- [x] Text To Speech
**Verified examples from the CLI:**
```plaintext
**FishAudio**
RAGFlow(user)> tts with 's1@test@fishaudio' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"reference_id": "90e65eaaf50e4470b8e6d43ee6afd7d5", "temperature": 0.7, "top_p": 0.7, "prosody": {"speed": 1, "volume": 0, "normalize_loudness": true}, "chunk_length": 300, "normalize": true, "sample_rate": 44100, "mp3_bitrate": 128, "latency": "normal", "max_new_tokens": 1024, "repetition_penalty": 1.2, "min_chunk_length": 50, "condition_on_previous_chunks": true, "early_stop_threshold": 1}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/s1_output.wav
SUCCESS
RAGFlow(user)> stream tts with 's1@test@fishaudio' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"reference_id": "90e65eaaf50e4470b8e6d43ee6afd7d5", "temperature": 0.7, "top_p": 0.7, "prosody": {"speed": 1, "volume": 0, "normalize_loudness": true}, "chunk_length": 300, "normalize": true, "sample_rate": 44100, "mp3_bitrate": 128, "latency": "normal", "max_new_tokens": 1024, "repetition_penalty": 1.2, "min_chunk_length": 50, "condition_on_previous_chunks": true, "early_stop_threshold": 1}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/s1_output.wav
SUCCESS
RAGFlow(user)> asr with 'transcribe-1@test@fishaudio' audio './internal/test.wav' param '{"language": "en", "ignore_timestamps": true}'
+----------------------------------------------------------------------------------------------------------------------+
| text |
+----------------------------------------------------------------------------------------------------------------------+
| The examination and testimony of the experts enabled the commission to conclude that five shots may have been fired. |
+----------------------------------------------------------------------------------------------------------------------+
```
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] New Feature (non-breaking change which adds functionality)
- [x] Refactoring
2026-05-14 18:58:00 +08:00
|
|
|
p.nextToken() // consume AUDIO
|
2026-05-12 17:17:44 +08:00
|
|
|
|
|
|
|
|
audioFile, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
Go: implement TTS for fishaudio, openrouter and asr for fishaudio (#14926)
### What problem does this PR solve?
This PR implement TTS for FishAudio and MiniMax provider and ASR for
FishAudio
**The following functionalities are now supported:**
**FishAudio:**
- [x] Text To Speech
- [x] Stream Text To Speech
- [x] Audio To Text
**OpenRouter:**
- [x] Text To Speech
**Verified examples from the CLI:**
```plaintext
**FishAudio**
RAGFlow(user)> tts with 's1@test@fishaudio' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"reference_id": "90e65eaaf50e4470b8e6d43ee6afd7d5", "temperature": 0.7, "top_p": 0.7, "prosody": {"speed": 1, "volume": 0, "normalize_loudness": true}, "chunk_length": 300, "normalize": true, "sample_rate": 44100, "mp3_bitrate": 128, "latency": "normal", "max_new_tokens": 1024, "repetition_penalty": 1.2, "min_chunk_length": 50, "condition_on_previous_chunks": true, "early_stop_threshold": 1}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/s1_output.wav
SUCCESS
RAGFlow(user)> stream tts with 's1@test@fishaudio' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"reference_id": "90e65eaaf50e4470b8e6d43ee6afd7d5", "temperature": 0.7, "top_p": 0.7, "prosody": {"speed": 1, "volume": 0, "normalize_loudness": true}, "chunk_length": 300, "normalize": true, "sample_rate": 44100, "mp3_bitrate": 128, "latency": "normal", "max_new_tokens": 1024, "repetition_penalty": 1.2, "min_chunk_length": 50, "condition_on_previous_chunks": true, "early_stop_threshold": 1}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/s1_output.wav
SUCCESS
RAGFlow(user)> asr with 'transcribe-1@test@fishaudio' audio './internal/test.wav' param '{"language": "en", "ignore_timestamps": true}'
+----------------------------------------------------------------------------------------------------------------------+
| text |
+----------------------------------------------------------------------------------------------------------------------+
| The examination and testimony of the experts enabled the commission to conclude that five shots may have been fired. |
+----------------------------------------------------------------------------------------------------------------------+
```
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] New Feature (non-breaking change which adds functionality)
- [x] Refactoring
2026-05-14 18:58:00 +08:00
|
|
|
cmd := NewCommand("asr_user_command")
|
2026-06-15 10:10:14 +08:00
|
|
|
if common.IsCompositeModelName(modelNameOrID) {
|
|
|
|
|
cmd.Params["composite_model_name"] = modelNameOrID
|
|
|
|
|
} else if common.IsUUID(modelNameOrID) {
|
|
|
|
|
cmd.Params["model_id"] = modelNameOrID
|
|
|
|
|
} else {
|
|
|
|
|
return nil, fmt.Errorf("invalid format of model name or ID: %s", modelNameOrID)
|
|
|
|
|
}
|
Go: implement TTS for fishaudio, openrouter and asr for fishaudio (#14926)
### What problem does this PR solve?
This PR implement TTS for FishAudio and MiniMax provider and ASR for
FishAudio
**The following functionalities are now supported:**
**FishAudio:**
- [x] Text To Speech
- [x] Stream Text To Speech
- [x] Audio To Text
**OpenRouter:**
- [x] Text To Speech
**Verified examples from the CLI:**
```plaintext
**FishAudio**
RAGFlow(user)> tts with 's1@test@fishaudio' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"reference_id": "90e65eaaf50e4470b8e6d43ee6afd7d5", "temperature": 0.7, "top_p": 0.7, "prosody": {"speed": 1, "volume": 0, "normalize_loudness": true}, "chunk_length": 300, "normalize": true, "sample_rate": 44100, "mp3_bitrate": 128, "latency": "normal", "max_new_tokens": 1024, "repetition_penalty": 1.2, "min_chunk_length": 50, "condition_on_previous_chunks": true, "early_stop_threshold": 1}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/s1_output.wav
SUCCESS
RAGFlow(user)> stream tts with 's1@test@fishaudio' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"reference_id": "90e65eaaf50e4470b8e6d43ee6afd7d5", "temperature": 0.7, "top_p": 0.7, "prosody": {"speed": 1, "volume": 0, "normalize_loudness": true}, "chunk_length": 300, "normalize": true, "sample_rate": 44100, "mp3_bitrate": 128, "latency": "normal", "max_new_tokens": 1024, "repetition_penalty": 1.2, "min_chunk_length": 50, "condition_on_previous_chunks": true, "early_stop_threshold": 1}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/s1_output.wav
SUCCESS
RAGFlow(user)> asr with 'transcribe-1@test@fishaudio' audio './internal/test.wav' param '{"language": "en", "ignore_timestamps": true}'
+----------------------------------------------------------------------------------------------------------------------+
| text |
+----------------------------------------------------------------------------------------------------------------------+
| The examination and testimony of the experts enabled the commission to conclude that five shots may have been fired. |
+----------------------------------------------------------------------------------------------------------------------+
```
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] New Feature (non-breaking change which adds functionality)
- [x] Refactoring
2026-05-14 18:58:00 +08:00
|
|
|
cmd.Params["audio_file"] = audioFile
|
|
|
|
|
|
|
|
|
|
for p.curToken.Type != TokenEOF && p.curToken.Type != TokenSemicolon {
|
|
|
|
|
switch p.curToken.Type {
|
|
|
|
|
case TokenParam:
|
|
|
|
|
p.nextToken()
|
|
|
|
|
if p.curToken.Type != TokenQuotedString {
|
|
|
|
|
return nil, fmt.Errorf("expect quoted string after 'param'")
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["param_str"] = strings.Trim(p.curToken.Value, "\"'")
|
|
|
|
|
p.nextToken()
|
|
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("unexpected token in asr command: %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-12 17:17:44 +08:00
|
|
|
// Semicolon is optional for UNSET TOKEN
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) parseTTSCommand() (*Command, error) {
|
Go: implement TTS for MiniMax provider and CLI testing for TTS (#14911)
### What problem does this PR solve?
This PR implement TTS for MiniMax provider and CLI testing for TTS
**The following functionalities are now supported:**
**MiniMax:**
- [x] Chat / Stream Chat
- [x] Embedding
- [x] Rerank
- [x] Model listing
- [x] Provider connection checking
- [x] Text To Speech
- [ ] OCRFile
- [ ] ~~Audio To Text~~
- [ ] ~~Balance~~
**Verified examples from the CLI:**
```plaintext
RAGFlow(user)> tts with 'speech-2.8-hd@test@minimax' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"voice_setting": {"voice_id": "English_radiant_girl", "speed": 1, "vol": 1, "pitch": 0}, "audio_setting": {"sample_rate": 32000, "bitrate": 128000, "format": "wav", "channel": 1}, "output_format": "hex"}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/speech-2.8-hd_output.wav
SUCCESS
RAGFlow(user)> stream tts with 'speech-2.8-hd@test@minimax' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"voice_setting": {"voice_id": "English_radiant_girl", "speed": 1, "vol": 1, "pitch": 0}, "audio_setting": {"sample_rate": 32000, "bitrate": 128000, "format": "wav", "channel": 1}, "output_format": "hex"}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/speech-2.8-hd_output.wav
SUCCESS
```
Set `Play` to play audio in CLI
Set `Save` `PATH_TO_SAVE` to save file
Set `format` to save file in wav or mp3
Set `Param` align with official request body
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
2026-05-14 13:19:31 +08:00
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("tts_user_command")
|
2026-05-12 17:17:44 +08:00
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenWith {
|
Go: implement TTS for MiniMax provider and CLI testing for TTS (#14911)
### What problem does this PR solve?
This PR implement TTS for MiniMax provider and CLI testing for TTS
**The following functionalities are now supported:**
**MiniMax:**
- [x] Chat / Stream Chat
- [x] Embedding
- [x] Rerank
- [x] Model listing
- [x] Provider connection checking
- [x] Text To Speech
- [ ] OCRFile
- [ ] ~~Audio To Text~~
- [ ] ~~Balance~~
**Verified examples from the CLI:**
```plaintext
RAGFlow(user)> tts with 'speech-2.8-hd@test@minimax' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"voice_setting": {"voice_id": "English_radiant_girl", "speed": 1, "vol": 1, "pitch": 0}, "audio_setting": {"sample_rate": 32000, "bitrate": 128000, "format": "wav", "channel": 1}, "output_format": "hex"}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/speech-2.8-hd_output.wav
SUCCESS
RAGFlow(user)> stream tts with 'speech-2.8-hd@test@minimax' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"voice_setting": {"voice_id": "English_radiant_girl", "speed": 1, "vol": 1, "pitch": 0}, "audio_setting": {"sample_rate": 32000, "bitrate": 128000, "format": "wav", "channel": 1}, "output_format": "hex"}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/speech-2.8-hd_output.wav
SUCCESS
```
Set `Play` to play audio in CLI
Set `Save` `PATH_TO_SAVE` to save file
Set `format` to save file in wav or mp3
Set `Param` align with official request body
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
2026-05-14 13:19:31 +08:00
|
|
|
return nil, fmt.Errorf("expect 'with' after tts")
|
2026-05-12 17:17:44 +08:00
|
|
|
}
|
Go: implement TTS for MiniMax provider and CLI testing for TTS (#14911)
### What problem does this PR solve?
This PR implement TTS for MiniMax provider and CLI testing for TTS
**The following functionalities are now supported:**
**MiniMax:**
- [x] Chat / Stream Chat
- [x] Embedding
- [x] Rerank
- [x] Model listing
- [x] Provider connection checking
- [x] Text To Speech
- [ ] OCRFile
- [ ] ~~Audio To Text~~
- [ ] ~~Balance~~
**Verified examples from the CLI:**
```plaintext
RAGFlow(user)> tts with 'speech-2.8-hd@test@minimax' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"voice_setting": {"voice_id": "English_radiant_girl", "speed": 1, "vol": 1, "pitch": 0}, "audio_setting": {"sample_rate": 32000, "bitrate": 128000, "format": "wav", "channel": 1}, "output_format": "hex"}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/speech-2.8-hd_output.wav
SUCCESS
RAGFlow(user)> stream tts with 'speech-2.8-hd@test@minimax' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"voice_setting": {"voice_id": "English_radiant_girl", "speed": 1, "vol": 1, "pitch": 0}, "audio_setting": {"sample_rate": 32000, "bitrate": 128000, "format": "wav", "channel": 1}, "output_format": "hex"}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/speech-2.8-hd_output.wav
SUCCESS
```
Set `Play` to play audio in CLI
Set `Save` `PATH_TO_SAVE` to save file
Set `format` to save file in wav or mp3
Set `Param` align with official request body
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
2026-05-14 13:19:31 +08:00
|
|
|
p.nextToken()
|
2026-05-12 17:17:44 +08:00
|
|
|
|
Go: implement TTS for MiniMax provider and CLI testing for TTS (#14911)
### What problem does this PR solve?
This PR implement TTS for MiniMax provider and CLI testing for TTS
**The following functionalities are now supported:**
**MiniMax:**
- [x] Chat / Stream Chat
- [x] Embedding
- [x] Rerank
- [x] Model listing
- [x] Provider connection checking
- [x] Text To Speech
- [ ] OCRFile
- [ ] ~~Audio To Text~~
- [ ] ~~Balance~~
**Verified examples from the CLI:**
```plaintext
RAGFlow(user)> tts with 'speech-2.8-hd@test@minimax' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"voice_setting": {"voice_id": "English_radiant_girl", "speed": 1, "vol": 1, "pitch": 0}, "audio_setting": {"sample_rate": 32000, "bitrate": 128000, "format": "wav", "channel": 1}, "output_format": "hex"}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/speech-2.8-hd_output.wav
SUCCESS
RAGFlow(user)> stream tts with 'speech-2.8-hd@test@minimax' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"voice_setting": {"voice_id": "English_radiant_girl", "speed": 1, "vol": 1, "pitch": 0}, "audio_setting": {"sample_rate": 32000, "bitrate": 128000, "format": "wav", "channel": 1}, "output_format": "hex"}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/speech-2.8-hd_output.wav
SUCCESS
```
Set `Play` to play audio in CLI
Set `Save` `PATH_TO_SAVE` to save file
Set `format` to save file in wav or mp3
Set `Param` align with official request body
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
2026-05-14 13:19:31 +08:00
|
|
|
if p.curToken.Type != TokenQuotedString && p.curToken.Type != TokenIdentifier {
|
|
|
|
|
return nil, fmt.Errorf("expect model name after 'with'")
|
2026-05-12 17:17:44 +08:00
|
|
|
}
|
2026-06-15 10:10:14 +08:00
|
|
|
|
|
|
|
|
modelNameOrID := strings.Trim(p.curToken.Value, "\"'")
|
2026-05-12 17:17:44 +08:00
|
|
|
p.nextToken()
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
if common.IsCompositeModelName(modelNameOrID) {
|
|
|
|
|
cmd.Params["composite_model_name"] = modelNameOrID
|
|
|
|
|
} else if common.IsUUID(modelNameOrID) {
|
|
|
|
|
cmd.Params["model_id"] = modelNameOrID
|
|
|
|
|
} else {
|
|
|
|
|
return nil, fmt.Errorf("invalid format of model name or ID: %s", modelNameOrID)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-12 17:17:44 +08:00
|
|
|
if p.curToken.Type != TokenText {
|
Go: implement TTS for MiniMax provider and CLI testing for TTS (#14911)
### What problem does this PR solve?
This PR implement TTS for MiniMax provider and CLI testing for TTS
**The following functionalities are now supported:**
**MiniMax:**
- [x] Chat / Stream Chat
- [x] Embedding
- [x] Rerank
- [x] Model listing
- [x] Provider connection checking
- [x] Text To Speech
- [ ] OCRFile
- [ ] ~~Audio To Text~~
- [ ] ~~Balance~~
**Verified examples from the CLI:**
```plaintext
RAGFlow(user)> tts with 'speech-2.8-hd@test@minimax' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"voice_setting": {"voice_id": "English_radiant_girl", "speed": 1, "vol": 1, "pitch": 0}, "audio_setting": {"sample_rate": 32000, "bitrate": 128000, "format": "wav", "channel": 1}, "output_format": "hex"}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/speech-2.8-hd_output.wav
SUCCESS
RAGFlow(user)> stream tts with 'speech-2.8-hd@test@minimax' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"voice_setting": {"voice_id": "English_radiant_girl", "speed": 1, "vol": 1, "pitch": 0}, "audio_setting": {"sample_rate": 32000, "bitrate": 128000, "format": "wav", "channel": 1}, "output_format": "hex"}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/speech-2.8-hd_output.wav
SUCCESS
```
Set `Play` to play audio in CLI
Set `Save` `PATH_TO_SAVE` to save file
Set `format` to save file in wav or mp3
Set `Param` align with official request body
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
2026-05-14 13:19:31 +08:00
|
|
|
return nil, fmt.Errorf("expect 'text' parameter")
|
2026-05-12 17:17:44 +08:00
|
|
|
}
|
Go: implement TTS for MiniMax provider and CLI testing for TTS (#14911)
### What problem does this PR solve?
This PR implement TTS for MiniMax provider and CLI testing for TTS
**The following functionalities are now supported:**
**MiniMax:**
- [x] Chat / Stream Chat
- [x] Embedding
- [x] Rerank
- [x] Model listing
- [x] Provider connection checking
- [x] Text To Speech
- [ ] OCRFile
- [ ] ~~Audio To Text~~
- [ ] ~~Balance~~
**Verified examples from the CLI:**
```plaintext
RAGFlow(user)> tts with 'speech-2.8-hd@test@minimax' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"voice_setting": {"voice_id": "English_radiant_girl", "speed": 1, "vol": 1, "pitch": 0}, "audio_setting": {"sample_rate": 32000, "bitrate": 128000, "format": "wav", "channel": 1}, "output_format": "hex"}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/speech-2.8-hd_output.wav
SUCCESS
RAGFlow(user)> stream tts with 'speech-2.8-hd@test@minimax' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"voice_setting": {"voice_id": "English_radiant_girl", "speed": 1, "vol": 1, "pitch": 0}, "audio_setting": {"sample_rate": 32000, "bitrate": 128000, "format": "wav", "channel": 1}, "output_format": "hex"}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/speech-2.8-hd_output.wav
SUCCESS
```
Set `Play` to play audio in CLI
Set `Save` `PATH_TO_SAVE` to save file
Set `format` to save file in wav or mp3
Set `Param` align with official request body
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
2026-05-14 13:19:31 +08:00
|
|
|
p.nextToken()
|
2026-05-12 17:17:44 +08:00
|
|
|
|
Go: implement TTS for MiniMax provider and CLI testing for TTS (#14911)
### What problem does this PR solve?
This PR implement TTS for MiniMax provider and CLI testing for TTS
**The following functionalities are now supported:**
**MiniMax:**
- [x] Chat / Stream Chat
- [x] Embedding
- [x] Rerank
- [x] Model listing
- [x] Provider connection checking
- [x] Text To Speech
- [ ] OCRFile
- [ ] ~~Audio To Text~~
- [ ] ~~Balance~~
**Verified examples from the CLI:**
```plaintext
RAGFlow(user)> tts with 'speech-2.8-hd@test@minimax' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"voice_setting": {"voice_id": "English_radiant_girl", "speed": 1, "vol": 1, "pitch": 0}, "audio_setting": {"sample_rate": 32000, "bitrate": 128000, "format": "wav", "channel": 1}, "output_format": "hex"}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/speech-2.8-hd_output.wav
SUCCESS
RAGFlow(user)> stream tts with 'speech-2.8-hd@test@minimax' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"voice_setting": {"voice_id": "English_radiant_girl", "speed": 1, "vol": 1, "pitch": 0}, "audio_setting": {"sample_rate": 32000, "bitrate": 128000, "format": "wav", "channel": 1}, "output_format": "hex"}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/speech-2.8-hd_output.wav
SUCCESS
```
Set `Play` to play audio in CLI
Set `Save` `PATH_TO_SAVE` to save file
Set `format` to save file in wav or mp3
Set `Param` align with official request body
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
2026-05-14 13:19:31 +08:00
|
|
|
if p.curToken.Type != TokenQuotedString {
|
|
|
|
|
return nil, fmt.Errorf("expect quoted string after 'text'")
|
2026-05-12 17:17:44 +08:00
|
|
|
}
|
Go: implement TTS for MiniMax provider and CLI testing for TTS (#14911)
### What problem does this PR solve?
This PR implement TTS for MiniMax provider and CLI testing for TTS
**The following functionalities are now supported:**
**MiniMax:**
- [x] Chat / Stream Chat
- [x] Embedding
- [x] Rerank
- [x] Model listing
- [x] Provider connection checking
- [x] Text To Speech
- [ ] OCRFile
- [ ] ~~Audio To Text~~
- [ ] ~~Balance~~
**Verified examples from the CLI:**
```plaintext
RAGFlow(user)> tts with 'speech-2.8-hd@test@minimax' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"voice_setting": {"voice_id": "English_radiant_girl", "speed": 1, "vol": 1, "pitch": 0}, "audio_setting": {"sample_rate": 32000, "bitrate": 128000, "format": "wav", "channel": 1}, "output_format": "hex"}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/speech-2.8-hd_output.wav
SUCCESS
RAGFlow(user)> stream tts with 'speech-2.8-hd@test@minimax' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"voice_setting": {"voice_id": "English_radiant_girl", "speed": 1, "vol": 1, "pitch": 0}, "audio_setting": {"sample_rate": 32000, "bitrate": 128000, "format": "wav", "channel": 1}, "output_format": "hex"}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/speech-2.8-hd_output.wav
SUCCESS
```
Set `Play` to play audio in CLI
Set `Save` `PATH_TO_SAVE` to save file
Set `format` to save file in wav or mp3
Set `Param` align with official request body
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
2026-05-14 13:19:31 +08:00
|
|
|
cmd.Params["text"] = strings.Trim(p.curToken.Value, "\"'")
|
2026-05-12 17:17:44 +08:00
|
|
|
p.nextToken()
|
|
|
|
|
|
Go: implement TTS for MiniMax provider and CLI testing for TTS (#14911)
### What problem does this PR solve?
This PR implement TTS for MiniMax provider and CLI testing for TTS
**The following functionalities are now supported:**
**MiniMax:**
- [x] Chat / Stream Chat
- [x] Embedding
- [x] Rerank
- [x] Model listing
- [x] Provider connection checking
- [x] Text To Speech
- [ ] OCRFile
- [ ] ~~Audio To Text~~
- [ ] ~~Balance~~
**Verified examples from the CLI:**
```plaintext
RAGFlow(user)> tts with 'speech-2.8-hd@test@minimax' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"voice_setting": {"voice_id": "English_radiant_girl", "speed": 1, "vol": 1, "pitch": 0}, "audio_setting": {"sample_rate": 32000, "bitrate": 128000, "format": "wav", "channel": 1}, "output_format": "hex"}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/speech-2.8-hd_output.wav
SUCCESS
RAGFlow(user)> stream tts with 'speech-2.8-hd@test@minimax' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"voice_setting": {"voice_id": "English_radiant_girl", "speed": 1, "vol": 1, "pitch": 0}, "audio_setting": {"sample_rate": 32000, "bitrate": 128000, "format": "wav", "channel": 1}, "output_format": "hex"}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/speech-2.8-hd_output.wav
SUCCESS
```
Set `Play` to play audio in CLI
Set `Save` `PATH_TO_SAVE` to save file
Set `format` to save file in wav or mp3
Set `Param` align with official request body
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
2026-05-14 13:19:31 +08:00
|
|
|
for p.curToken.Type != TokenEOF && p.curToken.Type != TokenSemicolon {
|
|
|
|
|
switch p.curToken.Type {
|
|
|
|
|
case TokenPlay:
|
|
|
|
|
p.nextToken()
|
|
|
|
|
cmd.Params["play"] = true
|
|
|
|
|
case TokenParam:
|
|
|
|
|
p.nextToken()
|
|
|
|
|
if p.curToken.Type != TokenQuotedString {
|
|
|
|
|
return nil, fmt.Errorf("expect quoted string after 'param'")
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["param_str"] = strings.Trim(p.curToken.Value, "\"'")
|
|
|
|
|
p.nextToken()
|
|
|
|
|
p.nextToken()
|
|
|
|
|
case TokenSave:
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenQuotedString && p.curToken.Type != TokenIdentifier {
|
|
|
|
|
return nil, fmt.Errorf("expect directory path after 'save'")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd.Params["save"] = true
|
|
|
|
|
cmd.Params["save_path"] = strings.Trim(p.curToken.Value, "\"'")
|
|
|
|
|
p.nextToken()
|
|
|
|
|
case TokenFormat:
|
|
|
|
|
p.nextToken()
|
|
|
|
|
if p.curToken.Type != TokenQuotedString && p.curToken.Type != TokenIdentifier {
|
|
|
|
|
return nil, fmt.Errorf("expect format string (e.g. 'wav') after 'format'")
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["format"] = strings.Trim(p.curToken.Value, "\"'")
|
|
|
|
|
p.nextToken()
|
|
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("unexpected token: %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-12 17:17:44 +08:00
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) parseOCRCommand() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume OCR
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenWith {
|
|
|
|
|
return nil, fmt.Errorf("expected WITH after OCR")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken() // consume WITH
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
modelNameOrID, err := p.parseQuotedString()
|
2026-05-12 17:17:44 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
2026-05-13 17:29:53 +08:00
|
|
|
cmd := NewCommand("ocr_user_command")
|
2026-05-12 17:17:44 +08:00
|
|
|
|
2026-05-13 17:29:53 +08:00
|
|
|
switch p.curToken.Type {
|
|
|
|
|
case TokenFile:
|
|
|
|
|
p.nextToken()
|
|
|
|
|
var file string
|
|
|
|
|
file, err = p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["file"] = file
|
|
|
|
|
p.nextToken()
|
|
|
|
|
case TokenURL:
|
|
|
|
|
p.nextToken()
|
|
|
|
|
var url string
|
|
|
|
|
url, err = p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["url"] = url
|
|
|
|
|
p.nextToken()
|
|
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("expected FILE or URL")
|
2026-05-12 17:17:44 +08:00
|
|
|
}
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
if common.IsCompositeModelName(modelNameOrID) {
|
|
|
|
|
cmd.Params["composite_model_name"] = modelNameOrID
|
|
|
|
|
} else if common.IsUUID(modelNameOrID) {
|
|
|
|
|
cmd.Params["model_id"] = modelNameOrID
|
|
|
|
|
} else {
|
|
|
|
|
return nil, fmt.Errorf("invalid format of model name or ID: %s", modelNameOrID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
2026-05-13 17:29:53 +08:00
|
|
|
|
2026-05-12 17:17:44 +08:00
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 12:29:52 +08:00
|
|
|
func (p *Parser) parseModelParseCommand() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume WITH
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
modelNameOrID, err := p.parseQuotedString()
|
2026-05-15 12:29:52 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("parse_file_user_command")
|
|
|
|
|
|
|
|
|
|
switch p.curToken.Type {
|
|
|
|
|
case TokenFile:
|
|
|
|
|
p.nextToken()
|
|
|
|
|
var file string
|
|
|
|
|
file, err = p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["file"] = file
|
|
|
|
|
p.nextToken()
|
|
|
|
|
case TokenURL:
|
|
|
|
|
p.nextToken()
|
|
|
|
|
var url string
|
|
|
|
|
url, err = p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["url"] = url
|
|
|
|
|
p.nextToken()
|
|
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("expected FILE or URL")
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
if common.IsCompositeModelName(modelNameOrID) {
|
|
|
|
|
cmd.Params["composite_model_name"] = modelNameOrID
|
|
|
|
|
} else if common.IsUUID(modelNameOrID) {
|
|
|
|
|
cmd.Params["model_id"] = modelNameOrID
|
|
|
|
|
} else {
|
|
|
|
|
return nil, fmt.Errorf("invalid format of model name or ID: %s", modelNameOrID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
2026-05-15 12:29:52 +08:00
|
|
|
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-23 10:16:20 +08:00
|
|
|
func (p *Parser) parseCheckCommand() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume CHECK
|
|
|
|
|
|
2026-06-02 19:34:25 +08:00
|
|
|
switch p.curToken.Type {
|
|
|
|
|
case TokenInstance:
|
|
|
|
|
return p.parseCheckInstanceCommand()
|
|
|
|
|
case TokenProvider:
|
|
|
|
|
return p.parseCheckProviderByKeyCommand()
|
|
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("expected INSTANCE or PROVIDER after CHECK")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) parseCheckInstanceCommand() (*Command, error) {
|
2026-04-23 10:16:20 +08:00
|
|
|
if p.curToken.Type != TokenInstance {
|
|
|
|
|
return nil, fmt.Errorf("expected INSTANCE after CHECK")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenQuotedString {
|
|
|
|
|
return nil, fmt.Errorf("expected instance name after INSTANCE")
|
|
|
|
|
}
|
|
|
|
|
instanceName := p.curToken.Value
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenFrom {
|
|
|
|
|
return nil, fmt.Errorf("expected FROM after instance name")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenQuotedString {
|
|
|
|
|
return nil, fmt.Errorf("expected provider name after FROM")
|
|
|
|
|
}
|
|
|
|
|
providerName := p.curToken.Value
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("check_provider_connection")
|
|
|
|
|
cmd.Params["provider_name"] = providerName
|
|
|
|
|
cmd.Params["instance_name"] = instanceName
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 19:34:25 +08:00
|
|
|
func (p *Parser) parseCheckProviderByKeyCommand() (*Command, error) {
|
|
|
|
|
if p.curToken.Type != TokenProvider {
|
|
|
|
|
return nil, fmt.Errorf("expected PROVIDER after CHECK")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenQuotedString {
|
|
|
|
|
return nil, fmt.Errorf("expected provider name after PROVIDER")
|
|
|
|
|
}
|
|
|
|
|
providerName := p.curToken.Value
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenRegion {
|
|
|
|
|
return nil, fmt.Errorf("expected REGION after provider name")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenQuotedString {
|
|
|
|
|
return nil, fmt.Errorf("expected region name after REGION")
|
|
|
|
|
}
|
|
|
|
|
regionName := p.curToken.Value
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenKey {
|
|
|
|
|
return nil, fmt.Errorf("expected KEY after region name")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenQuotedString {
|
|
|
|
|
return nil, fmt.Errorf("expected API key after KEY")
|
|
|
|
|
}
|
|
|
|
|
apiKey := p.curToken.Value
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
baseURL := ""
|
|
|
|
|
if p.curToken.Type == TokenURL {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
if p.curToken.Type != TokenQuotedString {
|
|
|
|
|
return nil, fmt.Errorf("expected base URL after URL")
|
|
|
|
|
}
|
|
|
|
|
baseURL = p.curToken.Value
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
if p.curToken.Type != TokenEOF {
|
|
|
|
|
return nil, fmt.Errorf("unexpected token: %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("check_provider_with_key")
|
|
|
|
|
cmd.Params["provider_name"] = providerName
|
|
|
|
|
cmd.Params["region"] = regionName
|
|
|
|
|
cmd.Params["api_key"] = apiKey
|
|
|
|
|
if baseURL != "" {
|
|
|
|
|
cmd.Params["base_url"] = baseURL
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 20:20:35 +08:00
|
|
|
func (p *Parser) parseUseCommand() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume USE
|
|
|
|
|
|
2026-06-10 10:57:00 +08:00
|
|
|
switch p.curToken.Type {
|
|
|
|
|
case TokenModel:
|
|
|
|
|
return p.parseUseModel()
|
|
|
|
|
case TokenAPI:
|
|
|
|
|
return p.parseUseAPIServer()
|
|
|
|
|
case TokenAdmin:
|
|
|
|
|
return p.parseUseAdminServer()
|
|
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("expected MODEL or SKILL after USE")
|
2026-04-02 20:20:35 +08:00
|
|
|
}
|
2026-06-10 10:57:00 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) parseUseModel() (*Command, error) {
|
2026-04-02 20:20:35 +08:00
|
|
|
p.nextToken() // consume MODEL
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
modelNameOrID, err := p.parseQuotedString()
|
2026-04-02 20:20:35 +08:00
|
|
|
if err != nil {
|
2026-04-24 20:59:30 +08:00
|
|
|
return nil, fmt.Errorf("expected model identifier in format 'model@instance@provider': %w", err)
|
2026-04-02 20:20:35 +08:00
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("use_model")
|
2026-06-15 10:10:14 +08:00
|
|
|
|
|
|
|
|
if common.IsCompositeModelName(modelNameOrID) {
|
|
|
|
|
cmd.Params["composite_model_name"] = modelNameOrID
|
|
|
|
|
} else if common.IsUUID(modelNameOrID) {
|
|
|
|
|
cmd.Params["model_id"] = modelNameOrID
|
|
|
|
|
} else {
|
|
|
|
|
return nil, fmt.Errorf("invalid format of model name or ID: %s", modelNameOrID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
2026-04-02 20:20:35 +08:00
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 10:57:00 +08:00
|
|
|
func (p *Parser) parseUseAPIServer() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume API
|
|
|
|
|
|
|
|
|
|
serverName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
cmd := NewCommand("use_api_server")
|
|
|
|
|
cmd.Params["server_name"] = serverName
|
|
|
|
|
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) parseUseAdminServer() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume ADMIN
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("use_admin_server")
|
|
|
|
|
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-25 21:39:14 +08:00
|
|
|
func (p *Parser) parseParseCommand() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume PARSE
|
|
|
|
|
|
2026-05-15 12:29:52 +08:00
|
|
|
switch p.curToken.Type {
|
|
|
|
|
case TokenDataset:
|
2026-03-25 21:39:14 +08:00
|
|
|
return p.parseParseDataset()
|
2026-05-15 12:29:52 +08:00
|
|
|
case TokenWith:
|
|
|
|
|
return p.parseModelParseCommand()
|
2026-05-25 14:00:08 +08:00
|
|
|
case TokenDocument:
|
2026-05-15 12:29:52 +08:00
|
|
|
return p.parseParseDocs()
|
2026-06-11 13:33:26 +08:00
|
|
|
case TokenFile:
|
|
|
|
|
return p.parseParseLocalFileCommand()
|
2026-05-25 14:00:08 +08:00
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("expected DATASET, WITH, or DOCUMENT")
|
2026-03-25 21:39:14 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) parseParseDataset() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume DATASET
|
|
|
|
|
datasetName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
var method string
|
|
|
|
|
if p.curToken.Type == TokenSync {
|
|
|
|
|
method = "sync"
|
|
|
|
|
} else if p.curToken.Type == TokenAsync {
|
|
|
|
|
method = "async"
|
|
|
|
|
} else {
|
|
|
|
|
return nil, fmt.Errorf("expected SYNC or ASYNC")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("parse_dataset")
|
|
|
|
|
cmd.Params["dataset_name"] = datasetName
|
|
|
|
|
cmd.Params["method"] = method
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
// Semicolon is optional for UNSET TOKEN
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) parseParseDocs() (*Command, error) {
|
2026-05-25 14:00:08 +08:00
|
|
|
p.nextToken() // consume document
|
|
|
|
|
|
|
|
|
|
documentsStr, err := p.parseQuotedString()
|
2026-03-25 21:39:14 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
2026-05-25 14:00:08 +08:00
|
|
|
if p.curToken.Type != TokenFrom {
|
|
|
|
|
return nil, fmt.Errorf("expected FROM")
|
2026-03-25 21:39:14 +08:00
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
2026-05-25 14:00:08 +08:00
|
|
|
datasetID, err := p.parseQuotedString()
|
2026-03-25 21:39:14 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-05-25 14:00:08 +08:00
|
|
|
p.nextToken()
|
2026-03-25 21:39:14 +08:00
|
|
|
|
2026-05-25 14:00:08 +08:00
|
|
|
cmd := NewCommand("parse_documents_user_command")
|
|
|
|
|
|
|
|
|
|
documents := strings.Split(documentsStr, " ")
|
|
|
|
|
|
|
|
|
|
cmd.Params["documents"] = documents
|
|
|
|
|
cmd.Params["dataset_id"] = datasetID
|
2026-03-25 21:39:14 +08:00
|
|
|
|
|
|
|
|
// Semicolon is optional for UNSET TOKEN
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 13:33:26 +08:00
|
|
|
func (p *Parser) parseParseLocalFileCommand() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume FILE
|
|
|
|
|
|
|
|
|
|
filename, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("user_parse_local_file_command")
|
|
|
|
|
cmd.Params["filename"] = filename
|
|
|
|
|
|
|
|
|
|
optionsLoop:
|
|
|
|
|
for {
|
|
|
|
|
switch p.curToken.Type {
|
|
|
|
|
case TokenVision:
|
|
|
|
|
p.nextToken()
|
|
|
|
|
var visionModel string
|
|
|
|
|
visionModel, err = p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["vision_model"] = visionModel
|
|
|
|
|
p.nextToken()
|
|
|
|
|
case TokenASR:
|
|
|
|
|
p.nextToken()
|
|
|
|
|
var asrModel string
|
|
|
|
|
asrModel, err = p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["asr_model"] = asrModel
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
case TokenOCR:
|
|
|
|
|
p.nextToken()
|
|
|
|
|
var ocrModel string
|
|
|
|
|
ocrModel, err = p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["ocr_model"] = ocrModel
|
|
|
|
|
p.nextToken()
|
|
|
|
|
case TokenChat:
|
|
|
|
|
p.nextToken()
|
|
|
|
|
var chatModel string
|
|
|
|
|
chatModel, err = p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["chat_model"] = chatModel
|
|
|
|
|
p.nextToken()
|
|
|
|
|
case TokenEmbed:
|
|
|
|
|
p.nextToken()
|
|
|
|
|
var embedModel string
|
|
|
|
|
embedModel, err = p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-06-12 14:56:44 +08:00
|
|
|
cmd.Params["embedding_model"] = embedModel
|
2026-06-11 13:33:26 +08:00
|
|
|
p.nextToken()
|
|
|
|
|
case TokenDocParse:
|
|
|
|
|
p.nextToken()
|
|
|
|
|
var docParseModel string
|
|
|
|
|
docParseModel, err = p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["doc_parse_model"] = docParseModel
|
|
|
|
|
p.nextToken()
|
|
|
|
|
case TokenSemicolon:
|
|
|
|
|
p.nextToken()
|
|
|
|
|
break optionsLoop // done
|
|
|
|
|
default:
|
|
|
|
|
// No more options to process
|
|
|
|
|
break optionsLoop
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-25 21:39:14 +08:00
|
|
|
func (p *Parser) parseBenchmarkCommand() (*Command, error) {
|
|
|
|
|
cmd := NewCommand("benchmark")
|
|
|
|
|
|
|
|
|
|
p.nextToken() // consume BENCHMARK
|
|
|
|
|
concurrency, err := p.parseNumber()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["concurrency"] = concurrency
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
iterations, err := p.parseNumber()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["iterations"] = iterations
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
// Parse user_statement
|
|
|
|
|
nestedCmd, err := p.parseUserStatement() // Not only user statement
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["command"] = nestedCmd
|
|
|
|
|
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) parseUserStatement() (*Command, error) {
|
|
|
|
|
switch p.curToken.Type {
|
|
|
|
|
case TokenPing:
|
2026-06-24 16:50:40 +08:00
|
|
|
return p.parseAPIPingServer()
|
2026-05-25 19:15:07 +08:00
|
|
|
case TokenDelete:
|
|
|
|
|
return p.parseDeleteCommand()
|
2026-03-25 21:39:14 +08:00
|
|
|
case TokenShow:
|
|
|
|
|
return p.parseShowCommand()
|
|
|
|
|
case TokenCreate:
|
|
|
|
|
return p.parseCreateCommand()
|
|
|
|
|
case TokenDrop:
|
|
|
|
|
return p.parseDropCommand()
|
|
|
|
|
case TokenUnset:
|
|
|
|
|
return p.parseUnsetCommand()
|
|
|
|
|
case TokenReset:
|
|
|
|
|
return p.parseResetCommand()
|
|
|
|
|
case TokenList:
|
2026-06-24 16:50:40 +08:00
|
|
|
return p.parseAPIListCommands()
|
2026-03-25 21:39:14 +08:00
|
|
|
case TokenParse:
|
|
|
|
|
return p.parseParseCommand()
|
|
|
|
|
case TokenImport:
|
|
|
|
|
return p.parseImportCommand()
|
2026-04-01 16:16:25 +08:00
|
|
|
case TokenInsert:
|
|
|
|
|
return p.parseInsertCommand()
|
2026-06-09 15:22:50 +08:00
|
|
|
case TokenRetrieve:
|
|
|
|
|
return p.parseRetrieveCommand()
|
2026-05-20 20:32:06 +08:00
|
|
|
case TokenGet:
|
|
|
|
|
return p.parseGetCommand()
|
2026-04-07 09:44:51 +08:00
|
|
|
case TokenUpdate:
|
|
|
|
|
return p.parseUpdateCommand()
|
|
|
|
|
case TokenRemove:
|
|
|
|
|
return p.parseRemoveCommand()
|
2026-03-25 21:39:14 +08:00
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("invalid user statement: %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) parseUnsetCommand() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume UNSET
|
|
|
|
|
|
2026-06-24 18:48:09 +08:00
|
|
|
if p.curToken.Type != TokenKey {
|
2026-03-25 21:39:14 +08:00
|
|
|
return nil, fmt.Errorf("expected TOKEN after UNSET")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
2026-06-24 18:48:09 +08:00
|
|
|
// Semicolon is optional
|
2026-03-25 21:39:14 +08:00
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
2026-06-24 18:48:09 +08:00
|
|
|
return NewCommand("api_unset_api_key"), nil
|
2026-03-25 21:39:14 +08:00
|
|
|
}
|
2026-04-07 09:44:51 +08:00
|
|
|
|
2026-06-08 11:49:37 +08:00
|
|
|
// parseGetCommand parses: GET CHUNK or GET METADATA
|
2026-05-20 20:32:06 +08:00
|
|
|
func (p *Parser) parseGetCommand() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume GET
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type == TokenChunk {
|
|
|
|
|
return p.parseGetChunk()
|
|
|
|
|
}
|
2026-06-08 11:49:37 +08:00
|
|
|
if p.curToken.Type == TokenMetadata {
|
|
|
|
|
return p.parseGetMetadata()
|
|
|
|
|
}
|
2026-05-20 20:32:06 +08:00
|
|
|
|
|
|
|
|
return nil, fmt.Errorf("unknown GET target: %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 19:15:07 +08:00
|
|
|
// parseGetChunk parses: GET CHUNK 'chunk_id' OF DOCUMENT 'doc_id' IN DATASET 'dataset_id'
|
2026-05-20 20:32:06 +08:00
|
|
|
func (p *Parser) parseGetChunk() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume CHUNK
|
|
|
|
|
|
|
|
|
|
// Parse chunk_id
|
|
|
|
|
chunkID, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected chunk_id: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("get_chunk")
|
|
|
|
|
cmd.Params["chunk_id"] = chunkID
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
if p.curToken.Type != TokenOf {
|
|
|
|
|
return nil, fmt.Errorf("expected OF after chunk_id")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
2026-05-25 19:15:07 +08:00
|
|
|
if p.curToken.Type != TokenDocument {
|
|
|
|
|
return nil, fmt.Errorf("expected DOCUMENT after OF")
|
2026-05-20 20:32:06 +08:00
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
2026-05-25 19:15:07 +08:00
|
|
|
// Parse doc_id
|
|
|
|
|
docID, err := p.parseQuotedString()
|
2026-05-20 20:32:06 +08:00
|
|
|
if err != nil {
|
2026-05-25 19:15:07 +08:00
|
|
|
return nil, fmt.Errorf("expected doc_id: %w", err)
|
2026-05-20 20:32:06 +08:00
|
|
|
}
|
2026-05-25 19:15:07 +08:00
|
|
|
cmd.Params["doc_id"] = docID
|
2026-05-20 20:32:06 +08:00
|
|
|
|
|
|
|
|
p.nextToken()
|
2026-05-25 19:15:07 +08:00
|
|
|
if p.curToken.Type != TokenIn {
|
|
|
|
|
return nil, fmt.Errorf("expected IN after doc_id")
|
2026-05-20 20:32:06 +08:00
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
2026-05-25 19:15:07 +08:00
|
|
|
if p.curToken.Type != TokenDataset {
|
|
|
|
|
return nil, fmt.Errorf("expected DATASET after IN")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Parse dataset_id
|
|
|
|
|
datasetID, err := p.parseQuotedString()
|
2026-05-20 20:32:06 +08:00
|
|
|
if err != nil {
|
2026-05-25 19:15:07 +08:00
|
|
|
return nil, fmt.Errorf("expected dataset_id: %w", err)
|
2026-05-20 20:32:06 +08:00
|
|
|
}
|
2026-05-25 19:15:07 +08:00
|
|
|
cmd.Params["dataset_id"] = datasetID
|
2026-05-20 20:32:06 +08:00
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-09 09:52:31 +08:00
|
|
|
// Internal
|
|
|
|
|
// parseUpdateCommand parses: UPDATE CHUNK 'chunk_id' OF DATASET 'dataset_name' SET '{"content": "..."}'
|
2026-04-07 09:44:51 +08:00
|
|
|
func (p *Parser) parseUpdateCommand() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume UPDATE
|
|
|
|
|
|
2026-04-09 09:52:31 +08:00
|
|
|
if p.curToken.Type == TokenChunk {
|
|
|
|
|
return p.parseUpdateChunk()
|
2026-04-07 09:44:51 +08:00
|
|
|
}
|
2026-04-09 09:52:31 +08:00
|
|
|
|
|
|
|
|
return nil, fmt.Errorf("unknown UPDATE target: %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 15:30:14 +08:00
|
|
|
// Internal CLI for GO
|
2026-05-25 19:15:07 +08:00
|
|
|
// parseUpdateChunk parses: UPDATE CHUNK 'chunk_id' OF DOCUMENT 'doc_id' IN DATASET 'dataset_id' SET '{"content": "..."}'
|
2026-04-09 09:52:31 +08:00
|
|
|
func (p *Parser) parseUpdateChunk() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume CHUNK
|
2026-04-07 09:44:51 +08:00
|
|
|
|
|
|
|
|
// Parse chunk_id
|
|
|
|
|
chunkID, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected chunk_id: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("update_chunk")
|
|
|
|
|
cmd.Params["chunk_id"] = chunkID
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
if p.curToken.Type != TokenOf {
|
|
|
|
|
return nil, fmt.Errorf("expected OF after chunk_id")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
2026-05-25 19:15:07 +08:00
|
|
|
if p.curToken.Type != TokenDocument {
|
|
|
|
|
return nil, fmt.Errorf("expected DOCUMENT after OF")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Parse doc_id
|
|
|
|
|
docID, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected doc_id: %w", err)
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["doc_id"] = docID
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
if p.curToken.Type != TokenIn {
|
|
|
|
|
return nil, fmt.Errorf("expected IN after doc_id")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
2026-04-07 09:44:51 +08:00
|
|
|
if p.curToken.Type != TokenDataset {
|
2026-05-25 19:15:07 +08:00
|
|
|
return nil, fmt.Errorf("expected DATASET after IN")
|
2026-04-07 09:44:51 +08:00
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Parse dataset_name
|
|
|
|
|
datasetName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected dataset_name: %w", err)
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["dataset_name"] = datasetName
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
if p.curToken.Type != TokenSet {
|
|
|
|
|
return nil, fmt.Errorf("expected SET after dataset_name")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Parse JSON body
|
|
|
|
|
jsonBody, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected JSON body: %w", err)
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["json_body"] = jsonBody
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// parseSetMeta parses: SET METADATA OF DOCUMENT 'doc_id' TO '{"key": "value"}'
|
|
|
|
|
func (p *Parser) parseSetMeta() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume METADATA
|
|
|
|
|
|
|
|
|
|
// Expect OF
|
|
|
|
|
if p.curToken.Type != TokenOf {
|
|
|
|
|
return nil, fmt.Errorf("expected OF after SET METADATA")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Expect DOCUMENT
|
|
|
|
|
if p.curToken.Type != TokenDocument {
|
|
|
|
|
return nil, fmt.Errorf("expected DOCUMENT after SET METADATA OF")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Parse doc_id
|
|
|
|
|
docID, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected doc_id: %w", err)
|
|
|
|
|
}
|
|
|
|
|
cmd := NewCommand("set_meta")
|
|
|
|
|
cmd.Params["doc_id"] = docID
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
// Expect TO
|
|
|
|
|
if p.curToken.Type != TokenTo {
|
|
|
|
|
return nil, fmt.Errorf("expected TO after doc_id")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Parse meta JSON
|
|
|
|
|
meta, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected meta JSON: %w", err)
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["meta"] = meta
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 19:15:07 +08:00
|
|
|
// parseDeleteMeta parses: DELETE METADATA OF DOCUMENT 'doc_id' [KEYS '["key1", "key2"]']
|
|
|
|
|
// If KEYS is not provided, deletes entire document metadata
|
|
|
|
|
func (p *Parser) parseDeleteMeta() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume METADATA
|
|
|
|
|
|
|
|
|
|
// Expect OF
|
|
|
|
|
if p.curToken.Type != TokenOf {
|
|
|
|
|
return nil, fmt.Errorf("expected OF after DELETE METADATA")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Expect DOCUMENT
|
|
|
|
|
if p.curToken.Type != TokenDocument {
|
|
|
|
|
return nil, fmt.Errorf("expected DOCUMENT after DELETE METADATA OF")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Parse doc_id
|
|
|
|
|
docID, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected doc_id: %w", err)
|
|
|
|
|
}
|
|
|
|
|
cmd := NewCommand("delete_meta")
|
|
|
|
|
cmd.Params["doc_id"] = docID
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
// KEYS is optional - if not provided, delete entire document metadata
|
|
|
|
|
if p.curToken.Type != TokenKeys {
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
if p.curToken.Type == TokenEOF {
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
return nil, fmt.Errorf("expected KEYS or end of command after doc_id")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse keys JSON array
|
|
|
|
|
p.nextToken()
|
|
|
|
|
keys, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected keys JSON array: %w", err)
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["keys"] = keys
|
|
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
if p.curToken.Type != TokenEOF {
|
|
|
|
|
return nil, fmt.Errorf("expected end of command after KEYS")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 09:44:51 +08:00
|
|
|
// parseRemoveTags parses: REMOVE TAGS 'tag1', 'tag2' from DATASET 'dataset_name';
|
|
|
|
|
func (p *Parser) parseRemoveTags() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume TAGS
|
|
|
|
|
|
|
|
|
|
// Parse first tag
|
|
|
|
|
tag, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected tag: %w", err)
|
|
|
|
|
}
|
|
|
|
|
tags := []string{tag}
|
|
|
|
|
|
|
|
|
|
// Parse additional tags separated by commas
|
|
|
|
|
for {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
if p.curToken.Type == TokenComma {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
tag, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected tag after comma: %w", err)
|
|
|
|
|
}
|
|
|
|
|
tags = append(tags, tag)
|
|
|
|
|
} else {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
cmd := NewCommand("rm_tags")
|
|
|
|
|
cmd.Params["tags"] = tags
|
|
|
|
|
|
|
|
|
|
// Expect from
|
|
|
|
|
if p.curToken.Type != TokenFrom {
|
|
|
|
|
return nil, fmt.Errorf("expected FROM after tags")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Expect DATASET
|
|
|
|
|
if p.curToken.Type != TokenDataset {
|
|
|
|
|
return nil, fmt.Errorf("expected DATASET after FROM")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Parse dataset_name
|
|
|
|
|
datasetName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected dataset_name: %w", err)
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["dataset_name"] = datasetName
|
|
|
|
|
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
2026-04-09 09:52:31 +08:00
|
|
|
|
|
|
|
|
// parseRemoveChunk parses:
|
2026-05-25 19:15:07 +08:00
|
|
|
// - REMOVE CHUNKS 'chunk_id1', 'chunk_id2' FROM DOCUMENT 'doc_id' IN DATASET 'dataset_name';
|
|
|
|
|
// - REMOVE ALL CHUNKS FROM DOCUMENT 'doc_id' IN DATASET 'dataset_name';
|
2026-04-09 09:52:31 +08:00
|
|
|
func (p *Parser) parseRemoveChunk() (*Command, error) {
|
|
|
|
|
cmd := NewCommand("remove_chunks")
|
|
|
|
|
|
|
|
|
|
// Check if ALL CHUNKS - if we came here from TokenAll case, curToken is already ALL
|
|
|
|
|
if p.curToken.Type == TokenAll {
|
|
|
|
|
p.nextToken() // consume ALL
|
|
|
|
|
if p.curToken.Type != TokenChunks {
|
|
|
|
|
return nil, fmt.Errorf("expected CHUNKS after ALL")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken() // consume CHUNKS
|
|
|
|
|
cmd.Params["delete_all"] = true
|
|
|
|
|
} else {
|
|
|
|
|
// curToken is TokenChunks, consume it first
|
|
|
|
|
p.nextToken()
|
2026-05-25 19:15:07 +08:00
|
|
|
// Multiple chunks: REMOVE CHUNKS 'id1' 'id2' FROM DOCUMENT 'doc_id' IN DATASET 'dataset_name' (space-separated)
|
2026-04-09 09:52:31 +08:00
|
|
|
// Parse first chunk ID
|
|
|
|
|
chunkID, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected chunk_id: %w", err)
|
|
|
|
|
}
|
|
|
|
|
chunkIDs := []string{chunkID}
|
|
|
|
|
|
2026-05-20 20:32:06 +08:00
|
|
|
// Parse additional chunk IDs separated by spaces (each quoted)
|
2026-04-09 09:52:31 +08:00
|
|
|
for {
|
|
|
|
|
p.nextToken()
|
2026-05-20 20:32:06 +08:00
|
|
|
// Stop if we hit FROM or non-quoted token
|
|
|
|
|
if p.curToken.Type == TokenFrom || p.curToken.Type != TokenQuotedString {
|
2026-04-09 09:52:31 +08:00
|
|
|
break
|
|
|
|
|
}
|
2026-05-20 20:32:06 +08:00
|
|
|
chunkID, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected chunk_id: %w", err)
|
|
|
|
|
}
|
|
|
|
|
chunkIDs = append(chunkIDs, chunkID)
|
2026-04-09 09:52:31 +08:00
|
|
|
}
|
|
|
|
|
cmd.Params["chunk_ids"] = chunkIDs
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Expect FROM
|
|
|
|
|
if p.curToken.Type != TokenFrom {
|
|
|
|
|
return nil, fmt.Errorf("expected FROM after chunk(s)")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Expect DOCUMENT
|
|
|
|
|
if p.curToken.Type != TokenDocument {
|
|
|
|
|
return nil, fmt.Errorf("expected DOCUMENT after FROM")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Parse doc_id
|
|
|
|
|
docID, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected doc_id: %w", err)
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["doc_id"] = docID
|
2026-05-25 19:15:07 +08:00
|
|
|
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Expect IN
|
|
|
|
|
if p.curToken.Type != TokenIn {
|
|
|
|
|
return nil, fmt.Errorf("expected IN after doc_id")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Expect DATASET
|
|
|
|
|
if p.curToken.Type != TokenDataset {
|
|
|
|
|
return nil, fmt.Errorf("expected DATASET after IN")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Parse dataset_name (quoted string)
|
|
|
|
|
datasetName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected dataset_name: %w", err)
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["dataset_name"] = datasetName
|
|
|
|
|
|
2026-04-09 09:52:31 +08:00
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
2026-06-09 15:22:50 +08:00
|
|
|
|
2026-06-12 14:56:44 +08:00
|
|
|
func (p *Parser) parseUserStartIngestion() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume Start
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenIngestion {
|
|
|
|
|
return nil, fmt.Errorf("expect INGESTION")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken() // consume Ingestion
|
|
|
|
|
|
|
|
|
|
documentID, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenFrom {
|
|
|
|
|
return nil, fmt.Errorf("expect FROM")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken() // consume FROM
|
|
|
|
|
|
|
|
|
|
datasetID, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("user_start_ingestion_command")
|
|
|
|
|
cmd.Params["document_id"] = documentID
|
|
|
|
|
cmd.Params["dataset_id"] = datasetID
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Semicolon is optional for UNSET TOKEN
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-09 15:22:50 +08:00
|
|
|
// parseShowTask parses SHOW ADMIN SERVER
|
|
|
|
|
func (p *Parser) parseUserShowAdmin() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume ADMIN
|
|
|
|
|
|
|
|
|
|
var cmd *Command
|
|
|
|
|
switch p.curToken.Type {
|
|
|
|
|
case TokenServer:
|
|
|
|
|
p.nextToken()
|
|
|
|
|
cmd = NewCommand("show_admin_server")
|
|
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("expected SERVER after ADMIN")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-12 14:56:44 +08:00
|
|
|
func (p *Parser) parseUserStopIngestion() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume Stop
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenIngestion {
|
|
|
|
|
return nil, fmt.Errorf("expect INGESTION")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken() // consume Ingestion
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenTasks {
|
|
|
|
|
return nil, fmt.Errorf("expect TASKS")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
taskStr, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tasks := strings.Split(taskStr, " ")
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("user_stop_ingestion_command")
|
|
|
|
|
cmd.Params["tasks"] = tasks
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Semicolon is optional for UNSET TOKEN
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-25 10:01:21 +08:00
|
|
|
func (p *Parser) parseAPIListIngestionTasks() (*Command, error) {
|
2026-06-12 14:56:44 +08:00
|
|
|
p.nextToken() // consume Ingestion
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenTasks {
|
|
|
|
|
return nil, fmt.Errorf("expected TASKS")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken() // consume TASKS
|
|
|
|
|
|
2026-06-25 10:01:21 +08:00
|
|
|
cmd := NewCommand("api_list_ingestion_tasks")
|
2026-06-12 14:56:44 +08:00
|
|
|
|
|
|
|
|
if p.curToken.Type == TokenFrom {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
datasetID, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["dataset_id"] = datasetID
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Semicolon is optional for UNSET TOKEN
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) parseUserRemoveTask() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume Ingestion
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenTasks {
|
|
|
|
|
return nil, fmt.Errorf("expected TASKS")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken() // consume TASKS
|
|
|
|
|
|
|
|
|
|
taskStr, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("user_remove_task_command")
|
|
|
|
|
|
|
|
|
|
tasks := strings.Split(taskStr, " ")
|
|
|
|
|
|
|
|
|
|
cmd.Params["tasks"] = tasks
|
|
|
|
|
|
|
|
|
|
// Semicolon is optional for UNSET TOKEN
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-09 15:22:50 +08:00
|
|
|
// parseShowTask parses SHOW API SERVER <server_name>
|
|
|
|
|
func (p *Parser) parseUserShowAPI() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume API
|
|
|
|
|
|
|
|
|
|
var cmd *Command
|
|
|
|
|
switch p.curToken.Type {
|
|
|
|
|
case TokenServer:
|
|
|
|
|
p.nextToken()
|
|
|
|
|
cmd = NewCommand("show_api_server")
|
|
|
|
|
|
|
|
|
|
serverName, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected dataset_name: %w", err)
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["api_server_name"] = serverName
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("expected SERVER after API")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) parseListApiCommand() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume API
|
|
|
|
|
|
|
|
|
|
var cmd *Command
|
|
|
|
|
switch p.curToken.Type {
|
|
|
|
|
case TokenServer:
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
cmd = NewCommand("list_api_server")
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("expected SERVER after API")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
2026-06-11 13:33:26 +08:00
|
|
|
|
|
|
|
|
func (p *Parser) parseExplainCommand() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume EXPLAIN
|
|
|
|
|
|
|
|
|
|
switch p.curToken.Type {
|
|
|
|
|
case TokenChunk:
|
|
|
|
|
return p.parseChunkCommand(true)
|
|
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("expected CHUNK after EXPLAIN")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) parseChunkCommand(explain bool) (*Command, error) {
|
|
|
|
|
p.nextToken() // consume CHUNK
|
|
|
|
|
|
|
|
|
|
filename, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected filename: %w", err)
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenWith {
|
|
|
|
|
return nil, fmt.Errorf("expected WITH after filename")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
dsl, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected DSL: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Semicolon is optional
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("user_chunk_command")
|
|
|
|
|
cmd.Params["dsl"] = dsl
|
|
|
|
|
cmd.Params["filename"] = filename
|
|
|
|
|
cmd.Params["explain"] = explain
|
|
|
|
|
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
2026-06-18 18:07:27 +08:00
|
|
|
|
|
|
|
|
// parseOpenaiChatCommand parses:
|
|
|
|
|
//
|
|
|
|
|
// OPENAI_CHAT <chat_id> <message>
|
|
|
|
|
// [model <string>] [system <string>]
|
|
|
|
|
// [history <string>] [history_delimiter <string>]
|
|
|
|
|
// [temperature <float>] [max_tokens <int>] [stream <bool>]
|
|
|
|
|
// [top_p <float>] [frequency_penalty <float>] [presence_penalty <float>]
|
|
|
|
|
// [extra_body <json>]
|
|
|
|
|
// ;
|
|
|
|
|
//
|
|
|
|
|
// Named options can appear in any order. The chat_id and message are
|
|
|
|
|
// required positional args; everything else is optional with a default.
|
|
|
|
|
//
|
|
|
|
|
// `history` is captured as a single string in cmd.Params["history_raw"]
|
|
|
|
|
// and is split into turns by cmd.Params["history_delimiter"] (default
|
|
|
|
|
// ";") later in buildOpenaiChatRequestBody — this two-step split lets
|
|
|
|
|
// `history_delimiter` and `history` appear in either order on the
|
|
|
|
|
// command line. The chosen delimiter must not appear inside any
|
|
|
|
|
// message body.
|
|
|
|
|
//
|
|
|
|
|
// `extra_body` is well-formed JSON. The accepted keys are:
|
|
|
|
|
//
|
|
|
|
|
// reference bool
|
|
|
|
|
// reference_metadata { include?: bool, fields?: string[] }
|
|
|
|
|
// metadata_condition { logic?: "and"|"or", conditions?: [{key, operator, value}] }
|
|
|
|
|
// (See user_command.go:allowedExtraBodyKeys for the authoritative set)
|
|
|
|
|
func (p *Parser) parseOpenaiChatCommand() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume OPENAI_CHAT
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type == TokenDash {
|
|
|
|
|
dashCount := 0
|
|
|
|
|
for p.curToken.Type == TokenDash {
|
|
|
|
|
dashCount++
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
if dashCount > 0 && p.curToken.Type == TokenIdentifier {
|
|
|
|
|
switch strings.ToLower(p.curToken.Value) {
|
|
|
|
|
case "h", "help":
|
|
|
|
|
return NewCommand("openai_chat_help"), nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil, fmt.Errorf("OPENAI_CHAT: only -h/--help takes no args; otherwise expected chat_id and message")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("openai_chat")
|
|
|
|
|
|
|
|
|
|
// Defaults — match the OpenAI spec / RAGFlow server behavior.
|
|
|
|
|
cmd.Params["model"] = "model" // placeholder; server resolves to dialog.llm_id
|
|
|
|
|
cmd.Params["temperature"] = 0.0
|
|
|
|
|
cmd.Params["max_tokens"] = 0
|
|
|
|
|
cmd.Params["stream"] = false
|
|
|
|
|
|
|
|
|
|
// Required positional: <chat_id> <message>
|
|
|
|
|
chatID, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("OPENAI_CHAT: expected chat_id as first argument: %w", err)
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["chat_id"] = chatID
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
message, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("OPENAI_CHAT: expected message as second argument: %w", err)
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["message"] = message
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Optional
|
|
|
|
|
handleOption := func(name string) error {
|
|
|
|
|
switch name {
|
|
|
|
|
case "model", "system":
|
|
|
|
|
v, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("OPENAI_CHAT %s: expected quoted string, got %s", name, p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
cmd.Params[name] = v
|
|
|
|
|
p.nextToken()
|
|
|
|
|
case "temperature", "top_p", "frequency_penalty", "presence_penalty":
|
|
|
|
|
v, err := p.parseFloat()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("OPENAI_CHAT %s: expected number, got %s", name, p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
cmd.Params[name] = v
|
|
|
|
|
p.nextToken()
|
|
|
|
|
case "max_tokens":
|
|
|
|
|
v, err := p.parseNumber()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("OPENAI_CHAT max_tokens: expected integer, got %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["max_tokens"] = v
|
|
|
|
|
p.nextToken()
|
|
|
|
|
case "stream":
|
|
|
|
|
v, err := p.parseBool()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("OPENAI_CHAT %s: expected true|false, got %s", name, p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
cmd.Params[name] = v
|
|
|
|
|
// parseBool already advances the cursor.
|
|
|
|
|
case "extra_body":
|
|
|
|
|
raw, err := p.parseJSONLiteral()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("OPENAI_CHAT %s: %w", name, err)
|
|
|
|
|
}
|
|
|
|
|
cmd.Params[name] = raw
|
|
|
|
|
p.nextToken()
|
|
|
|
|
case "history":
|
|
|
|
|
raw, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("OPENAI_CHAT history: expected quoted string, got %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["history_raw"] = raw
|
|
|
|
|
p.nextToken()
|
|
|
|
|
case "history_delimiter":
|
|
|
|
|
v, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("OPENAI_CHAT history_delimiter: expected quoted string, got %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["history_delimiter"] = v
|
|
|
|
|
p.nextToken()
|
|
|
|
|
default:
|
|
|
|
|
return fmt.Errorf("OPENAI_CHAT: unknown option %q (valid: model, system, history, history_delimiter, temperature, max_tokens, stream, top_p, frequency_penalty, presence_penalty, extra_body)", name)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Named options, any order, until ';'.
|
|
|
|
|
optionsLoop:
|
|
|
|
|
for {
|
|
|
|
|
switch p.curToken.Type {
|
|
|
|
|
case TokenSemicolon:
|
|
|
|
|
p.nextToken()
|
|
|
|
|
break optionsLoop
|
|
|
|
|
case TokenEOF:
|
|
|
|
|
break optionsLoop
|
|
|
|
|
|
|
|
|
|
case TokenIdentifier, TokenQuotedString:
|
|
|
|
|
name := p.curToken.Value
|
|
|
|
|
if p.curToken.Type == TokenQuotedString {
|
|
|
|
|
name = strings.Trim(name, "'\"")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
if err := handleOption(name); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
if !isKeyword(p.curToken.Type) {
|
|
|
|
|
return nil, fmt.Errorf("OPENAI_CHAT: unexpected token %q in option list (valid options: model, system, history, history_delimiter, temperature, max_tokens, stream, top_p, frequency_penalty, presence_penalty, extra_body)", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
name := p.curToken.Value
|
|
|
|
|
p.nextToken()
|
|
|
|
|
if err := handleOption(name); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// parseJSONLiteral consumes a TokenQuotedString whose payload is a JSON
|
|
|
|
|
// value (object, array, string, number, or boolean) and returns it as
|
|
|
|
|
// the original raw string (NOT decoded — the caller decides whether to
|
|
|
|
|
// embed it into a larger JSON object or pass it through as-is).
|
|
|
|
|
func (p *Parser) parseJSONLiteral() (string, error) {
|
|
|
|
|
if p.curToken.Type != TokenQuotedString {
|
|
|
|
|
return "", fmt.Errorf("expected JSON literal in single/double quotes, got %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
raw := p.curToken.Value
|
|
|
|
|
// Validate it actually parses as JSON so we fail fast on
|
|
|
|
|
// typos like `'{}' extra comma'` or `'not json'`.
|
|
|
|
|
var probe interface{}
|
|
|
|
|
if err := json.Unmarshal([]byte(raw), &probe); err != nil {
|
|
|
|
|
return "", fmt.Errorf("invalid JSON literal %q: %w", raw, err)
|
|
|
|
|
}
|
|
|
|
|
return raw, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// parseBool accepts a TokenIdentifier "true"/"false"
|
|
|
|
|
func (p *Parser) parseBool() (bool, error) {
|
|
|
|
|
switch strings.ToLower(p.curToken.Value) {
|
|
|
|
|
case "true":
|
|
|
|
|
p.nextToken()
|
|
|
|
|
return true, nil
|
|
|
|
|
case "false":
|
|
|
|
|
p.nextToken()
|
|
|
|
|
return false, nil
|
|
|
|
|
}
|
|
|
|
|
return false, fmt.Errorf("expected true or false, got %q", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// historyRoleRegex matches the role prefix on a turn. The captured
|
|
|
|
|
// alternation is the role; the colon is required so we don't
|
|
|
|
|
// accidentally split on a word like "user:foo" appearing inside
|
|
|
|
|
// other content.
|
|
|
|
|
var historyRoleRegex = regexp.MustCompile(`(?i)^(user|assistant):`)
|
|
|
|
|
|
|
|
|
|
// defaultHistoryDelimiter is the turn separator used when the
|
|
|
|
|
// caller does not pass the `history_delimiter` option.
|
|
|
|
|
const defaultHistoryDelimiter = ";"
|
|
|
|
|
|
|
|
|
|
// parseHistory splits the history literal into a slice of
|
|
|
|
|
// {"role": ..., "content": ...} maps. Format:
|
|
|
|
|
//
|
|
|
|
|
// "user:question one;assistant:answer one;user:question two"
|
|
|
|
|
//
|
|
|
|
|
// Turns are separated by `history_delimiter` (default `;`). Each
|
|
|
|
|
// segment must start with the role prefix `user:` or `assistant:`
|
|
|
|
|
// (case-insensitive).
|
|
|
|
|
func parseHistory(literal, delimiter string) ([]map[string]string, error) {
|
|
|
|
|
if delimiter == "" {
|
|
|
|
|
delimiter = defaultHistoryDelimiter
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Trim a single pair of surrounding quotes if present.
|
|
|
|
|
s := strings.TrimSpace(literal)
|
|
|
|
|
if len(s) >= 2 {
|
|
|
|
|
first, last := s[0], s[len(s)-1]
|
|
|
|
|
if (first == '"' && last == '"') || (first == '\'' && last == '\'') {
|
|
|
|
|
s = s[1 : len(s)-1]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
raw := strings.Split(s, delimiter)
|
|
|
|
|
turns := make([]map[string]string, 0, len(raw))
|
|
|
|
|
for _, segment := range raw {
|
|
|
|
|
segment = strings.TrimSpace(segment)
|
|
|
|
|
if segment == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
m := historyRoleRegex.FindStringSubmatch(segment)
|
|
|
|
|
if m == nil {
|
|
|
|
|
return nil, fmt.Errorf("history segment %q must start with 'user:' or 'assistant:'", segment)
|
|
|
|
|
}
|
|
|
|
|
role := strings.ToLower(m[1])
|
|
|
|
|
// Drop the "<role>:" prefix (m[0] is the whole match, e.g.
|
|
|
|
|
// "user:"; we want the content AFTER the colon).
|
|
|
|
|
content := strings.TrimPrefix(segment, m[0])
|
|
|
|
|
turns = append(turns, map[string]string{
|
|
|
|
|
"role": role,
|
|
|
|
|
"content": content,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
if len(turns) == 0 {
|
|
|
|
|
return nil, fmt.Errorf("history is empty or unparseable: %q", literal)
|
|
|
|
|
}
|
|
|
|
|
return turns, nil
|
|
|
|
|
}
|