Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion crates/ov_cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -460,14 +460,42 @@ async fn main() {
}

async fn handle_add_resource(
path: String,
mut path: String,
to: Option<String>,
reason: String,
instruction: String,
wait: bool,
timeout: Option<f64>,
ctx: CliContext,
) -> Result<()> {
// Validate path: if it's a local path, check if it exists
if !path.starts_with("http://") && !path.starts_with("https://") {
use std::path::Path;

// Unescape path: replace backslash followed by space with just space
let unescaped_path = path.replace("\\ ", " ");
let path_obj = Path::new(&unescaped_path);
if !path_obj.exists() {
eprintln!("Error: Path '{}' does not exist.", path);

// Check if there might be unquoted spaces
use std::env;
let args: Vec<String> = env::args().collect();

if let Some(add_resource_pos) = args.iter().position(|s| s == "add-resource" || s == "add") {
if args.len() > add_resource_pos + 2 {
let extra_args = &args[add_resource_pos + 2..];
let suggested_path = format!("{} {}", path, extra_args.join(" "));
eprintln!("\nIt looks like you may have forgotten to quote a path with spaces.");
eprintln!("Suggested command: ov add-resource \"{}\"", suggested_path);
}
}

std::process::exit(1);
}
path = unescaped_path;
}

let client = ctx.get_client();
commands::resources::add_resource(
&client, &path, to, reason, instruction, wait, timeout, ctx.output_format, ctx.compact
Expand Down
3 changes: 2 additions & 1 deletion examples/chatmem/ov.conf.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"api_base" : "https://ark-cn-beijing.bytedance.net/api/v3",
"api_key" : "not_gonna_give_u_this",
"backend" : "volcengine",
"model" : "doubao-seed-1-8-251228"
"model" : "doubao-seed-1-8-251228",
"thinking": false
}
}
3 changes: 2 additions & 1 deletion examples/mcp-query/ov.conf.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"api_base" : "https://ark-cn-beijing.bytedance.net/api/v3",
"api_key" : "<your-api-key>",
"provider" : "volcengine",
"model" : "doubao-seed-1-8-251228"
"model" : "doubao-seed-1-8-251228",
"thinking": false
}
}
3 changes: 2 additions & 1 deletion examples/memex/ov.conf.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"api_base" : "https://ark.cn-beijing.volces.com/api/v3",
"api_key" : "your-volcengine-api-key",
"backend" : "volcengine",
"model" : "doubao-seed-1-8-251228"
"model" : "doubao-seed-1-8-251228",
"thinking": false
}
}
3 changes: 2 additions & 1 deletion examples/ov.conf.example
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@
"api_base": "https://ark.cn-beijing.volces.com/api/v3",
"temperature": 0.0,
"max_retries": 2,
"provider": "volcengine"
"provider": "volcengine",
"thinking": false
},
"rerank": {
"ak": null,
Expand Down
3 changes: 2 additions & 1 deletion examples/query/ov.conf.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"api_base" : "https://ark-cn-beijing.bytedance.net/api/v3",
"api_key" : "not_gonna_give_u_this",
"provider" : "volcengine",
"model" : "doubao-seed-1-8-251228"
"model" : "doubao-seed-1-8-251228",
"thinking": false
}
}
3 changes: 2 additions & 1 deletion examples/server_client/ov.conf.example
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"api_base": "https://ark.cn-beijing.volces.com/api/v3",
"temperature": 0.0,
"max_retries": 2,
"provider": "volcengine"
"provider": "volcengine",
"thinking": false
}
}
26 changes: 23 additions & 3 deletions openviking/models/vlm/backends/litellm_vlm.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,21 +146,31 @@ def _build_kwargs(self, model: str, messages: list) -> dict[str, Any]:

return kwargs

def get_completion(self, prompt: str) -> str:
def get_completion(self, prompt: str, thinking: bool = False) -> str:
"""Get text completion synchronously."""
model = self._resolve_model(self.model or "gpt-4o-mini")
messages = [{"role": "user", "content": prompt}]
original_thinking = self._thinking
if thinking:
self._thinking = thinking
kwargs = self._build_kwargs(model, messages)
self._thinking = original_thinking

response = completion(**kwargs)
self._update_token_usage_from_response(response)
return response.choices[0].message.content or ""

