openwebui_base: new module for using openwebui with Odoo

This commit is contained in:
Marc Durepos 2025-07-17 16:14:57 -04:00
parent c0834eb327
commit eff17d674d
15 changed files with 437 additions and 0 deletions

View file

@ -0,0 +1 @@
from . import models

View file

@ -0,0 +1,30 @@
# Bemade Inc.
#
# Copyright (C) 2023-June Bemade Inc. (<https://www.bemade.org>).
# Author: Marc Durepos (Contact : mdurepos@durpro.com)
#
# This program is under the terms of the GNU Lesser General Public License (LGPL-3)
# For details, visit https://www.gnu.org/licenses/lgpl-3.0.en.html
{
"name": "OpenWebUI Base",
"version": "18.0.0.1.0",
"license": "LGPL-3",
"category": "Tools",
"summary": "Base module for OpenWebUI integration",
"author": "Bemade Inc.",
"website": "https://www.bemade.org",
"depends": ["base", "base_setup"],
"external_dependencies": {
"python": ["openwebui-client>=0.3.0"],
},
"data": [
"security/ir.model.access.csv",
"views/openwebui_provider_views.xml",
"views/openwebui_model_views.xml",
"views/res_config_settings.xml",
],
"installable": True,
"application": False,
"auto_install": False,
}

View file

@ -0,0 +1,4 @@
from . import res_company
from . import res_config_settings
from . import openwebui_model
from . import openwebui_provider

View file

@ -0,0 +1,29 @@
from odoo import models, fields, api
class OpenWebUIModel(models.Model):
_name = "openwebui.model"
_description = "OpenWebUI Model"
technical_name = fields.Char(
required=True,
help="Technical name of the model on the OpenWebUI server.",
readonly=True,
)
name = fields.Char(
required=True,
help="User-friendly name of the model.",
readonly=True,
)
provider_id = fields.Many2one(
"openwebui.provider",
required=True,
)
_sql_constraints = [
(
"technical_name_provider_unique",
"unique(technical_name, provider_id)",
"Technical name must be unique per provider",
)
]

View file

@ -0,0 +1,66 @@
from odoo import models, fields, api
from .openwebui_model import OpenWebUIModel
import openwebui_client
from typing import Optional
class OpenWebUIProvider(models.Model):
_name = "openwebui.provider"
_description = "OpenWebUI Provider"
name = fields.Char(string="Name", required=True)
base_url = fields.Char(
string="Base URL",
required=True,
help="Base URL of the OpenWebUI server.",
)
api_key = fields.Char(
string="API Key",
required=True,
help="API key for authentication.",
groups="base.group_system",
)
model_ids = fields.One2many(
comodel_name="openwebui.model",
inverse_name="provider_id",
)
default_model_id = fields.Many2one(
comodel_name="openwebui.model",
)
def get_client(
self, model: Optional[OpenWebUIModel] = None
) -> openwebui_client.OpenWebUIClient:
if not self.base_url or not self.api_key:
raise ValueError("Base URL and API key are required")
model = model or self.default_model_id
return openwebui_client.OpenWebUIClient(
base_url=self.base_url,
api_key=self.api_key,
default_model=model.technical_name if model else None,
)
def sync_models(self):
client = self.get_client()
local_models = self.model_ids
remote_models = {model.id: model.name for model in client.models.list()}
for model in local_models:
if model.technical_name not in remote_models.keys():
model.unlink()
elif model.name != remote_models[model.technical_name]:
model.name = remote_models[model.technical_name]
for model_id, model_name in remote_models.items():
if model_id not in local_models.mapped("technical_name"):
self.env["openwebui.model"].create(
{
"name": model_name,
"technical_name": model_id,
"provider_id": self.id,
}
)
def create(self, vals_list):
providers = super().create(vals_list)
for provider in providers:
provider.sync_models()
return providers

View file

@ -0,0 +1,15 @@
from odoo import models, fields
class Company(models.Model):
_inherit = "res.company"
openwebui_provider_id = fields.Many2one(
"openwebui.provider",
string="OpenWebUI Provider",
)
openwebui_default_model_id = fields.Many2one(
"openwebui.model",
string="OpenWebUI Default Model",
domain=[("provider_id", "=", "openwebui_provider_id")],
)

View file

