Skip to content
Open
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
139 changes: 139 additions & 0 deletions FreeLLMAPI/FreeLLMAPI.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
<?php

namespace App\SupportedApps\FreeLLMAPI;

class FreeLLMAPI extends \App\SupportedApps implements \App\EnhancedApps
{
public $config;

public function test()
{
try {
$this->providers();
echo "Successfully communicated with the API";
} catch (\Throwable $e) {
echo $e->getMessage();
}
}

public function livestats()
{
$data = [
"providers" => null,
"attention" => null,
];
try {
$providers = $this->providers();
$data["providers"] = count($providers);
$data["attention"] = 0;
foreach ($providers as $provider) {
if ($provider->status !== "healthy") {
$data["attention"]++;
}
}
return parent::getLiveStats("active", $data);
} catch (\Throwable $e) {
return parent::getLiveStats("inactive", $data);
}
}

private function providers()
{
$result = $this->request("providers");
if (!isset($result->providers) || !is_array($result->providers)) {
throw new \RuntimeException("Failed: Invalid provider response");
}
foreach ($result->providers as $provider) {
if (
!is_object($provider) ||
!isset($provider->name, $provider->status) ||
!is_string($provider->name) ||
!in_array(
$provider->status,
["healthy", "rate_limited", "invalid", "unknown"],
true
)
) {
throw new \RuntimeException("Failed: Invalid provider status");
}
}
return $result->providers;
}

private function request($endpoint)
{
$key = trim($this->config->api_key ?? "");
if ($key === "") {
throw new \RuntimeException(
"Failed: A unified API key is required"
);
}
$url = $this->url($endpoint);
try {
$response = parent::execute($url, [
"headers" => [
"Accept" => "application/json",
"Authorization" => "Bearer " . $key,
],
"timeout" => 5,
"connect_timeout" => 3,
"allow_redirects" => false,
]);
} catch (\Throwable $e) {
throw new \RuntimeException(
"Failed: Could not connect to FreeLLMAPI"
);
}
if ($response === null) {
throw new \RuntimeException(
"Failed: Could not connect to FreeLLMAPI"
);
}
$code = $response->getStatusCode();
if ($code === 401 || $code === 403) {
throw new \RuntimeException(
"Failed: Invalid unified API key (client-profile keys are not supported)"
);
}
if ($code !== 200) {
throw new \RuntimeException(
"Failed: FreeLLMAPI returned HTTP " .
$code .
" for " .
$endpoint
);
}
$result = json_decode((string) $response->getBody());
if (!is_object($result)) {
throw new \RuntimeException(
"Failed: FreeLLMAPI returned invalid JSON"
);
}
return $result;
}

public function url($endpoint)
{
$base = rtrim(trim($this->config->url ?? ""), "/");
$parts = parse_url($base);
if (
!$parts ||
!in_array(
strtolower($parts["scheme"] ?? ""),
["http", "https"],
true
) ||
empty($parts["host"]) ||
isset($parts["user"]) ||
isset($parts["pass"]) ||
isset($parts["query"]) ||
isset($parts["fragment"])
) {
throw new \RuntimeException(
"Failed: Specify the FreeLLMAPI base HTTP or HTTPS URL"
);
}
$base = preg_replace('~/v1$~', "", $base);
return $base . "/v1/" . $endpoint;
}
}
10 changes: 10 additions & 0 deletions FreeLLMAPI/app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"appid": "3f0af0d4dccdfd367e649a61d17291787e6a1aef",
"name": "FreeLLMAPI",
"website": "https://github.com/tashfeenahmed/freellmapi",
"license": "MIT License",
"description": "FreeLLMAPI aggregates free LLM providers behind one OpenAI-compatible API with automatic routing and failover.",
"enhanced": true,
"tile_background": "dark",
"icon": "freellmapi.svg"
}
15 changes: 15 additions & 0 deletions FreeLLMAPI/config.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<h2>{{ __('app.apps.config') }} ({{ __('app.optional') }}) @include('items.enable')</h2>
<div class="items">
<div class="input">
<label>{{ strtoupper(__('app.url')) }}</label>
{!! Form::text('config[override_url]', isset($item) ? $item->getconfig()->override_url ?? null : null, ['placeholder' => __('app.apps.override'), 'id' => 'override_url', 'class' => 'form-control']) !!}
</div>
<div class="input">
<label>Unified API key</label>
{!! Form::input('password', 'config[api_key]', isset($item) ? $item->getconfig()->api_key ?? null : null, ['placeholder' => 'Unified API key', 'data-config' => 'api_key', 'class' => 'form-control config-item', 'autocomplete' => 'off']) !!}
<small>Use the unified key from FreeLLMAPI Settings.</small>
</div>
<div class="input">
<button style="margin-top: 32px;" class="btn test" id="test_config">Test</button>
</div>
</div>
1 change: 1 addition & 0 deletions FreeLLMAPI/freellmapi.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions FreeLLMAPI/livestats.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<ul class="livestats">
<li>
<span class="title">Total<br>Providers</span>
<strong>{{ $providers === null ? 'Unavailable' : number_format($providers) }}</strong>
</li>
<li>
<span class="title">Attention<br>Needed</span>
<strong>{{ $attention === null ? 'Unavailable' : number_format($attention) }}</strong>
</li>
</ul>
Loading