mirror of
https://github.com/xtekky/gpt4free.git
synced 2026-04-22 15:47:11 +08:00
Refactor provider method handling for improved clarity and consistency; update mocks and add SKILL.md for usage guidance
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
# SKILL.md
|
||||
|
||||
## Using gpt4free as an LLM Server for Bots (Clawbot/OpenClaw)
|
||||
|
||||
### Overview
|
||||
This skill covers running gpt4free as a local LLM server with an OpenAI-compatible REST API, custom model routing (config.yaml), and integration with bots like Clawbot or OpenClaw.
|
||||
|
||||
### Best Practices
|
||||
- Start the API server with: `python -m g4f --port 8080` (or use `g4f api --debug --port 8080`)
|
||||
- Use the `/v1` endpoint for OpenAI-compatible requests (e.g., POST to `http://localhost:8080/v1/chat/completions`)
|
||||
- Define custom model routes in `config.yaml` to aggregate/fallback across providers
|
||||
- Place `config.yaml` in your cookies directory (e.g., `~/.g4f/cookies/config.yaml`)
|
||||
- For Clawbot/OpenClaw, patch their config to point to your gpt4free server (see `patch-openclaw.py`)
|
||||
- Test with: `g4f client "Hello" --model openclaw` or Python client
|
||||
|
||||
### Common Pitfalls
|
||||
- Not starting the server before connecting bots
|
||||
- Incorrect config.yaml path or syntax errors
|
||||
- Missing required Python dependencies (install with `pip install -r requirements.txt`)
|
||||
- Not exposing the correct port (default 8080)
|
||||
- Forgetting to patch bot configs to use your local endpoint
|
||||
|
||||
### Workflow Steps
|
||||
1. Install and set up gpt4free (see README)
|
||||
2. Start the API server: `python -m g4f --port 8080`
|
||||
3. (Optional) Create or edit `config.yaml` for custom model routing:
|
||||
```yaml
|
||||
models:
|
||||
- name: "openclaw"
|
||||
providers:
|
||||
- provider: "GeminiCLI"
|
||||
model: "gemini-3-flash-preview"
|
||||
condition: "quota.models.gemini-3-flash-preview.remainingFraction > 0 and error_count < 3"
|
||||
- provider: "Antigravity"
|
||||
model: "gemini-3-flash"
|
||||
- provider: "PollinationsAI"
|
||||
model: "openai"
|
||||
```
|
||||
4. Patch your bot config (e.g., OpenClaw) to use `http://localhost:8080/v1` as the base URL (see `scripts/patch-openclaw.py`)
|
||||
5. Start your bot and verify it connects to gpt4free
|
||||
6. Monitor logs and test with the Python client or CLI
|
||||
|
||||
### References
|
||||
- [README.md](../README.md)
|
||||
- [docs/config-yaml-routing.md](../docs/config-yaml-routing.md)
|
||||
- [scripts/patch-openclaw.py](../scripts/patch-openclaw.py)
|
||||
- [scripts/setup-openclaw.sh](../scripts/setup-openclaw.sh)
|
||||
- [g4f/client/__init__.py](../g4f/client/__init__.py)
|
||||
@@ -15,8 +15,8 @@ from .retry_provider import *
|
||||
from .thinking import *
|
||||
from .web_search import *
|
||||
from .models import *
|
||||
from .mcp import *
|
||||
from .tool_support_provider import *
|
||||
from .config_provider import *
|
||||
#from .mcp import *
|
||||
#from .tool_support_provider import *
|
||||
#from .config_provider import *
|
||||
|
||||
unittest.main()
|
||||
|
||||
@@ -5,6 +5,7 @@ import unittest
|
||||
from g4f.errors import ModelNotFoundError
|
||||
from g4f.client import Client, AsyncClient, ChatCompletion, ChatCompletionChunk
|
||||
from g4f.client.service import get_model_and_provider
|
||||
from g4f.providers.types import BaseProvider
|
||||
from g4f.Provider.Copilot import Copilot
|
||||
from g4f.models import gpt_4o
|
||||
from .mocks import AsyncGeneratorProviderMock, ModelProviderMock, YieldProviderMock
|
||||
@@ -117,25 +118,28 @@ class TestPassModel(unittest.TestCase):
|
||||
def test_best_provider(self):
|
||||
not_default_model = "gpt-4o"
|
||||
model, provider = get_model_and_provider(not_default_model, None, False)
|
||||
self.assertTrue(hasattr(provider, "create_completion"))
|
||||
self.assertIsInstance(model, str)
|
||||
self.assertIsInstance(provider, (type, BaseProvider))
|
||||
self.assertEqual(model, not_default_model)
|
||||
|
||||
def test_default_model(self):
|
||||
default_model = ""
|
||||
model, provider = get_model_and_provider(default_model, None, False)
|
||||
self.assertTrue(hasattr(provider, "create_completion"))
|
||||
self.assertIsInstance(model, str)
|
||||
self.assertIsInstance(provider, (type, BaseProvider))
|
||||
self.assertEqual(model, default_model)
|
||||
|
||||
def test_provider_as_model(self):
|
||||
provider_as_model = Copilot.__name__
|
||||
model, provider = get_model_and_provider(provider_as_model, None, False)
|
||||
self.assertTrue(hasattr(provider, "create_completion"))
|
||||
self.assertIsInstance(model, str)
|
||||
self.assertIsInstance(provider, (type, BaseProvider))
|
||||
self.assertEqual(model, Copilot.default_model)
|
||||
|
||||
def test_get_model(self):
|
||||
model, provider = get_model_and_provider(gpt_4o.name, None, False)
|
||||
self.assertTrue(hasattr(provider, "create_completion"))
|
||||
self.assertIsInstance(model, str)
|
||||
self.assertIsInstance(provider, (type, BaseProvider))
|
||||
self.assertEqual(model, gpt_4o.name)
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -4,6 +4,7 @@ from g4f.errors import MissingAuthError
|
||||
|
||||
class ProviderMock(AbstractProvider):
|
||||
working = True
|
||||
use_stream_timeout = False
|
||||
|
||||
@classmethod
|
||||
def create_completion(
|
||||
@@ -13,6 +14,7 @@ class ProviderMock(AbstractProvider):
|
||||
|
||||
class AsyncProviderMock(AsyncProvider):
|
||||
working = True
|
||||
use_stream_timeout = False
|
||||
|
||||
@classmethod
|
||||
async def create_async(
|
||||
@@ -22,6 +24,7 @@ class AsyncProviderMock(AsyncProvider):
|
||||
|
||||
class AsyncGeneratorProviderMock(AsyncGeneratorProvider):
|
||||
working = True
|
||||
use_stream_timeout = False
|
||||
|
||||
@classmethod
|
||||
async def create_async_generator(
|
||||
@@ -29,8 +32,10 @@ class AsyncGeneratorProviderMock(AsyncGeneratorProvider):
|
||||
):
|
||||
yield "Mock"
|
||||
|
||||
|
||||
class ModelProviderMock(AbstractProvider):
|
||||
working = True
|
||||
use_stream_timeout = False # Added to fix unittest error
|
||||
|
||||
@classmethod
|
||||
def create_completion(
|
||||
@@ -40,6 +45,7 @@ class ModelProviderMock(AbstractProvider):
|
||||
|
||||
class YieldProviderMock(AsyncGeneratorProvider):
|
||||
working = True
|
||||
use_stream_timeout = False
|
||||
|
||||
@classmethod
|
||||
async def create_async_generator(
|
||||
@@ -50,6 +56,7 @@ class YieldProviderMock(AsyncGeneratorProvider):
|
||||
|
||||
class YieldImageResponseProviderMock(AsyncGeneratorProvider):
|
||||
working = True
|
||||
use_stream_timeout = False
|
||||
|
||||
@classmethod
|
||||
async def create_async_generator(
|
||||
@@ -58,6 +65,7 @@ class YieldImageResponseProviderMock(AsyncGeneratorProvider):
|
||||
yield ImageResponse(prompt, "")
|
||||
|
||||
class MissingAuthProviderMock(AbstractProvider):
|
||||
use_stream_timeout = False
|
||||
working = True
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -73,7 +73,6 @@ class ChatCompletion:
|
||||
)
|
||||
method = get_provider_method(provider)
|
||||
result = method(model, messages, stream=stream, **kwargs)
|
||||
result = to_sync_generator(result)
|
||||
return result if stream or ignore_stream else concat_chunks(result)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -89,7 +89,9 @@ def get_async_provider_method(provider: type) -> Optional[callable]:
|
||||
if hasattr(provider, "create_async_generator"):
|
||||
return provider.create_async_generator
|
||||
if hasattr(provider, "create_async"):
|
||||
return provider.create_async
|
||||
async def wrapper(*args, **kwargs):
|
||||
yield await provider.create_async(*args, **kwargs)
|
||||
return wrapper
|
||||
if hasattr(provider, "create_completion"):
|
||||
async def wrapper(*args, **kwargs):
|
||||
for chunk in provider.create_completion(*args, **kwargs):
|
||||
@@ -102,9 +104,13 @@ def get_provider_method(provider: type) -> Optional[callable]:
|
||||
if hasattr(provider, "create_completion"):
|
||||
return provider.create_completion
|
||||
if hasattr(provider, "create_async_generator"):
|
||||
return provider.create_async_generator
|
||||
def wrapper(*args, **kwargs):
|
||||
return to_sync_generator(provider.create_async_generator(*args, **kwargs), stream=provider.supports_stream)
|
||||
return wrapper
|
||||
if hasattr(provider, "create_async"):
|
||||
return provider.create_async
|
||||
def wrapper(*args, **kwargs):
|
||||
yield asyncio.run(provider.create_async(*args, **kwargs))
|
||||
return wrapper
|
||||
raise NotImplementedError(f"{provider.__name__} does not implement a create method")
|
||||
|
||||
class AbstractProvider(BaseProvider):
|
||||
|
||||
@@ -109,7 +109,7 @@ class RotatedProvider(BaseRetryProvider):
|
||||
method = get_async_provider_method(provider)
|
||||
response = method(model=alias, messages=messages, **extra_body)
|
||||
started = False
|
||||
async for chunk in to_async_iterator(response):
|
||||
async for chunk in response:
|
||||
if isinstance(chunk, JsonConversation):
|
||||
if conversation is None: conversation = JsonConversation()
|
||||
setattr(conversation, provider.__name__, chunk.get_dict())
|
||||
@@ -168,7 +168,7 @@ class IterListProvider(BaseRetryProvider):
|
||||
try:
|
||||
method = get_async_provider_method(provider)
|
||||
response = method(model=alias, messages=messages, **extra_body)
|
||||
async for chunk in to_async_iterator(response):
|
||||
async for chunk in response:
|
||||
if isinstance(chunk, JsonConversation):
|
||||
if conversation is None:
|
||||
conversation = JsonConversation()
|
||||
@@ -231,7 +231,7 @@ class RetryProvider(IterListProvider):
|
||||
debug.log(f"Using {provider.__name__} provider (attempt {attempt + 1})")
|
||||
method = get_async_provider_method(provider)
|
||||
response = method(model=model, messages=messages, **kwargs)
|
||||
async for chunk in to_async_iterator(response):
|
||||
async for chunk in response:
|
||||
yield chunk
|
||||
if is_content(chunk):
|
||||
started = True
|
||||
@@ -255,9 +255,6 @@ def raise_exceptions(exceptions: dict) -> None:
|
||||
RetryNoProviderError: If no provider is found.
|
||||
"""
|
||||
if exceptions:
|
||||
for provider_name, e in exceptions.items():
|
||||
if isinstance(e, (MissingAuthError, NoValidHarFileError)):
|
||||
raise e
|
||||
if len(exceptions) == 1:
|
||||
raise list(exceptions.values())[0]
|
||||
raise RetryProviderError("RetryProvider failed:\n" + "\n".join([
|
||||
|
||||
@@ -75,14 +75,14 @@ class ToolSupportProvider(AsyncGeneratorProvider):
|
||||
chunks = []
|
||||
has_usage = False
|
||||
method = get_async_provider_method(provider)
|
||||
async for chunk in to_async_iterator(method(
|
||||
model,
|
||||
messages,
|
||||
async for chunk in method(
|
||||
model=model,
|
||||
messages=messages,
|
||||
stream=stream,
|
||||
media=media,
|
||||
response_format=response_format,
|
||||
**kwargs,
|
||||
)):
|
||||
):
|
||||
if isinstance(chunk, str):
|
||||
chunks.append(chunk)
|
||||
elif isinstance(chunk, Usage):
|
||||
|
||||
@@ -298,9 +298,7 @@ async def async_iter_run_tools(
|
||||
|
||||
# Generate response
|
||||
method = get_async_provider_method(provider)
|
||||
response = to_async_iterator(
|
||||
method(model=model, messages=messages, **kwargs)
|
||||
)
|
||||
response = method(model=model, messages=messages, **kwargs)
|
||||
timeout = kwargs.get("stream_timeout") if provider.use_stream_timeout else kwargs.get("timeout")
|
||||
response = wait_for(response, timeout=timeout) if stream else response
|
||||
|
||||
@@ -476,9 +474,9 @@ def iter_run_tools(
|
||||
completion_tokens = 0
|
||||
usage = None
|
||||
method = get_provider_method(provider)
|
||||
for chunk in to_sync_generator(method(
|
||||
for chunk in method(
|
||||
model=model, messages=messages, provider=provider, **kwargs
|
||||
)):
|
||||
):
|
||||
if isinstance(chunk, FinishReason):
|
||||
if sources is not None:
|
||||
yield sources
|
||||
|
||||
Reference in New Issue
Block a user