@ -0,0 +1,19 @@
from odoo import models, fields
class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"
openwebui_provider_id = fields.Many2one(
comodel_name="openwebui.provider",
related="company_id.openwebui_provider_id",
readonly=False,
company_dependent=True,
)
openwebui_default_model_id = fields.Many2one(
comodel_name="openwebui.model",
related="company_id.openwebui_default_model_id",
readonly=False,
company_dependent=True,
)

View file

@ -0,0 +1,5 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_openwebui_provider_admin,openwebui.provider admin,model_openwebui_provider,base.group_system,1,1,1,1
access_openwebui_provider_user,openwebui.provider user,model_openwebui_provider,base.group_user,1,0,0,0
access_openwebui_model_admin,openwebui.model admin,model_openwebui_model,base.group_system,1,1,1,1
access_openwebui_model_user,openwebui.model user,model_openwebui_model,base.group_user,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_openwebui_provider_admin openwebui.provider admin model_openwebui_provider base.group_system 1 1 1 1
3 access_openwebui_provider_user openwebui.provider user model_openwebui_provider base.group_user 1 0 0 0
4 access_openwebui_model_admin openwebui.model admin model_openwebui_model base.group_system 1 1 1 1
5 access_openwebui_model_user openwebui.model user model_openwebui_model base.group_user 1 0 0 0

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -0,0 +1,7 @@
<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
<!-- Background with rounded corners -->
<rect width="128" height="128" rx="24" ry="24" fill="#1a1a1a"/>
<!-- OI Text -->
<text x="64" y="80" font-family="Arial, sans-serif" font-size="48" font-weight="bold" text-anchor="middle" fill="#f5f5dc">OI</text>
</svg>

After

Width:  |  Height:  |  Size: 359 B

View file

@ -0,0 +1 @@
from . import test_openwebui

View file

@ -0,0 +1,52 @@
from odoo.tests import TransactionCase, Form
import os
class TestOpenWebUI(TransactionCase):
def setUp(self):
super().setUp()
self.provider = self.env["openwebui.provider"].create(
{
"name": "Test Provider",
"base_url": os.getenv("OPENWEBUI_BASE_URL"),
"api_key": os.getenv("OPENWEBUI_API_KEY"),
}
)
def test_sync_models(self):
self.assertTrue(self.provider.model_ids)
self.assertIn(
"MS.qwen3:32b-q8_0", self.provider.model_ids.mapped("technical_name")
)
def test_get_client(self):
client = self.provider.get_client()
self.assertTrue(client)
def test_settings(self):
model = self.provider.model_ids.filtered(
lambda model: model.technical_name == "MS.qwen3:32b-q8_0"
)
wizard = self.env["res.config.settings"].create({})
with Form(wizard) as form:
form.openwebui_provider_id = self.provider
form.openwebui_default_model_id = model
self.assertEqual(self.env.company.openwebui_provider_id, self.provider)
self.assertEqual(self.env.company.openwebui_default_model_id, model)
def test_openwebui_chat(self):
model = self.provider.model_ids.filtered(
lambda model: model.technical_name == "MS.qwen3:32b-q8_0"
)
self.env.company.openwebui_default_model_id = model
self.env.company.openwebui_provider_id = self.provider
response = self.provider.get_client().chat.completions.create(
model=model.technical_name,
messages=[
{
"role": "user",
"content": "Hello, how are you?",
}
],
)
self.assertTrue(response and response.choices)

View file

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Form View for OpenWebUI Model -->
<record id="view_openwebui_model_form" model="ir.ui.view">
<field name="name">openwebui.model.form</field>
<field name="model">openwebui.model</field>
<field name="arch" type="xml">
<form string="AI Model">
<sheet>
<div class="oe_title">
<h1>
<field name="name" placeholder="Model Name"/>
</h1>
</div>
<group>
<group>
<field name="technical_name" readonly="1"/>
<field name="provider_id" readonly="1"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<!-- Tree View for OpenWebUI Model -->
<record id="view_openwebui_model_tree" model="ir.ui.view">
<field name="name">openwebui.model.tree</field>
<field name="model">openwebui.model</field>
<field name="arch" type="xml">
<list string="AI Models">
<field name="name"/>
<field name="technical_name"/>
<field name="provider_id"/>
</list>
</field>
</record>
<!-- Search View for OpenWebUI Model -->
<record id="view_openwebui_model_search" model="ir.ui.view">
<field name="name">openwebui.model.search</field>
<field name="model">openwebui.model</field>
<field name="arch" type="xml">
<search string="Search AI Models">
<field name="name"/>
<field name="technical_name"/>
<field name="provider_id"/>
<group expand="0" string="Group By">
<filter string="Provider" name="group_by_provider" context="{'group_by': 'provider_id'}"/>
</group>
</search>
</field>
</record>
<!-- Action for OpenWebUI Model -->
<record id="action_openwebui_model" model="ir.actions.act_window">
<field name="name">AI Models</field>
<field name="res_model">openwebui.model</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No AI models found
</p>
<p>
AI models are synchronized from your providers.
Add a provider and sync models to see them here.
</p>
</field>
</record>
<!-- Menu Item for OpenWebUI Model -->
<menuitem id="menu_openwebui_model"
name="AI Models"
parent="menu_openwebui_configuration"
action="action_openwebui_model"
sequence="20"
groups="base.group_system"/>
</odoo>