async def get_completion_async(self, prompt: str, max_retries: int = 0) -> str:
async def get_completion_async(
self, prompt: str, thinking: bool = False, max_retries: int = 0
) -> str:
"""Get text completion asynchronously."""
model = self._resolve_model(self.model or "gpt-4o-mini")
messages = [{"role": "user", "content": prompt}]
original_thinking = self._thinking
if thinking:
self._thinking = thinking
kwargs = self._build_kwargs(model, messages)
self._thinking = original_thinking

last_error = None
for attempt in range(max_retries + 1):
Expand All @@ -171,7 +181,7 @@ async def get_completion_async(self, prompt: str, max_retries: int = 0) -> str:
except Exception as e:
last_error = e
if attempt < max_retries:
await asyncio.sleep(2 ** attempt)
await asyncio.sleep(2**attempt)

if last_error:
raise last_error
Expand All @@ -181,6 +191,7 @@ def get_vision_completion(
self,
prompt: str,
images: List[Union[str, Path, bytes]],
thinking: bool = False,
) -> str:
"""Get vision completion synchronously."""
model = self._resolve_model(self.model or "gpt-4o-mini")
Expand All @@ -191,7 +202,11 @@ def get_vision_completion(
content.append({"type": "text", "text": prompt})

messages = [{"role": "user", "content": content}]
original_thinking = self._thinking
if thinking:
self._thinking = thinking
kwargs = self._build_kwargs(model, messages)
self._thinking = original_thinking

response = completion(**kwargs)
self._update_token_usage_from_response(response)
Expand All @@ -201,6 +216,7 @@ async def get_vision_completion_async(
self,
prompt: str,
images: List[Union[str, Path, bytes]],
thinking: bool = False,
) -> str:
"""Get vision completion asynchronously."""
model = self._resolve_model(self.model or "gpt-4o-mini")
Expand All @@ -211,7 +227,11 @@ async def get_vision_completion_async(
content.append({"type": "text", "text": prompt})

messages = [{"role": "user", "content": content}]
original_thinking = self._thinking
if thinking:
self._thinking = thinking
kwargs = self._build_kwargs(model, messages)
self._thinking = original_thinking

response = await acompletion(**kwargs)
self._update_token_usage_from_response(response)
Expand Down
32 changes: 16 additions & 16 deletions openviking/models/vlm/backends/openai_vlm.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,6 @@ def get_completion(self, prompt: str, thinking: bool = False) -> str:
"temperature": self.temperature,
}

if self.provider == "volcengine":
kwargs["thinking"] = {"type": "disabled" if not thinking else "enabled"}

response = client.chat.completions.create(**kwargs)
self._update_token_usage_from_response(response)
return response.choices[0].message.content or ""
Expand All @@ -79,9 +76,6 @@ async def get_completion_async(
"temperature": self.temperature,
}

if self.provider == "volcengine":
kwargs["thinking"] = {"type": "disabled" if not thinking else "enabled"}

last_error = None
for attempt in range(max_retries + 1):
try:
Expand Down Expand Up @@ -131,6 +125,7 @@ def get_vision_completion(
self,
prompt: str,
images: List[Union[str, Path, bytes]],
thinking: bool = False,
) -> str:
"""Get vision completion"""
client = self.get_client()
Expand All @@ -140,18 +135,21 @@ def get_vision_completion(
content.append(self._prepare_image(img))
content.append({"type": "text", "text": prompt})

response = client.chat.completions.create(
model=self.model or "gpt-4o-mini",
messages=[{"role": "user", "content": content}],
temperature=self.temperature,
)
kwargs = {
"model": self.model or "gpt-4o-mini",
"messages": [{"role": "user", "content": content}],
"temperature": self.temperature,
}

response = client.chat.completions.create(**kwargs)
self._update_token_usage_from_response(response)
return response.choices[0].message.content or ""

async def get_vision_completion_async(
self,
prompt: str,
images: List[Union[str, Path, bytes]],
thinking: bool = False,
) -> str:
"""Get vision completion asynchronously"""
client = self.get_async_client()
Expand All @@ -161,10 +159,12 @@ async def get_vision_completion_async(
content.append(self._prepare_image(img))
content.append({"type": "text", "text": prompt})

response = await client.chat.completions.create(
model=self.model or "gpt-4o-mini",
messages=[{"role": "user", "content": content}],
temperature=self.temperature,
)
kwargs = {
"model": self.model or "gpt-4o-mini",
"messages": [{"role": "user", "content": content}],
"temperature": self.temperature,
}

response = await client.chat.completions.create(**kwargs)
self._update_token_usage_from_response(response)
return response.choices[0].message.content or ""
115 changes: 109 additions & 6 deletions openviking/models/vlm/backends/volcengine_vlm.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
# SPDX-License-Identifier: Apache-2.0
"""VolcEngine VLM backend implementation"""

import asyncio
import base64
from pathlib import Path
from typing import Any, Dict, List, Union

Expand Down Expand Up @@ -54,22 +56,123 @@ def get_async_client(self):
)
return self._async_client

def get_completion(self, prompt: str) -> str:
return super().get_completion(prompt)
def get_completion(self, prompt: str, thinking: bool = False) -> str:
"""Get text completion"""
client = self.get_client()
kwargs = {
"model": self.model or "doubao-seed-1-8-251228",
"messages": [{"role": "user", "content": prompt}],
"temperature": self.temperature,
"thinking": {"type": "disabled" if not thinking else "enabled"},
}

async def get_completion_async(self, prompt: str, max_retries: int = 0) -> str:
return await super().get_completion_async(prompt, max_retries)
response = client.chat.completions.create(**kwargs)
self._update_token_usage_from_response(response)
return response.choices[0].message.content or ""

async def get_completion_async(
self, prompt: str, thinking: bool = False, max_retries: int = 0
) -> str:
"""Get text completion asynchronously"""
client = self.get_async_client()
kwargs = {
"model": self.model or "doubao-seed-1-8-251228",
"messages": [{"role": "user", "content": prompt}],
"temperature": self.temperature,
"thinking": {"type": "disabled" if not thinking else "enabled"},
}

last_error = None
for attempt in range(max_retries + 1):
try:
response = await client.chat.completions.create(**kwargs)
self._update_token_usage_from_response(response)
return response.choices[0].message.content or ""
except Exception as e:
last_error = e
if attempt < max_retries:
await asyncio.sleep(2**attempt)

if last_error:
raise last_error
else:
raise RuntimeError("Unknown error in async completion")

def _prepare_image(self, image: Union[str, Path, bytes]) -> Dict[str, Any]:
"""Prepare image data"""
if isinstance(image, bytes):
b64 = base64.b64encode(image).decode("utf-8")
return {
"type": "image_url",
"image_url": {"url": f"data:image/png;base64,{b64}"},
}
elif isinstance(image, Path) or (
isinstance(image, str) and not image.startswith(("http://", "https://"))
):
path = Path(image)
suffix = path.suffix.lower()
mime_type = {
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".webp": "image/webp",
}.get(suffix, "image/png")
with open(path, "rb") as f:
b64 = base64.b64encode(f.read()).decode("utf-8")
return {
"type": "image_url",
"image_url": {"url": f"data:{mime_type};base64,{b64}"},
}
else:
return {"type": "image_url", "image_url": {"url": image}}

def get_vision_completion(
self,
prompt: str,
images: List[Union[str, Path, bytes]],
thinking: bool = False,
) -> str:
return super().get_vision_completion(prompt, images)
"""Get vision completion"""
client = self.get_client()

content = []
for img in images:
content.append(self._prepare_image(img))
content.append({"type": "text", "text": prompt})

kwargs = {
"model": self.model or "doubao-seed-1-8-251228",
"messages": [{"role": "user", "content": content}],
"temperature": self.temperature,
"thinking": {"type": "disabled" if not thinking else "enabled"},
}

response = client.chat.completions.create(**kwargs)
self._update_token_usage_from_response(response)
return response.choices[0].message.content or ""

async def get_vision_completion_async(
self,
prompt: str,
images: List[Union[str, Path, bytes]],
thinking: bool = False,
) -> str:
return await super().get_vision_completion_async(prompt, images)
"""Get vision completion asynchronously"""
client = self.get_async_client()

content = []
for img in images:
content.append(self._prepare_image(img))
content.append({"type": "text", "text": prompt})

kwargs = {
"model": self.model or "doubao-seed-1-8-251228",
"messages": [{"role": "user", "content": content}],
"temperature": self.temperature,
"thinking": {"type": "disabled" if not thinking else "enabled"},
}

response = await client.chat.completions.create(**kwargs)
self._update_token_usage_from_response(response)
return response.choices[0].message.content or ""
Loading