View file

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Form View for OpenWebUI Provider -->
<record id="view_openwebui_provider_form" model="ir.ui.view">
<field name="name">openwebui.provider.form</field>
<field name="model">openwebui.provider</field>
<field name="arch" type="xml">
<form string="OpenWebUI Provider">
<sheet>
<div class="oe_title">
<h1>
<field name="name" placeholder="Provider Name"/>
</h1>
</div>
<group>
<group>
<field name="base_url" placeholder="https://api.example.com"/>
<field name="api_key" password="True" groups="base.group_system"/>
</group>
<group>
<field name="default_model_id" domain="[('provider_id', '=', id)]"/>
</group>
</group>
<notebook>
<page string="Models" name="models">
<field name="model_ids" readonly="1">
<list>
<field name="name"/>
<field name="technical_name"/>
</list>
</field>
<div class="oe_button_box" name="button_box">
<button name="sync_models" string="Sync Models" type="object" class="oe_highlight" groups="base.group_system"/>
</div>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- Tree View for OpenWebUI Provider -->
<record id="view_openwebui_provider_tree" model="ir.ui.view">
<field name="name">openwebui.provider.tree</field>
<field name="model">openwebui.provider</field>
<field name="arch" type="xml">
<list string="OpenWebUI Providers">
<field name="name"/>
<field name="base_url"/>
</list>
</field>
</record>
<!-- Search View for OpenWebUI Provider -->
<record id="view_openwebui_provider_search" model="ir.ui.view">
<field name="name">openwebui.provider.search</field>
<field name="model">openwebui.provider</field>
<field name="arch" type="xml">
<search string="Search OpenWebUI Providers">
<field name="name"/>
<field name="base_url"/>
</search>
</field>
</record>
<!-- Action for OpenWebUI Provider -->
<record id="action_openwebui_provider" model="ir.actions.act_window">
<field name="name">AI Providers</field>
<field name="res_model">openwebui.provider</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first AI provider
</p>
<p>
Configure AI providers to connect with OpenWebUI.
</p>
</field>
</record>
<!-- Menu Item for OpenWebUI Provider -->
<menuitem id="menu_openwebui_root"
name="OpenWebUI"
sequence="100"
groups="base.group_user"/>
<menuitem id="menu_openwebui_configuration"
name="Configuration"
parent="menu_openwebui_root"
sequence="100"
groups="base.group_system"/>
<menuitem id="menu_openwebui_provider"
name="AI Providers"
parent="menu_openwebui_configuration"
action="action_openwebui_provider"
sequence="10"
groups="base.group_system"/>
</odoo>

View file

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.openwebui.base</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//form" position="inside">
<app string="OpenWebUI" name="openwebui_base" groups="base.group_system">
<block title="AI Configuration" id="openwebui_config_container">
<setting id="ai_provider" help="Select the AI provider to use with OpenWebUI">
<field name="openwebui_provider_id"/>
</setting>
<setting id="default_model" help="Select the default model to use with OpenWebUI">
<field name="openwebui_default_model_id"/>
</setting>
</block>
</app>
</xpath>
</field>
</record>
<record id="action_openwebui_config_settings" model="ir.actions.act_window">
<field name="name">Settings</field>
<field name="res_model">res.config.settings</field>
<field name="view_id" ref="res_config_settings_view_form"/>
<field name="view_mode">form</field>
<field name="target">inline</field>
<field name="context">{'module' : 'openwebui_base', 'bin_size': False}</field>
</record>
</odoo>