├── .deploy ├── docker-compose.yml └── nginx-proxy-compose.yml ├── .github └── workflows │ ├── README.md │ ├── build-container.yml │ ├── build.yml │ ├── dockerhub.yml │ └── release.yml ├── .gitignore ├── .gitmodules ├── .kamal ├── hooks │ ├── docker-setup.sample │ ├── post-deploy.sample │ ├── post-proxy-reboot.sample │ ├── pre-build.sample │ ├── pre-connect.sample │ ├── pre-deploy.sample │ └── pre-proxy-reboot.sample └── secrets ├── .vscode ├── launch.json └── tasks.json ├── AiServer.ServiceInterface ├── AdminServices.cs ├── AiProviderFactory.cs ├── AiServer.ServiceInterface.csproj ├── AnthropicAiProvider.cs ├── AppConfig.cs ├── AppData.cs ├── AppDb │ ├── ChangeDiffusionProviderStatusCommand.cs │ ├── ChangeProviderStatusCommand.cs │ ├── CompleteOllamaGenerateCommand.cs │ └── CompleteOpenAiChatCommand.cs ├── AppExtensions.cs ├── AudioServices.cs ├── ChatSummaryServices.cs ├── Comfy │ ├── ComfyClient.Parsing.cs │ ├── ComfyClient.cs │ ├── ComfyExtensions.cs │ └── ComfyWebSocketClient.cs ├── ComfyApiServices.cs ├── ComfyConverters.cs ├── ComfyGateway.cs ├── ComfyMetadata.cs ├── ComfyServices.cs ├── ComfyWorkflowParser.cs ├── DtoExtensions.cs ├── Generation │ ├── ComfyProvider.cs │ ├── DalleApiClient.cs │ ├── FileServices.cs │ ├── OpenAiProvider.cs │ ├── ReplicateApiClient.cs │ └── ReplicateProvider.cs ├── GenerationServices.cs ├── GoogleAiProvider.cs ├── ImageServices.Generation.cs ├── ImageServices.Media.cs ├── Jobs │ ├── CreateGenerationCommand.cs │ ├── CreateOllamaGenerationCommand.cs │ ├── CreateOpenAiChatCommand.cs │ ├── NotifyGenerationResponseCommand.cs │ └── NotifyOpenAiChatResponseCommand.cs ├── MediaProviderServices.cs ├── MediaTransform │ ├── CreateMediaTransformCommand.cs │ └── NotifyMediaTransformCommand.cs ├── MediaTransformProviderServices.cs ├── MultipartFormDataExtensions.cs ├── MyServices.cs ├── OllamaServices.cs ├── OpenAiChatServices.cs ├── OpenAiProvider.cs ├── PromptServices.cs ├── QueueOperationServices.cs ├── Recurring │ ├── CheckGenerationProviders.cs │ └── CheckOpenAiProviders.cs ├── SpeechServices.cs ├── Tags.cs └── VideoServices.cs ├── AiServer.ServiceModel ├── AiProvider.cs ├── AiServer.ServiceModel.csproj ├── ApiAdmin.cs ├── ApiKeys.cs ├── Comfy.cs ├── ComfyTypes.cs ├── Databases.cs ├── GenerationAdmin.cs ├── GenerationProviders.cs ├── Generations.cs ├── GetSummaryStats.cs ├── Hello.cs ├── Icons.cs ├── MediaTransformProviders.cs ├── MediaTransforms.cs ├── Ollama.cs ├── OpenAiChat.cs ├── OpenAiChatServer.cs ├── Prompts.cs ├── QueueGenerations.cs ├── QueueMediaTransforms.cs ├── QueueOperations.cs ├── Tags.cs ├── Tasks.cs └── Types │ ├── ComfyApi.cs │ ├── ComfyTypes.cs │ ├── Generations.cs │ ├── OpenAiChat.cs │ └── README.md ├── AiServer.Tests ├── AiServer.Tests.csproj ├── AudioIntegrationTests.cs ├── BlazorDiffusionTasks.cs ├── ComfyAdminTasks.cs ├── ComfyInstallerTests.cs ├── ComfyMergeWorkflowTests.cs ├── ComfyUITests.cs ├── ComfyWorkflowExecuteTests.cs ├── ComfyWorkflowParseTests.cs ├── ConnectivityTests.cs ├── DiffusionProviderTests.cs ├── GoogleTests.cs ├── ImageServiceTests.cs ├── ImageToImageTests.cs ├── ImageToTextTests.cs ├── ImageUpscaleTests.cs ├── IntegrationTest.cs ├── MigrationTests.cs ├── OllamaApiTests.cs ├── OpenAiChatTaskTests.cs ├── OpenAiChatTests.cs ├── OpenAiProviderTests.cs ├── PingTests.cs ├── PvqApiTests.cs ├── QueueAudioIntegrationTests.cs ├── QueueImageServiceTests.cs ├── QueueImageToImageTests.cs ├── QueueImageToTextTests.cs ├── QueueImageUpscaleTests.cs ├── QueueImageWithMaskTests.cs ├── QueueSpeechToTextTests.cs ├── QueueTextToImageTests.cs ├── QueueTextToSpeechTests.cs ├── QueueVideoIntegrationTests.cs ├── SpeechToTextTests.cs ├── TestUtils.cs ├── TextToImageTests.cs ├── Types │ └── Post.cs ├── UnitTest.cs ├── VideoIntegrationTests.cs ├── appsettings.json ├── files │ ├── comfyui_upload_test.png │ ├── comfyui_upload_test_mask.png │ ├── object_info.json │ ├── speech_to_text_test.wav │ ├── test_audio.mp3 │ ├── test_audio.wav │ ├── test_image.jpg │ ├── test_video.mp4 │ ├── test_video.webm │ ├── top1000questions.txt │ └── watermark_image.png └── workflows │ ├── audio-to-text │ ├── faster_whisper_suttitle.json │ └── transcribe-audio-whisper.json │ ├── image-to-image │ └── sd1.5_pruned_emaonly.json │ ├── image-to-text │ └── florence2.json │ ├── image-to-video │ └── wan.json │ ├── image_to_image.json │ ├── image_to_image_upscale.json │ ├── image_to_image_with_mask.json │ ├── image_to_text.json │ ├── prompts │ ├── basic-prompt-workflow.json │ ├── basic-prompt.json │ ├── florence2.json │ └── wan.json │ ├── results │ ├── sd1.5_pruned_emaonly.output.json │ ├── sd3.5_fp8.output.json │ ├── sdxl_lightning_4step.output.json │ ├── transcribe-audio-whisper.output.json │ └── transcribe-video-whisper.output.json │ ├── speech_to_text.json │ ├── text-to-audio │ └── stable_audio.json │ ├── text-to-image │ ├── basic.json │ ├── dreamshaperXL.json │ ├── epiCRealismXL.json │ ├── flux1-schnell.json │ ├── hidream_i1_dev_fp8.json │ ├── hidream_i1_fast_fp8.json │ ├── jibMixRealisticXL.json │ ├── juggernautXL.json │ ├── realvisxl.json │ ├── sd3.5_large.json │ ├── sd3.5_large_fp8_scaled.json │ ├── sd3.5_large_turbo.json │ └── sdxl_lightning_4step.json │ ├── text_to_audio.json │ ├── text_to_image.json │ ├── text_to_speech.json │ ├── video-to-text │ └── transcribe-video-whisper.json │ └── workflow_simple_generation.json ├── AiServer.sln ├── AiServer ├── AiServer.csproj ├── Configure.AppHost.cs ├── Configure.Auth.cs ├── Configure.AutoQuery.cs ├── Configure.BackgroundJobs.cs ├── Configure.Db.Migrations.cs ├── Configure.Db.cs ├── Configure.HealthChecks.cs ├── Configure.Profiling.cs ├── Configure.RequestLogs.cs ├── Migrations │ ├── Migration1001.cs │ └── Migration1002.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── appsettings.Development.json ├── appsettings.json ├── package-lock.json ├── package.json ├── postinstall.js ├── seed │ ├── ai-providers.json │ └── media-providers.json ├── tailwind.config.js ├── tailwind.input.css ├── workflows │ ├── image_to_image.json │ ├── image_to_image_upscale.json │ ├── image_to_image_with_mask.json │ ├── image_to_text.json │ ├── speech_to_text.json │ ├── text-to-image │ │ ├── flux1.json │ │ ├── hidream.json │ │ ├── sd3.5-fp8-scaled.json │ │ ├── sd35-large.json │ │ ├── sd35-turbo.json │ │ ├── sdxl-lightning.json │ │ └── sdxl.json │ ├── text_to_audio.json │ ├── text_to_image.json │ └── text_to_speech.json └── wwwroot │ ├── Ui.mjs │ ├── admin │ └── index.html │ ├── css │ ├── app.css │ ├── asciinema-player.css │ ├── highlight.css │ ├── lite-yt-embed.css │ └── typography.css │ ├── img │ ├── langs │ │ ├── csharp.svg │ │ ├── dart.svg │ │ ├── fsharp.svg │ │ ├── java.svg │ │ ├── javascript.svg │ │ ├── kotlin.svg │ │ ├── mjs.svg │ │ ├── node.svg │ │ ├── php.svg │ │ ├── python.svg │ │ ├── swift.svg │ │ ├── typescript.svg │ │ └── vbnet.svg │ ├── logo.svg │ ├── models │ │ ├── aws.svg │ │ ├── chatgpt.svg │ │ ├── claude-3-5-sonnet.svg │ │ ├── claude-3-haiku.svg │ │ ├── claude-3-opus.svg │ │ ├── claude-3-sonnet.svg │ │ ├── codellama.svg │ │ ├── codestral.svg │ │ ├── command-r-plus.svg │ │ ├── command-r.svg │ │ ├── dbrx.svg │ │ ├── deepseek-3.jpg │ │ ├── deepseek-coder-v2.jpg │ │ ├── deepseek-coder.jpg │ │ ├── deepseek.jpg │ │ ├── dolphin.png │ │ ├── gemini-pro.svg │ │ ├── gemini.svg │ │ ├── gemma.svg │ │ ├── gpt-3.5.svg │ │ ├── gpt-4.svg │ │ ├── llama.svg │ │ ├── mathstral.svg │ │ ├── mistral-nemo.svg │ │ ├── mistral.svg │ │ ├── mixtral.jpg │ │ ├── phi.svg │ │ ├── phi3.svg │ │ ├── phi4.svg │ │ ├── qwen.svg │ │ ├── starcoder.png │ │ ├── thudm.webp │ │ ├── wizardlm.png │ │ └── yi.svg │ ├── overview.svg │ ├── providers │ │ ├── anthropic.svg │ │ ├── aws.svg │ │ ├── comfyui.png │ │ ├── custom.svg │ │ ├── google-cloud.svg │ │ ├── groq.svg │ │ ├── mistral-ai.svg │ │ ├── ollama.svg │ │ ├── openai.svg │ │ ├── openrouter.svg │ │ └── replicate.svg │ └── uis │ │ ├── Chat.webp │ │ ├── ImageToImage.webp │ │ ├── ImageToText.webp │ │ ├── ImageUpscale.webp │ │ ├── SpeechToText.webp │ │ ├── TextToImage.webp │ │ ├── TextToSpeech.webp │ │ └── admin │ │ ├── ai-models.webp │ │ ├── ai-providers-new-ollama.webp │ │ ├── ai-providers.webp │ │ ├── ai-types.webp │ │ ├── analytics.webp │ │ ├── api-keys-edit.webp │ │ ├── api-keys.webp │ │ ├── background-jobs-live.webp │ │ ├── background-jobs-queue.webp │ │ ├── background-jobs.webp │ │ ├── dashboard.webp │ │ ├── logging.webp │ │ ├── media-providers-comfyui.webp │ │ ├── media-providers-replicate.webp │ │ ├── media-providers.webp │ │ └── media-types.webp │ ├── index.html │ ├── lib │ ├── data │ │ ├── ai-models.json │ │ ├── ai-types.json │ │ ├── image-to-image-models.json │ │ ├── media-models.json │ │ ├── media-types.json │ │ ├── object_info.json │ │ ├── prompts.json │ │ ├── prompts.md │ │ ├── tts-voices.json │ │ └── workflows │ │ │ ├── audio-to-text │ │ │ └── whisper │ │ │ │ └── transcribe-audio.json │ │ │ ├── image-to-image │ │ │ └── sd-1.5 │ │ │ │ └── simple.json │ │ │ ├── image-to-text │ │ │ └── florence2 │ │ │ │ └── describe-image.json │ │ │ ├── image-to-video │ │ │ └── wan │ │ │ │ └── generate-video.json │ │ │ ├── text-to-audio │ │ │ └── stable-audio │ │ │ │ └── generate-audio.json │ │ │ ├── text-to-image │ │ │ ├── flux │ │ │ │ └── flux1-schnell.json │ │ │ ├── hidream │ │ │ │ ├── hidream_i1_dev_fp8.json │ │ │ │ └── hidream_i1_fast_fp8.json │ │ │ ├── sd-1.5 │ │ │ │ └── basic.json │ │ │ ├── sd-3.5-large │ │ │ │ ├── sd3.5_large.json │ │ │ │ ├── sd3.5_large_fp8_scaled.json │ │ │ │ └── sd3.5_large_turbo.json │ │ │ ├── sdxl-base │ │ │ │ ├── sdxl-base-with-refiner-sytan.json │ │ │ │ └── sdxl-base-with-refiner.json │ │ │ ├── sdxl-lightning │ │ │ │ └── 4step.json │ │ │ ├── sdxl-turbo │ │ │ │ └── sdxl_turbo-v1.json │ │ │ └── sdxl │ │ │ │ ├── CheyenneXL.json │ │ │ │ ├── dreamshaperXL.json │ │ │ │ ├── epiCRealismXL.json │ │ │ │ ├── jibMixRealisticXL.json │ │ │ │ ├── juggernautXL.json │ │ │ │ └── realvisxl.json │ │ │ └── video-to-text │ │ │ └── whisper │ │ │ └── transcribe-video.json │ ├── js │ │ ├── asciinema-player.js │ │ ├── highlight.js │ │ ├── highlight.min.js │ │ └── lite-yt-embed.js │ └── mjs │ │ ├── app.mjs │ │ ├── chart.js │ │ ├── charts.mjs │ │ ├── color.js │ │ ├── dart.min.js │ │ ├── fsharp.min.js │ │ ├── highlight.mjs │ │ ├── marked.mjs │ │ ├── servicestack-client.min.mjs │ │ ├── servicestack-client.mjs │ │ ├── servicestack-vue.min.mjs │ │ ├── servicestack-vue.mjs │ │ ├── vue.min.mjs │ │ └── vue.mjs │ ├── mjs │ ├── components │ │ ├── AiProviders.mjs │ │ ├── Artifacts.mjs │ │ ├── AsciiCinema.mjs │ │ ├── AudioPlayer.mjs │ │ ├── Chat.mjs │ │ ├── CommandPalette.mjs │ │ ├── ConvertImage.mjs │ │ ├── ConvertVideo.mjs │ │ ├── Features.mjs │ │ ├── FileUpload.mjs │ │ ├── ImageToImage.mjs │ │ ├── ImageToText.mjs │ │ ├── ImageUpscale.mjs │ │ ├── ListenButton.mjs │ │ ├── MediaProviders.mjs │ │ ├── OpenAiChatLangs.mjs │ │ ├── PromptGenerator.mjs │ │ ├── ShellCommand.mjs │ │ ├── SignIn.mjs │ │ ├── SignInForm.mjs │ │ ├── SpeechToText.mjs │ │ ├── TextToImage.mjs │ │ ├── TextToSpeech.mjs │ │ ├── Transform.mjs │ │ └── UiHome.mjs │ ├── dom.mjs │ ├── dtos.mjs │ ├── markdown.mjs │ └── utils.mjs │ └── tailwind │ ├── Analytics.mjs │ ├── ApiKeyDialog.mjs │ ├── ApiKeys.mjs │ ├── BackgroundJobs.mjs │ ├── CopyIcon.mjs │ ├── LogLinks.mjs │ ├── Logging.mjs │ ├── ManageUserApiKeys.mjs │ ├── README.txt │ ├── metadata.mjs │ └── sync.sh ├── NuGet.Config ├── README.md ├── config ├── deploy.yml └── litestream.yml ├── docker-compose.yml ├── example.env ├── install.sh ├── license.txt └── public └── text2img └── sdxl_lightning_samples.jpg /.deploy/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | app: 3 | image: ghcr.io/${IMAGE_REPO}:${RELEASE_VERSION} 4 | restart: always 5 | ports: 6 | - "8080" 7 | container_name: ${APP_NAME}_app 8 | env_file: ".env" 9 | environment: 10 | VIRTUAL_HOST: ${HOST_DOMAIN},ai-server-cdn.diffusion.works 11 | VIRTUAL_PORT: 8080 12 | LETSENCRYPT_HOST: ${HOST_DOMAIN},ai-server-cdn.diffusion.works 13 | LETSENCRYPT_EMAIL: ${LETSENCRYPT_EMAIL} 14 | CIVIT_AI_API_KEY: ${CIVIT_AI_API_KEY} 15 | REPLICATE_API_KEY: ${REPLICATE_API_KEY} 16 | HTTPS_METHOD: noredirect # Disable HTTPS redirect since Cloudflare infinite loop redirects when default 17 | volumes: 18 | - ./App_Data:/app/App_Data 19 | 20 | app-migration: 21 | image: ghcr.io/${IMAGE_REPO}:${RELEASE_VERSION} 22 | restart: "no" 23 | container_name: ${APP_NAME}_app_migration 24 | env_file: ".env" 25 | # API Providers populated based on available Environment Variables 26 | environment: 27 | GOOGLE_API_KEY: ${GOOGLE_API_KEY} 28 | GROQ_API_KEY: ${GROQ_API_KEY} 29 | MISTRAL_API_KEY: ${MISTRAL_API_KEY} 30 | OPENAI_API_KEY: ${OPENAI_API_KEY} 31 | OPENROUTER_API_KEY: ${OPENROUTER_API_KEY} 32 | profiles: 33 | - migration 34 | command: --AppTasks=migrate 35 | volumes: 36 | - ./App_Data:/app/App_Data 37 | 38 | networks: 39 | default: 40 | external: true 41 | name: nginx 42 | -------------------------------------------------------------------------------- /.deploy/nginx-proxy-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | services: 4 | nginx-proxy: 5 | image: nginxproxy/nginx-proxy 6 | container_name: nginx-proxy 7 | restart: always 8 | ports: 9 | - "80:80" 10 | - "443:443" 11 | volumes: 12 | - conf:/etc/nginx/conf.d 13 | - vhost:/etc/nginx/vhost.d 14 | - html:/usr/share/nginx/html 15 | - dhparam:/etc/nginx/dhparam 16 | - certs:/etc/nginx/certs:ro 17 | - /var/run/docker.sock:/tmp/docker.sock:ro 18 | labels: 19 | - "com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy" 20 | 21 | letsencrypt: 22 | image: nginxproxy/acme-companion:2.2 23 | container_name: nginx-proxy-le 24 | restart: always 25 | depends_on: 26 | - "nginx-proxy" 27 | environment: 28 | - DEFAULT_EMAIL=you@example.com 29 | volumes: 30 | - certs:/etc/nginx/certs:rw 31 | - acme:/etc/acme.sh 32 | - vhost:/etc/nginx/vhost.d 33 | - html:/usr/share/nginx/html 34 | - /var/run/docker.sock:/var/run/docker.sock:ro 35 | 36 | networks: 37 | default: 38 | name: nginx 39 | 40 | volumes: 41 | conf: 42 | vhost: 43 | html: 44 | dhparam: 45 | certs: 46 | acme: -------------------------------------------------------------------------------- /.github/workflows/build-container.yml: -------------------------------------------------------------------------------- 1 | name: Build Container 2 | permissions: 3 | packages: write 4 | contents: write 5 | on: 6 | workflow_run: 7 | workflows: ["Build"] 8 | types: 9 | - completed 10 | branches: 11 | - main 12 | - master 13 | workflow_dispatch: 14 | 15 | env: 16 | DOCKER_BUILDKIT: 1 17 | KAMAL_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} 18 | KAMAL_REGISTRY_USERNAME: ${{ github.actor }} 19 | 20 | jobs: 21 | build-container: 22 | runs-on: ubuntu-latest 23 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 24 | steps: 25 | - name: Checkout code 26 | uses: actions/checkout@v3 27 | 28 | - name: Set up environment variables 29 | run: | 30 | echo "image_repository_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV 31 | echo "repository_name=$(echo ${{ github.repository }} | cut -d '/' -f 2)" >> $GITHUB_ENV 32 | echo "repository_name_lower=$(echo ${{ github.repository }} | cut -d '/' -f 2 | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV 33 | echo "org_name=$(echo ${{ github.repository }} | cut -d '/' -f 1)" >> $GITHUB_ENV 34 | if [ -n "${{ secrets.APPSETTINGS_PATCH }}" ]; then 35 | echo "HAS_APPSETTINGS_PATCH=true" >> $GITHUB_ENV 36 | else 37 | echo "HAS_APPSETTINGS_PATCH=false" >> $GITHUB_ENV 38 | fi 39 | if [ -n "${{ secrets.KAMAL_DEPLOY_IP }}" ]; then 40 | echo "HAS_DEPLOY_ACTION=true" >> $GITHUB_ENV 41 | else 42 | echo "HAS_DEPLOY_ACTION=false" >> $GITHUB_ENV 43 | fi 44 | 45 | 46 | - name: Setup Node.js 47 | if: steps.check_client.outputs.client_exists == 'true' 48 | uses: actions/setup-node@v3 49 | with: 50 | node-version: 22 51 | 52 | - name: Install x tool 53 | run: dotnet tool install -g x 54 | 55 | - name: Apply Production AppSettings 56 | if: env.HAS_APPSETTINGS_PATCH == 'true' 57 | working-directory: ./AiServer 58 | run: | 59 | cat <> appsettings.json.patch 60 | ${{ secrets.APPSETTINGS_PATCH }} 61 | EOF 62 | x patch appsettings.json.patch 63 | 64 | - name: Login to GitHub Container Registry 65 | uses: docker/login-action@v3 66 | with: 67 | registry: ghcr.io 68 | username: ${{ env.KAMAL_REGISTRY_USERNAME }} 69 | password: ${{ env.KAMAL_REGISTRY_PASSWORD }} 70 | 71 | - name: Setup .NET 72 | uses: actions/setup-dotnet@v3 73 | with: 74 | dotnet-version: '8.0' 75 | 76 | - name: Build and push Docker image 77 | run: | 78 | dotnet publish --os linux --arch x64 -c Release -p:ContainerRepository=${{ env.image_repository_name }} -p:ContainerRegistry=ghcr.io -p:ContainerImageTags=latest -p:ContainerPort=80 79 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: {} 5 | push: 6 | branches: 7 | - '**' # matches every branch 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: checkout 14 | uses: actions/checkout@v3 15 | 16 | - name: Setup dotnet 17 | uses: actions/setup-dotnet@v3 18 | with: 19 | dotnet-version: '8.0' 20 | 21 | - name: Extract SS license 22 | run: | 23 | SS_LICENSE=`echo '${{ secrets.APPSETTINGS_PATCH }}' | jq -r .[0].value.license` 24 | echo "::add-mask::$SS_LICENSE" 25 | echo "SERVICESTACK_LICENSE=${SS_LICENSE}" >> $GITHUB_ENV 26 | # Add Comfy Agent API key 27 | echo "COMFY_API_KEY=${{ secrets.COMFY_API_KEY }}" >> $GITHUB_ENV 28 | # Add Open AI Provider Keys 29 | echo "GOOGLE_API_KEY=${{ secrets.GOOGLE_API_KEY }}" >> $GITHUB_ENV 30 | echo "GROQ_API_KEY=${{ secrets.GROQ_API_KEY }}" >> $GITHUB_ENV 31 | echo "MISTRAL_API_KEY=${{ secrets.MISTRAL_API_KEY }}" >> $GITHUB_ENV 32 | echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}" >> $GITHUB_ENV 33 | echo "OPENROUTER_API_KEY=${{ secrets.OPENROUTER_API_KEY }}" >> $GITHUB_ENV 34 | 35 | - name: build 36 | run: dotnet build 37 | working-directory: . 38 | 39 | - name: test 40 | run: | 41 | dotnet test 42 | if [ $? -eq 0 ]; then 43 | echo TESTS PASSED 44 | else 45 | echo TESTS FAILED 46 | exit 1 47 | fi 48 | working-directory: ./AiServer.Tests 49 | 50 | -------------------------------------------------------------------------------- /.github/workflows/dockerhub.yml: -------------------------------------------------------------------------------- 1 | name: Build and Push .NET 8 Docker Image to DockerHub 2 | 3 | on: 4 | push: 5 | branches: [main, master] 6 | release: 7 | types: [published] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build_and_push: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v3 16 | 17 | - name: Setup .NET 18 | uses: actions/setup-dotnet@v3 19 | with: 20 | dotnet-version: '8.0.x' 21 | 22 | - name: Login to DockerHub 23 | uses: docker/login-action@v2 24 | with: 25 | username: servicestack 26 | password: ${{ secrets.DOCKERHUB_TOKEN }} 27 | 28 | - name: Build and push Docker image 29 | env: 30 | DOCKER_REPO: servicestack/${{ github.event.repository.name }} 31 | run: | 32 | # Determine version tag 33 | if [[ "${{ github.ref }}" == "refs/tags/"* ]]; then 34 | VERSION=${GITHUB_REF#refs/tags/} 35 | else 36 | VERSION=${{ github.sha }} 37 | fi 38 | 39 | # Build and push using dotnet publish 40 | dotnet publish --os linux --arch x64 -c Release \ 41 | -p:PublishProfile=DefaultContainer \ 42 | -p:ContainerRepository=$DOCKER_REPO \ 43 | -p:ContainerImageTags="latest" 44 | 45 | # Push the image 46 | docker push $DOCKER_REPO:latest -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "agent-comfy"] 2 | path = agent-comfy 3 | url = https://github.com/ServiceStack/agent-comfy.git 4 | -------------------------------------------------------------------------------- /.kamal/hooks/docker-setup.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Docker set up on $KAMAL_HOSTS..." 4 | -------------------------------------------------------------------------------- /.kamal/hooks/post-deploy.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # A sample post-deploy hook 4 | # 5 | # These environment variables are available: 6 | # KAMAL_RECORDED_AT 7 | # KAMAL_PERFORMER 8 | # KAMAL_VERSION 9 | # KAMAL_HOSTS 10 | # KAMAL_ROLE (if set) 11 | # KAMAL_DESTINATION (if set) 12 | # KAMAL_RUNTIME 13 | 14 | echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds" 15 | -------------------------------------------------------------------------------- /.kamal/hooks/post-proxy-reboot.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Rebooted kamal-proxy on $KAMAL_HOSTS" 4 | -------------------------------------------------------------------------------- /.kamal/hooks/pre-build.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # A sample pre-build hook 4 | # 5 | # Checks: 6 | # 1. We have a clean checkout 7 | # 2. A remote is configured 8 | # 3. The branch has been pushed to the remote 9 | # 4. The version we are deploying matches the remote 10 | # 11 | # These environment variables are available: 12 | # KAMAL_RECORDED_AT 13 | # KAMAL_PERFORMER 14 | # KAMAL_VERSION 15 | # KAMAL_HOSTS 16 | # KAMAL_ROLE (if set) 17 | # KAMAL_DESTINATION (if set) 18 | 19 | if [ -n "$(git status --porcelain)" ]; then 20 | echo "Git checkout is not clean, aborting..." >&2 21 | git status --porcelain >&2 22 | exit 1 23 | fi 24 | 25 | first_remote=$(git remote) 26 | 27 | if [ -z "$first_remote" ]; then 28 | echo "No git remote set, aborting..." >&2 29 | exit 1 30 | fi 31 | 32 | current_branch=$(git branch --show-current) 33 | 34 | if [ -z "$current_branch" ]; then 35 | echo "Not on a git branch, aborting..." >&2 36 | exit 1 37 | fi 38 | 39 | remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1) 40 | 41 | if [ -z "$remote_head" ]; then 42 | echo "Branch not pushed to remote, aborting..." >&2 43 | exit 1 44 | fi 45 | 46 | if [ "$KAMAL_VERSION" != "$remote_head" ]; then 47 | echo "Version ($KAMAL_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2 48 | exit 1 49 | fi 50 | 51 | exit 0 52 | -------------------------------------------------------------------------------- /.kamal/hooks/pre-connect.sample: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # A sample pre-connect check 4 | # 5 | # Warms DNS before connecting to hosts in parallel 6 | # 7 | # These environment variables are available: 8 | # KAMAL_RECORDED_AT 9 | # KAMAL_PERFORMER 10 | # KAMAL_VERSION 11 | # KAMAL_HOSTS 12 | # KAMAL_ROLE (if set) 13 | # KAMAL_DESTINATION (if set) 14 | # KAMAL_RUNTIME 15 | 16 | hosts = ENV["KAMAL_HOSTS"].split(",") 17 | results = nil 18 | max = 3 19 | 20 | elapsed = Benchmark.realtime do 21 | results = hosts.map do |host| 22 | Thread.new do 23 | tries = 1 24 | 25 | begin 26 | Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME) 27 | rescue SocketError 28 | if tries < max 29 | puts "Retrying DNS warmup: #{host}" 30 | tries += 1 31 | sleep rand 32 | retry 33 | else 34 | puts "DNS warmup failed: #{host}" 35 | host 36 | end 37 | end 38 | 39 | tries 40 | end 41 | end.map(&:value) 42 | end 43 | 44 | retries = results.sum - hosts.size 45 | nopes = results.count { |r| r == max } 46 | 47 | puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ] 48 | -------------------------------------------------------------------------------- /.kamal/hooks/pre-deploy.sample: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # A sample pre-deploy hook 4 | # 5 | # Checks the Github status of the build, waiting for a pending build to complete for up to 720 seconds. 6 | # 7 | # Fails unless the combined status is "success" 8 | # 9 | # These environment variables are available: 10 | # KAMAL_RECORDED_AT 11 | # KAMAL_PERFORMER 12 | # KAMAL_VERSION 13 | # KAMAL_HOSTS 14 | # KAMAL_COMMAND 15 | # KAMAL_SUBCOMMAND 16 | # KAMAL_ROLE (if set) 17 | # KAMAL_DESTINATION (if set) 18 | 19 | # Only check the build status for production deployments 20 | if ENV["KAMAL_COMMAND"] == "rollback" || ENV["KAMAL_DESTINATION"] != "production" 21 | exit 0 22 | end 23 | 24 | require "bundler/inline" 25 | 26 | # true = install gems so this is fast on repeat invocations 27 | gemfile(true, quiet: true) do 28 | source "https://rubygems.org" 29 | 30 | gem "octokit" 31 | gem "faraday-retry" 32 | end 33 | 34 | MAX_ATTEMPTS = 72 35 | ATTEMPTS_GAP = 10 36 | 37 | def exit_with_error(message) 38 | $stderr.puts message 39 | exit 1 40 | end 41 | 42 | class GithubStatusChecks 43 | attr_reader :remote_url, :git_sha, :github_client, :combined_status 44 | 45 | def initialize 46 | @remote_url = `git config --get remote.origin.url`.strip.delete_prefix("https://github.com/") 47 | @git_sha = `git rev-parse HEAD`.strip 48 | @github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"]) 49 | refresh! 50 | end 51 | 52 | def refresh! 53 | @combined_status = github_client.combined_status(remote_url, git_sha) 54 | end 55 | 56 | def state 57 | combined_status[:state] 58 | end 59 | 60 | def first_status_url 61 | first_status = combined_status[:statuses].find { |status| status[:state] == state } 62 | first_status && first_status[:target_url] 63 | end 64 | 65 | def complete_count 66 | combined_status[:statuses].count { |status| status[:state] != "pending"} 67 | end 68 | 69 | def total_count 70 | combined_status[:statuses].count 71 | end 72 | 73 | def current_status 74 | if total_count > 0 75 | "Completed #{complete_count}/#{total_count} checks, see #{first_status_url} ..." 76 | else 77 | "Build not started..." 78 | end 79 | end 80 | end 81 | 82 | 83 | $stdout.sync = true 84 | 85 | puts "Checking build status..." 86 | attempts = 0 87 | checks = GithubStatusChecks.new 88 | 89 | begin 90 | loop do 91 | case checks.state 92 | when "success" 93 | puts "Checks passed, see #{checks.first_status_url}" 94 | exit 0 95 | when "failure" 96 | exit_with_error "Checks failed, see #{checks.first_status_url}" 97 | when "pending" 98 | attempts += 1 99 | end 100 | 101 | exit_with_error "Checks are still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds" if attempts == MAX_ATTEMPTS 102 | 103 | puts checks.current_status 104 | sleep(ATTEMPTS_GAP) 105 | checks.refresh! 106 | end 107 | rescue Octokit::NotFound 108 | exit_with_error "Build status could not be found" 109 | end 110 | -------------------------------------------------------------------------------- /.kamal/hooks/pre-proxy-reboot.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Rebooting kamal-proxy on $KAMAL_HOSTS..." 4 | -------------------------------------------------------------------------------- /.kamal/secrets: -------------------------------------------------------------------------------- 1 | # Secrets defined here are available for reference under registry/password, env/secret, builder/secrets, 2 | # and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either 3 | # password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git. 4 | 5 | # Option 1: Read secrets from the environment 6 | KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD 7 | KAMAL_REGISTRY_USERNAME=$KAMAL_REGISTRY_USERNAME 8 | GOOGLE_API_KEY=$GOOGLE_API_KEY 9 | GROQ_API_KEY=$GROQ_API_KEY 10 | MISTRAL_API_KEY=$MISTRAL_API_KEY 11 | OPENAI_API_KEY=$OPENAI_API_KEY 12 | OPENROUTER_API_KEY=$OPENROUTER_API_KEY 13 | R2_ACCESS_KEY_ID=$R2_ACCESS_KEY_ID 14 | R2_SECRET_ACCESS_KEY=$R2_SECRET_ACCESS_KEY 15 | 16 | # Option 2: Read secrets via a command 17 | # RAILS_MASTER_KEY=$(cat config/master.key) 18 | 19 | # Option 3: Read secrets via kamal secrets helpers 20 | # These will handle logging in and fetching the secrets in as few calls as possible 21 | # There are adapters for 1Password, LastPass + Bitwarden 22 | # 23 | # SECRETS=$(kamal secrets fetch --adapter 1password --account my-account --from MyVault/MyItem KAMAL_REGISTRY_PASSWORD RAILS_MASTER_KEY) 24 | # KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD $SECRETS) 25 | # RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY $SECRETS) 26 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (web)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | "program": "${workspaceFolder}/AiServer/bin/Debug/net6.0/AiServer.dll", 13 | "args": [], 14 | "cwd": "${workspaceFolder}/AiServer", 15 | "stopAtEntry": false, 16 | "serverReadyAction": { 17 | "action": "openExternally", 18 | "pattern": "\\bNow listening on:\\s+(https?://\\S+)" 19 | }, 20 | "env": { 21 | "ASPNETCORE_ENVIRONMENT": "Development" 22 | }, 23 | "sourceFileMap": { 24 | "/Views": "${workspaceFolder}/Views" 25 | } 26 | }, 27 | { 28 | "name": ".NET Core Attach", 29 | "type": "coreclr", 30 | "request": "attach" 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "build", 8 | "command": "dotnet build", 9 | "type": "shell", 10 | "group": "build", 11 | "presentation": { 12 | "reveal": "silent" 13 | }, 14 | "problemMatcher": "$msCompile" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /AiServer.ServiceInterface/AdminServices.cs: -------------------------------------------------------------------------------- 1 | using AiServer.ServiceModel; 2 | using ServiceStack; 3 | using ServiceStack.Jobs; 4 | using ServiceStack.OrmLite; 5 | 6 | namespace AiServer.ServiceInterface; 7 | 8 | public class AdminServices(IBackgroundJobs jobs) : Service 9 | { 10 | public object Any(AdminData request) 11 | { 12 | var tables = new (string Label, Type Type)[] 13 | { 14 | (nameof(AiProvider), typeof(AiProvider)), 15 | }; 16 | var dialect = Db.GetDialectProvider(); 17 | var totalSql = tables.Map(x => $"SELECT '{x.Label}', COUNT(*) FROM {dialect.GetQuotedTableName(x.Type.GetModelMetadata())}") 18 | .Join(" UNION "); 19 | var results = Db.Dictionary(totalSql); 20 | var pageStats = tables.Map(x => new PageStats 21 | { 22 | Label = x.Label, 23 | Total = results[x.Label], 24 | }); 25 | 26 | var jobTables = new (string Label, Type Type)[] 27 | { 28 | (nameof(JobSummary), typeof(JobSummary)), 29 | }; 30 | using var dbJobs = jobs.OpenDb(); 31 | totalSql = jobTables.Map(x => $"SELECT '{x.Label}', COUNT(*) FROM {dialect.GetQuotedTableName(x.Type.GetModelMetadata())}") 32 | .Join(" UNION "); 33 | results = dbJobs.Dictionary(totalSql); 34 | pageStats.AddRange(jobTables.Map(x => new PageStats 35 | { 36 | Label = x.Label, 37 | Total = results[x.Label], 38 | })); 39 | 40 | return new AdminDataResponse { 41 | PageStats = pageStats 42 | }; 43 | } 44 | } -------------------------------------------------------------------------------- /AiServer.ServiceInterface/AiProviderFactory.cs: -------------------------------------------------------------------------------- 1 | using AiServer.ServiceModel; 2 | 3 | namespace AiServer.ServiceInterface; 4 | 5 | public record OpenAiChatResult(OpenAiChatResponse Response, int DurationMs); 6 | 7 | public interface IOpenAiProvider 8 | { 9 | Task IsOnlineAsync(AiProvider provider, CancellationToken token = default); 10 | 11 | Task ChatAsync(AiProvider provider, OpenAiChat request, CancellationToken token = default); 12 | } 13 | 14 | public record OllamaGenerationResult(OllamaGenerateResponse Response, int DurationMs); 15 | public interface IOllamaAiProvider 16 | { 17 | Task GenerateAsync(AiProvider provider, OllamaGenerate request, CancellationToken token = default); 18 | } 19 | 20 | public class AiProviderFactory( 21 | OpenAiProvider openAiProvider, 22 | OllamaAiProvider ollamaAiProvider, 23 | GoogleAiProvider googleProvider, 24 | AnthropicAiProvider anthropicAiProvider, 25 | CustomOpenAiProvider customOpenAiProvider) 26 | { 27 | public IOpenAiProvider GetOpenAiProvider(AiProviderType aiProviderType=AiProviderType.OpenAiProvider) 28 | { 29 | return aiProviderType switch 30 | { 31 | AiProviderType.OllamaAiProvider => ollamaAiProvider, 32 | AiProviderType.GoogleAiProvider => googleProvider, 33 | AiProviderType.AnthropicAiProvider => anthropicAiProvider, 34 | AiProviderType.CustomOpenAiProvider => customOpenAiProvider, 35 | _ => openAiProvider 36 | }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /AiServer.ServiceInterface/AiServer.ServiceInterface.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /AiServer.ServiceInterface/AppConfig.cs: -------------------------------------------------------------------------------- 1 | using AiServer.ServiceModel.Types; 2 | using ServiceStack; 3 | 4 | namespace AiServer.ServiceInterface; 5 | 6 | public class AppConfig 7 | { 8 | public static AppConfig Instance { get; } = new(); 9 | public string? AuthSecret { get; set; } 10 | public string? ArtifactsPath { get; set; } 11 | public string? FilesPath { get; set; } 12 | 13 | public ComfyApiModel? DefaultModel { get; set; } 14 | public ComfyApiModelSettings? DefaultModelSettings { get; set; } 15 | 16 | public string? CivitAiApiKey { get; set; } 17 | public string? ReplicateApiKey { get; set; } 18 | 19 | public string ApplicationBaseUrl { get; set; } 20 | public string AssetsBaseUrl { get; set; } 21 | } -------------------------------------------------------------------------------- /AiServer.ServiceInterface/AppDb/ChangeDiffusionProviderStatusCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | using AiServer.ServiceModel; 3 | using AiServer.ServiceModel.Types; 4 | using ServiceStack; 5 | using ServiceStack.OrmLite; 6 | 7 | namespace AiServer.ServiceInterface.AppDb; 8 | 9 | public class ChangeMediaProviderStatus 10 | { 11 | public string Name { get; set; } 12 | public DateTime? OfflineDate { get; set; } 13 | } 14 | 15 | [Tag(Tags.Database)] 16 | [Worker(Workers.AppDb)] 17 | public class ChangeMediaProviderStatusCommand(AppData appData, IDbConnection db) 18 | : SyncCommand 19 | { 20 | protected override void Run(ChangeMediaProviderStatus request) 21 | { 22 | db.UpdateOnly(() => new MediaProvider 23 | { 24 | OfflineDate = request.OfflineDate, 25 | }, where: x => x.Name == request.Name); 26 | 27 | var apiProvider = appData.MediaProviders.FirstOrDefault(x => x.Name == request.Name); 28 | if (apiProvider != null) 29 | apiProvider.OfflineDate = request.OfflineDate; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /AiServer.ServiceInterface/AppDb/ChangeProviderStatusCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | using AiServer.ServiceModel; 3 | using Microsoft.Extensions.Logging; 4 | using ServiceStack; 5 | using ServiceStack.OrmLite; 6 | 7 | namespace AiServer.ServiceInterface.AppDb; 8 | 9 | public class ChangeProviderStatus 10 | { 11 | public string Name { get; set; } 12 | public DateTime? OfflineDate { get; set; } 13 | } 14 | 15 | [Tag(Tags.Database)] 16 | [Worker(Workers.AppDb)] 17 | public class ChangeProviderStatusCommand(ILogger log, AppData appData, IDbConnection db) 18 | : SyncCommand 19 | { 20 | protected override void Run(ChangeProviderStatus request) 21 | { 22 | var apiProvider = appData.AiProviders.FirstOrDefault(x => x.Name == request.Name); 23 | if (apiProvider != null) 24 | apiProvider.OfflineDate = request.OfflineDate; 25 | 26 | db.UpdateOnly(() => new AiProvider { 27 | OfflineDate = request.OfflineDate, 28 | }, where:x => x.Name == request.Name); 29 | 30 | if (request.OfflineDate != null) 31 | { 32 | log.LogError("[{Name}] has been taken offline", request.Name); 33 | } 34 | else 35 | { 36 | log.LogInformation("[{Name}] is back online", request.Name); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /AiServer.ServiceInterface/AppDb/CompleteOllamaGenerateCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | using AiServer.ServiceModel; 3 | using ServiceStack; 4 | using ServiceStack.Jobs; 5 | using ServiceStack.OrmLite; 6 | 7 | namespace AiServer.ServiceInterface.AppDb; 8 | 9 | public record class CompleteOllamaGenerate(QueueOllamaGeneration Request, OllamaGenerateResponse Response, BackgroundJob Job); 10 | 11 | [Worker(Workers.AppDb)] 12 | public class CompleteOllamaGenerateCommand(IDbConnection db) : SyncCommand 13 | { 14 | protected override void Run(CompleteOllamaGenerate ctx) 15 | { 16 | var summary = new ChatSummary 17 | { 18 | Id = ctx.Job.Id, 19 | RefId = ctx.Job.RefId!, 20 | CreatedDate = ctx.Job.CreatedDate, 21 | DurationMs = ctx.Job.DurationMs, 22 | Tag = ctx.Job.Tag, 23 | Model = ctx.Request.Request.Model, 24 | Provider = ctx.Job.Worker!, 25 | PromptTokens = ctx.Response?.PromptTokens ?? 0, 26 | CompletionTokens = ctx.Response?.EvalCount ?? 0, 27 | }; 28 | try 29 | { 30 | db.Insert(summary); 31 | } 32 | catch (Exception e) 33 | { 34 | // completing failed jobs could fail with unique constraint 35 | db.DeleteById(summary.Id); 36 | db.Insert(summary); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /AiServer.ServiceInterface/AppDb/CompleteOpenAiChatCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | using AiServer.ServiceModel; 3 | using ServiceStack; 4 | using ServiceStack.Jobs; 5 | using ServiceStack.OrmLite; 6 | 7 | namespace AiServer.ServiceInterface.AppDb; 8 | 9 | public record class CompleteOpenAiChat(QueueOpenAiChatCompletion Request, OpenAiChatResponse Response, BackgroundJob Job); 10 | 11 | [Worker(Workers.AppDb)] 12 | public class CompleteOpenAiChatCommand(IDbConnection db) : SyncCommand 13 | { 14 | protected override void Run(CompleteOpenAiChat ctx) 15 | { 16 | var summary = new ChatSummary 17 | { 18 | Id = ctx.Job.Id, 19 | RefId = ctx.Job.RefId!, 20 | CreatedDate = ctx.Job.CreatedDate, 21 | DurationMs = ctx.Job.DurationMs, 22 | Tag = ctx.Job.Tag, 23 | Model = ctx.Request.Request.Model, 24 | Provider = ctx.Job.Worker!, 25 | PromptTokens = ctx.Response.Usage?.PromptTokens ?? 0, 26 | CompletionTokens = ctx.Response.Usage?.CompletionTokens ?? 0, 27 | }; 28 | try 29 | { 30 | db.Insert(summary); 31 | } 32 | catch (Exception e) 33 | { 34 | // completing failed jobs could fail with unique constraint 35 | db.DeleteById(summary.Id); 36 | db.Insert(summary); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /AiServer.ServiceInterface/AudioServices.cs: -------------------------------------------------------------------------------- 1 | using AiServer.ServiceModel; 2 | using ServiceStack; 3 | using ServiceStack.Jobs; 4 | 5 | namespace AiServer.ServiceInterface; 6 | 7 | public class AudioServices(IBackgroundJobs jobs) : Service 8 | { 9 | public async Task Any(ConvertAudio request) 10 | { 11 | if (Request?.Files == null || Request.Files.Length == 0) 12 | { 13 | throw new ArgumentException("No audio file provided"); 14 | } 15 | 16 | var outputformat = Enum.Parse(request.OutputFormat.ToString()); 17 | if(!IsAudioFormat(outputformat)) 18 | throw new ArgumentException("Invalid output format"); 19 | 20 | var transformRequest = new CreateMediaTransform 21 | { 22 | Request = new MediaTransformArgs 23 | { 24 | OutputFormat = outputformat, 25 | TaskType = MediaTransformTaskType.AudioConvert 26 | } 27 | }; 28 | 29 | var transformService = base.ResolveService(); 30 | return await transformRequest.ProcessSyncTransformAsync(jobs, transformService); 31 | } 32 | public async Task Any(QueueConvertAudio request) 33 | { 34 | if (Request?.Files == null || Request.Files.Length == 0) 35 | { 36 | throw new ArgumentException("No audio file provided"); 37 | } 38 | 39 | var outputformat = Enum.Parse(request.OutputFormat.ToString()); 40 | if(!IsAudioFormat(outputformat)) 41 | throw new ArgumentException("Invalid output format"); 42 | 43 | var transformRequest = new CreateMediaTransform 44 | { 45 | Request = new MediaTransformArgs 46 | { 47 | OutputFormat = outputformat, 48 | TaskType = MediaTransformTaskType.AudioConvert 49 | }, 50 | ReplyTo = request.ReplyTo, 51 | RefId = request.RefId 52 | }; 53 | 54 | var transformService = base.ResolveService(); 55 | return await transformRequest.ProcessQueuedTransformAsync(jobs, transformService); 56 | } 57 | 58 | private bool IsAudioFormat(MediaOutputFormat outputformat) 59 | { 60 | switch (outputformat) 61 | { 62 | case MediaOutputFormat.MP3: 63 | case MediaOutputFormat.WAV: 64 | case MediaOutputFormat.FLAC: 65 | return true; 66 | case MediaOutputFormat.MP4: 67 | case MediaOutputFormat.AVI: 68 | case MediaOutputFormat.MKV: 69 | case MediaOutputFormat.MOV: 70 | case MediaOutputFormat.WebM: 71 | case MediaOutputFormat.GIF: 72 | return false; 73 | default: 74 | throw new ArgumentOutOfRangeException(nameof(outputformat), outputformat, null); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /AiServer.ServiceInterface/ComfyApiServices.cs: -------------------------------------------------------------------------------- 1 | using AiServer.ServiceInterface.Comfy; 2 | using AiServer.ServiceModel; 3 | using AiServer.ServiceModel.Types; 4 | using ServiceStack; 5 | using ServiceStack.Jobs; 6 | 7 | namespace AiServer.ServiceInterface; 8 | 9 | public class ComfyApiServices(AppData appData) : Service 10 | { 11 | public async Task Any(GetComfyModels request) 12 | { 13 | try 14 | { 15 | var comfyClient = new ComfyClient(request.ApiBaseUrl!, request.ApiKey); 16 | var response = await comfyClient.GetModelsListAsync(default); 17 | return new GetComfyModelsResponse 18 | { 19 | Results = response.Select(x => x.Name).ToList() 20 | }; 21 | } 22 | catch (Exception e) 23 | { 24 | Console.WriteLine(e); 25 | throw HttpError.BadRequest(e.Message); 26 | } 27 | } 28 | 29 | public async Task Any(GetComfyModelMappings request) 30 | { 31 | var models = appData.MediaModelsMap 32 | .Where(x => x.Value.ApiModels.ContainsKey("ComfyUI")) 33 | .Select(x => new KeyValuePair(x.Value.ApiModels["ComfyUI"], x.Key)) 34 | // Filter out duplicates 35 | .GroupBy(x => x.Key) 36 | .Select(x => x.First()) 37 | .ToDictionary(); 38 | return new GetComfyModelMappingsResponse 39 | { 40 | Models = models 41 | }; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /AiServer.ServiceInterface/DtoExtensions.cs: -------------------------------------------------------------------------------- 1 | using AiServer.ServiceModel; 2 | 3 | namespace AiServer.ServiceInterface; 4 | 5 | public static class DtoExtensions 6 | { 7 | public static TextGenerationResponse ToTextGenerationResponse(this GenerationResponse response) => response.TextOutputs?.Count > 0 ? new() { 8 | Results = response.TextOutputs, 9 | Duration = response.Duration, 10 | ResponseStatus = response.ResponseStatus 11 | } : throw new Exception("Failed to generate any text outputs"); 12 | 13 | public static ArtifactGenerationResponse ToArtifactGenerationResponse(this GenerationResponse response) => response.Outputs?.Count > 0 ? new() { 14 | Results = response.Outputs, 15 | Duration = response.Duration, 16 | ResponseStatus = response.ResponseStatus 17 | } : throw new Exception("Failed to generate any outputs"); 18 | 19 | } -------------------------------------------------------------------------------- /AiServer.ServiceInterface/Generation/ReplicateProvider.cs: -------------------------------------------------------------------------------- 1 | using AiServer.ServiceModel; 2 | using AiServer.ServiceModel.Types; 3 | using Microsoft.Extensions.Logging; 4 | using ServiceStack; 5 | 6 | namespace AiServer.ServiceInterface.Generation; 7 | 8 | public class ReplicateAiProvider(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory) 9 | : IAiProvider 10 | { 11 | private readonly Dictionary _clients = new(); 12 | private readonly object _lockObj = new(); 13 | 14 | public Task IsOnlineAsync(MediaProvider provider, CancellationToken token = default) 15 | { 16 | return Task.FromResult(true); 17 | } 18 | 19 | public async Task<(GenerationResult, TimeSpan)> RunAsync(MediaProvider provider, GenerationArgs request, CancellationToken token = default) 20 | { 21 | var client = GetClient(provider); 22 | var start = DateTime.UtcNow; 23 | // Check model is valid 24 | if (string.IsNullOrEmpty(request.Model)) 25 | throw new Exception("Model is required"); 26 | if(provider.Models == null) 27 | throw new Exception("Provider Models not found"); 28 | if (provider.Models.All(x => x != request.Model)) 29 | throw new Exception("Model not found"); 30 | var result = await client.GenerateImage(request); 31 | return (result, DateTime.UtcNow - start); 32 | } 33 | 34 | public async Task DownloadOutputAsync(MediaProvider provider, AiProviderFileOutput output, 35 | CancellationToken token = default) 36 | { 37 | var client = GetClient(provider); 38 | return await client.DownloadOutputAsync(provider, output, token); 39 | } 40 | 41 | public List SupportedAiTasks => [AiTaskType.TextToImage]; 42 | public AiServiceProvider ProviderType => AiServiceProvider.Replicate; 43 | 44 | private ReplicateClient GetClient(MediaProvider provider) 45 | { 46 | lock (_lockObj) 47 | { 48 | if (!_clients.ContainsKey(provider.Name)) 49 | { 50 | var httpClient = httpClientFactory.CreateClient($"ReplicateClient"); 51 | var replicateClient = new ReplicateClient( 52 | httpClient, (string.IsNullOrEmpty(provider.ApiKey) ? AppConfig.Instance.ReplicateApiKey : provider.ApiKey), 53 | loggerFactory.CreateLogger()); 54 | _clients.Add(provider.Name, replicateClient); 55 | } 56 | return _clients[provider.Name]; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /AiServer.ServiceInterface/Jobs/CreateOpenAiChatCommand.cs: -------------------------------------------------------------------------------- 1 | using AiServer.ServiceInterface.AppDb; 2 | using AiServer.ServiceModel; 3 | using Microsoft.Extensions.Logging; 4 | using ServiceStack; 5 | using ServiceStack.Jobs; 6 | 7 | namespace AiServer.ServiceInterface.Jobs; 8 | 9 | public class CreateOpenAiChatCommand(ILogger logger, IBackgroundJobs jobs, AppData appData, AiProviderFactory aiFactory) 10 | : AsyncCommandWithResult 11 | { 12 | protected override async Task RunAsync(QueueOpenAiChatCompletion request, CancellationToken token) 13 | { 14 | var job = Request.GetBackgroundJob(); 15 | var log = Request.CreateJobLogger(jobs,logger); 16 | var apiProvider = appData.AssertAiProvider(job.Worker!); 17 | var chatProvider = aiFactory.GetOpenAiProvider(apiProvider.AiType.Provider); 18 | 19 | try 20 | { 21 | var origModel = request.Request.Model; 22 | request.Request.Model = appData.GetQualifiedModel(origModel) ?? origModel; 23 | log.LogInformation("CHAT OpenAi #{JobId} request for {OriginalModel}, using {Model}", job.Id, origModel, request.Request.Model); 24 | var (response, durationMs) = await chatProvider.ChatAsync(apiProvider, request.Request, token); 25 | request.Request.Model = origModel; 26 | 27 | job.DurationMs = durationMs; 28 | jobs.RunCommand( 29 | new CompleteOpenAiChat(Request: request, Response: response, Job: job)); 30 | 31 | log.LogInformation("CHAT OpenAi #{JobId} request finished in {Ms} ms{ReplyMessage}", 32 | job.Id, job.DurationMs, job.ReplyTo == null ? "" : $", sending response to {job.ReplyTo}"); 33 | if (job.ReplyTo != null) 34 | { 35 | jobs.EnqueueCommand(response, new() { 36 | ParentId = job.Id, 37 | ReplyTo = job.ReplyTo, 38 | }); 39 | } 40 | return response; 41 | } 42 | catch (Exception e) 43 | { 44 | var offline = !await chatProvider.IsOnlineAsync(apiProvider, token); 45 | log.LogError("CHAT OpenAi #{JobId} request failed after {Ms} with: {Message} (offline:{Offline})", 46 | job.Id, job.DurationMs, e.Message, offline); 47 | if (offline) 48 | { 49 | jobs.RunCommand(new ChangeProviderStatus { 50 | Name = apiProvider.Name, 51 | OfflineDate = DateTime.UtcNow, 52 | }); 53 | } 54 | throw; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /AiServer.ServiceInterface/Jobs/NotifyGenerationResponseCommand.cs: -------------------------------------------------------------------------------- 1 | using AiServer.ServiceModel; 2 | using ServiceStack.Jobs; 3 | 4 | namespace AiServer.ServiceInterface.Jobs; 5 | 6 | using System.Threading.Tasks; 7 | using ServiceStack; 8 | 9 | public class GenerationCallback 10 | { 11 | public string? State { get; set; } 12 | public string? RefId { get; set; } 13 | public List? Outputs { get; set; } 14 | 15 | public List? TextOutputs { get; set; } 16 | public ResponseStatus? ResponseStatus { get; set; } 17 | } 18 | 19 | public class NotifyGenerationResponseCommand(IHttpClientFactory clientFactory) 20 | : AsyncCommand 21 | { 22 | protected override async Task RunAsync(GenerationCallback request, CancellationToken token) 23 | { 24 | await clientFactory.SendJsonCallbackAsync(Request.GetBackgroundJob().ReplyTo!, request, token:token); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /AiServer.ServiceInterface/Jobs/NotifyOpenAiChatResponseCommand.cs: -------------------------------------------------------------------------------- 1 | using AiServer.ServiceModel; 2 | using ServiceStack; 3 | using ServiceStack.Jobs; 4 | 5 | namespace AiServer.ServiceInterface.Jobs; 6 | 7 | public class NotifyOpenAiChatResponseCommand(IHttpClientFactory clientFactory) : AsyncCommand 8 | { 9 | protected override async Task RunAsync(OpenAiChatResponse request, CancellationToken token) 10 | { 11 | await clientFactory.SendJsonCallbackAsync(Request.GetBackgroundJob().ReplyTo!, request, token:token); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /AiServer.ServiceInterface/MediaTransform/NotifyMediaTransformCommand.cs: -------------------------------------------------------------------------------- 1 | using AiServer.ServiceModel; 2 | using ServiceStack; 3 | using ServiceStack.Jobs; 4 | 5 | namespace AiServer.ServiceInterface.MediaTransform; 6 | 7 | public class MediaTransformCallback 8 | { 9 | public string? State { get; set; } 10 | public string? RefId { get; set; } 11 | public List? Outputs { get; set; } 12 | public ResponseStatus? ResponseStatus { get; set; } 13 | } 14 | 15 | public class NotifyMediaTransformCommand(IHttpClientFactory clientFactory) 16 | : AsyncCommand 17 | { 18 | protected override async Task RunAsync(MediaTransformCallback request, CancellationToken token) 19 | { 20 | await clientFactory.SendJsonCallbackAsync(Request.GetBackgroundJob().ReplyTo!, request, token:token); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /AiServer.ServiceInterface/MultipartFormDataExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using Microsoft.Extensions.Logging; 3 | 4 | namespace AiServer.ServiceInterface; 5 | 6 | // Extension method version 7 | public static class MultipartFormDataExtensions 8 | { 9 | public static async Task ToDebugStringAsync(this MultipartFormDataContent content) 10 | { 11 | var sb = new StringBuilder(); 12 | 13 | sb.AppendLine(" p.Name == "boundary")?.Value}"); 15 | 16 | int index = 0; 17 | foreach (var part in content) 18 | { 19 | sb.AppendLine($"\nPart {index++}:"); 20 | 21 | // Headers 22 | foreach (var header in part.Headers) 23 | { 24 | sb.AppendLine($" {header.Key}: {string.Join("; ", header.Value)}"); 25 | } 26 | 27 | // Content 28 | if (part is StringContent stringContent) 29 | { 30 | sb.AppendLine($" Content: {await stringContent.ReadAsStringAsync()}"); 31 | } 32 | else if (part is ByteArrayContent) 33 | { 34 | sb.AppendLine(" ByteArrayContent"); 35 | } 36 | else 37 | { 38 | sb.AppendLine($" Content-Type: {part.GetType().Name}"); 39 | } 40 | } 41 | sb.AppendLine("MultipartFormDataContent>"); 42 | 43 | return sb.ToString(); 44 | } 45 | public static async Task LogContentAsync(this MultipartFormDataContent content, 46 | ILogger logger, LogLevel logLevel = LogLevel.Debug) 47 | { 48 | if (!logger.IsEnabled(logLevel)) 49 | return; 50 | 51 | logger.Log(logLevel, await content.ToDebugStringAsync()); 52 | } 53 | } -------------------------------------------------------------------------------- /AiServer.ServiceInterface/MyServices.cs: -------------------------------------------------------------------------------- 1 | using ServiceStack; 2 | using AiServer.ServiceModel; 3 | using AiServer.ServiceModel.Types; 4 | using ServiceStack.DataAnnotations; 5 | 6 | namespace AiServer.ServiceInterface; 7 | 8 | public class MyServices : Service 9 | { 10 | public object Any(Hello request) 11 | { 12 | return new HelloResponse { Result = $"Hello, {request.Name}!" }; 13 | } 14 | } 15 | 16 | public class DummyReplyToService : Service 17 | { 18 | public static List RefIds = new(); 19 | public object Any(DummyReplyTo request) 20 | { 21 | if(RefIds.Count > 100) 22 | RefIds.Clear(); 23 | RefIds.Add(request.RefId); 24 | return new DummyReplyToResponse { RefId = request.RefId }; 25 | } 26 | 27 | public void Any(HasDummyReplyTo request) 28 | { 29 | if (RefIds.Contains(request.RefId)) 30 | RefIds.Remove(request.RefId); 31 | else 32 | { 33 | throw HttpError.NotFound("RefId not found."); 34 | } 35 | } 36 | } 37 | 38 | [Route("/dummyreplyto")] 39 | [ExcludeMetadata(Feature = Feature.All)] 40 | public class DummyReplyTo : IReturn 41 | { 42 | public string? RefId { get; set; } 43 | } 44 | 45 | public class DummyReplyToResponse 46 | { 47 | public string? RefId { get; set; } 48 | } 49 | 50 | [ExcludeMetadata(Feature = Feature.All)] 51 | public class HasDummyReplyTo : IReturnVoid 52 | { 53 | public string? RefId { get; set; } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /AiServer.ServiceInterface/OllamaServices.cs: -------------------------------------------------------------------------------- 1 | using AiServer.ServiceModel; 2 | using ServiceStack; 3 | 4 | namespace AiServer.ServiceInterface; 5 | 6 | public class OllamaServices : Service 7 | { 8 | public async Task Any(GetOllamaModels request) 9 | { 10 | var apiTagsUrl = request.ApiBaseUrl.CombineWith("/api/tags"); 11 | var json = await apiTagsUrl.GetJsonFromUrlAsync(); 12 | var ollamaModels = new List(); 13 | var obj = (Dictionary) JSON.parse(json); 14 | if (obj.TryGetValue("models", out var oModels) && oModels is List models) 15 | { 16 | foreach (var oModel in models.Cast>()) 17 | { 18 | var dto = new OllamaModel(); 19 | oModel.PopulateInstance(dto); 20 | ollamaModels.Add(dto); 21 | } 22 | } 23 | return new GetOllamaModelsResponse 24 | { 25 | Results = ollamaModels 26 | }; 27 | } 28 | } -------------------------------------------------------------------------------- /AiServer.ServiceInterface/PromptServices.cs: -------------------------------------------------------------------------------- 1 | using ServiceStack; 2 | using AiServer.ServiceModel; 3 | 4 | namespace AiServer.ServiceInterface; 5 | 6 | public class PromptServices(AppData appData, IAutoQueryData autoQueryData) : Service 7 | { 8 | public object Get(QueryPrompts query) 9 | { 10 | var db = appData.Prompts.ToDataSource(query, Request!); 11 | return autoQueryData.Execute(query, autoQueryData.CreateQuery(query, Request, db), db); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /AiServer.ServiceInterface/Recurring/CheckGenerationProviders.cs: -------------------------------------------------------------------------------- 1 | using AiServer.ServiceInterface.AppDb; 2 | using Microsoft.Extensions.Logging; 3 | using ServiceStack; 4 | using ServiceStack.Jobs; 5 | 6 | namespace AiServer.ServiceInterface.Recurring; 7 | 8 | public class CheckGenerationProviders(ILogger logger, IBackgroundJobs jobs, AppData appData) : AsyncCommand 9 | { 10 | private static long count; 11 | protected override async Task RunAsync(CancellationToken token) 12 | { 13 | Interlocked.Increment(ref count); 14 | 15 | if (appData.IsStopped) 16 | return; 17 | 18 | var log = Request.CreateJobLogger(jobs, logger); 19 | // Check if any offline providers are back online 20 | var offlineMediaProviders = appData.MediaProviders.Where(x => x is { Enabled:true, OfflineDate:not null }).ToList(); 21 | if (offlineMediaProviders.Count > 0) 22 | { 23 | log.LogInformation("GENERATION {Count} Rechecking {OfflineCount} offline providers", count, offlineMediaProviders.Count); 24 | foreach (var apiProvider in offlineMediaProviders) 25 | { 26 | var chatProvider = appData.GetGenerationProvider(apiProvider); 27 | if (await chatProvider.IsOnlineAsync(apiProvider, token)) 28 | { 29 | if (appData.IsStopped) 30 | return; 31 | 32 | log.LogInformation("GENERATION Provider {Provider} is back online", apiProvider.Name); 33 | jobs.RunCommand(new ChangeMediaProviderStatus { 34 | Name = apiProvider.Name, 35 | OfflineDate = null, 36 | }); 37 | } 38 | } 39 | } 40 | else 41 | { 42 | log.LogInformation("GENERATION All providers are online"); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /AiServer.ServiceInterface/Recurring/CheckOpenAiProviders.cs: -------------------------------------------------------------------------------- 1 | using AiServer.ServiceInterface.AppDb; 2 | using Microsoft.Extensions.Logging; 3 | using ServiceStack; 4 | using ServiceStack.Jobs; 5 | 6 | namespace AiServer.ServiceInterface.Recurring; 7 | 8 | public class CheckOpenAiProviders(ILogger log, AppData appData, IBackgroundJobs jobs) : AsyncCommand 9 | { 10 | private static long count; 11 | protected override async Task RunAsync(CancellationToken token) 12 | { 13 | Interlocked.Increment(ref count); 14 | if (appData.IsStopped) 15 | return; 16 | 17 | var allStats = jobs.GetWorkerStats(); 18 | var allStatsTable = Inspect.dumpTable(allStats, new TextDumpOptions { 19 | Caption = "Worker Stats", 20 | Headers = [ 21 | nameof(WorkerStats.Name), 22 | nameof(WorkerStats.Queued), 23 | nameof(WorkerStats.Received), 24 | nameof(WorkerStats.Completed), 25 | nameof(WorkerStats.Retries), 26 | nameof(WorkerStats.Failed), 27 | ], 28 | }).Trim(); 29 | 30 | var offlineWorkers = appData.AiProviders 31 | .Where(x => x is { Enabled: true, OfflineDate: not null }).Map(x => x.Name); 32 | 33 | var offlineProviders = offlineWorkers.IsEmpty() ? "None" : offlineWorkers.Join(", "); 34 | log.LogInformation(""" 35 | Workers: 36 | {Stats} 37 | 38 | Offline: {Offline} 39 | """, 40 | appData.StoppedAt == null ? allStatsTable : $"Stopped at {appData.StoppedAt}", offlineProviders); 41 | 42 | var offlineAiProviders = appData.AiProviders.Where(x => x is { Enabled:true, OfflineDate:not null }).ToList(); 43 | if (offlineAiProviders.Count > 0) 44 | { 45 | log.LogInformation("CHAT {Count} Rechecking {OfflineCount} offline providers", count, offlineAiProviders.Count); 46 | foreach (var apiProvider in offlineAiProviders) 47 | { 48 | var chatProvider = appData.GetOpenAiProvider(apiProvider); 49 | if (await chatProvider.IsOnlineAsync(apiProvider, token)) 50 | { 51 | log.LogInformation("CHAT Provider {Provider} is back online", apiProvider.Name); 52 | jobs.RunCommand(new ChangeProviderStatus { 53 | Name = apiProvider.Name, 54 | OfflineDate = null, 55 | }); 56 | } 57 | } 58 | } 59 | else 60 | { 61 | log.LogInformation("CHAT All providers are online"); 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /AiServer.ServiceInterface/Tags.cs: -------------------------------------------------------------------------------- 1 | namespace AiServer.ServiceInterface; 2 | 3 | public static class Tags 4 | { 5 | public const string Database = nameof(Database); 6 | public const string MonthlyDatabase = nameof(MonthlyDatabase); 7 | public const string OpenAiChat = nameof(OpenAiChat); 8 | public const string ComfyWorkflow = nameof(ComfyWorkflow); 9 | public const string Executor = nameof(Executor); 10 | public const string Notifications = nameof(Notifications); 11 | public const string ImageGeneration = nameof(ImageGeneration); 12 | } -------------------------------------------------------------------------------- /AiServer.ServiceModel/AiServer.ServiceModel.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /AiServer.ServiceModel/ApiKeys.cs: -------------------------------------------------------------------------------- 1 | using ServiceStack; 2 | 3 | namespace AiServer.ServiceModel; 4 | 5 | // public class QueryApiKeys : QueryDb {} 6 | // public class ApiKey 7 | // { 8 | // /// 9 | // /// The API Key 10 | // /// 11 | // public string Id { get; set; } 12 | // 13 | // public string UserId { get; set; } 14 | // 15 | // /// 16 | // /// The Name of the API Key or Worker using it 17 | // /// 18 | // public string UserName { get; set; } 19 | // 20 | // public string MaskedKey { get; set; } 21 | // 22 | // public DateTime CreatedDate { get; set; } 23 | // 24 | // public DateTime? ExpiryDate { get; set; } 25 | // 26 | // public DateTime? CancelledDate { get; set; } 27 | // 28 | // public string? Notes { get; set; } 29 | // } -------------------------------------------------------------------------------- /AiServer.ServiceModel/Comfy.cs: -------------------------------------------------------------------------------- 1 | using AiServer.ServiceModel.Types; 2 | using ServiceStack; 3 | 4 | namespace AiServer.ServiceModel; 5 | 6 | 7 | [Tag(Tags.MediaInfo)] 8 | public class GetComfyModels : IReturn 9 | { 10 | public string? ApiBaseUrl { get; set; } 11 | public string? ApiKey { get; set; } 12 | } 13 | 14 | public class GetComfyModelsResponse 15 | { 16 | public List Results { get; set; } 17 | public ResponseStatus? ResponseStatus { get; set; } 18 | } 19 | 20 | public class GetComfyModelMappingsResponse 21 | { 22 | public Dictionary Models { get; set; } 23 | } 24 | 25 | [Tag(Tags.MediaInfo)] 26 | public class GetComfyModelMappings : IReturn 27 | { 28 | } 29 | 30 | public class ComfyAgentDownloadStatus 31 | { 32 | public string? Name { get; set; } 33 | public int? Progress { get; set; } 34 | } 35 | 36 | [Route("/comfy/{Year}/{Month}/{Day}/{Filename}")] 37 | public class DownloadComfyFile : IReturn 38 | { 39 | public int? Year { get; set; } 40 | public int? Month { get; set; } 41 | public int? Day { get; set; } 42 | public string? FileName { get; set; } 43 | } 44 | 45 | [Tag(Tags.Comfy)] 46 | [Route("/comfy/workflows")] 47 | public class GetComfyWorkflows : IGet, IReturn {} 48 | 49 | [Tag(Tags.Comfy)] 50 | [Route("/comfy/workflows/info")] 51 | public class GetComfyWorkflowInfo : IGet, IReturn 52 | { 53 | [ValidateNotEmpty] 54 | public string Workflow { get; set; } 55 | } 56 | public class GetComfyWorkflowInfoResponse 57 | { 58 | public ComfyWorkflowInfo Result { get; set; } 59 | public ResponseStatus? ResponseStatus { get; set; } 60 | } 61 | 62 | [Tag(Tags.Comfy)] 63 | [Route("/comfy/workflows/prompt")] 64 | public class GetComfyApiPrompt : IGet, IReturn 65 | { 66 | [ValidateNotEmpty] 67 | public string Workflow { get; set; } 68 | } 69 | 70 | [Tag(Tags.Comfy)] 71 | [Route("/comfy/workflows/queue")] 72 | public class QueueComfyWorkflow : IPost, IReturn 73 | { 74 | [ValidateNotEmpty] 75 | public string Workflow { get; set; } 76 | public Dictionary? Args { get; set; } 77 | } 78 | public class QueueComfyWorkflowResponse 79 | { 80 | public long MediaProviderId { get; set; } 81 | public string RefId { get; set; } 82 | public string PromptId { get; set; } 83 | public long JobId { get; set; } 84 | public ResponseStatus? ResponseStatus { get; set; } 85 | } 86 | -------------------------------------------------------------------------------- /AiServer.ServiceModel/ComfyTypes.cs: -------------------------------------------------------------------------------- 1 | using AiServer.ServiceModel.Types; 2 | 3 | namespace AiServer.ServiceModel; 4 | 5 | public class ComfyFileRef 6 | { 7 | public string Path { get; set; } 8 | public int Size { get; set; } 9 | public double Modified { get; set; } 10 | } 11 | 12 | public enum ComfyWorkflowType 13 | { 14 | TextToImage, 15 | ImageToImage, 16 | ImageToText, 17 | TextToAudio, 18 | TextToVideo, 19 | TextTo3D, 20 | AudioToText, 21 | VideoToText, 22 | ImageToVideo, 23 | } 24 | 25 | public enum ComfyPrimarySource 26 | { 27 | Text, 28 | Image, 29 | Video, 30 | Audio, 31 | } 32 | 33 | //jq -r 'to_entries[] | .value.input.required // {} | to_entries[] | .value[0] | if type == "array" then "ENUM" else . end' files/object_info.json | sort | uniq 34 | public enum ComfyInputType 35 | { 36 | Unknown, 37 | Audio, 38 | Boolean, 39 | Clip, 40 | ClipVision, 41 | ClipVisionOutput, 42 | Combo, 43 | Conditioning, 44 | ControlNet, 45 | Enum, 46 | FasterWhisperModel, 47 | Filepath, 48 | Fl2Model, 49 | Float, 50 | Floats, 51 | Gligen, 52 | Guider, 53 | Hooks, 54 | Image, 55 | Int, 56 | Latent, 57 | LatentOperation, 58 | Load3D, 59 | Load3DAnimation, 60 | Mask, 61 | Mesh, 62 | Model, 63 | Noise, 64 | Photomaker, 65 | Sampler, 66 | Sigmas, 67 | String, 68 | StyleModel, 69 | Subtitle, 70 | TranscriptionPipeline, 71 | Transcriptions, 72 | UpscaleModel, 73 | VAE, 74 | VHSAudio, 75 | Voxel, 76 | WavBytes, 77 | WavBytesBatch, 78 | Webcam, 79 | } 80 | 81 | public class ComfyInput 82 | { 83 | public int NodeId { get; set; } 84 | public int ValueIndex { get; set; } 85 | public string Name { get; set; } 86 | public string Label { get; set; } 87 | public ComfyInputType Type { get; set; } 88 | public string? Tooltip { get; set; } 89 | public object? Default { get; set; } 90 | public decimal? Min { get; set; } 91 | public decimal? Max { get; set; } 92 | public decimal? Step { get; set; } 93 | public decimal? Round { get; set; } 94 | public bool? Multiline { get; set; } 95 | public bool? DynamicPrompts { get; set; } 96 | public bool? ControlAfterGenerate { get; set; } 97 | public string[]? EnumValues { get; set; } 98 | public Dictionary? ComboValues { get; set; } 99 | } 100 | 101 | public class ComfyArg 102 | { 103 | public ComfyInput Input { get; set; } 104 | public object? Value { get; set; } 105 | } 106 | 107 | public class ComfyWorkflowInfo 108 | { 109 | public string Name { get; set; } 110 | public string Path { get; set; } 111 | public ComfyWorkflowType Type { get; set; } 112 | public ComfyPrimarySource Input { get; set; } 113 | public ComfyPrimarySource Output { get; set; } 114 | public List Inputs { get; set; } = []; 115 | } -------------------------------------------------------------------------------- /AiServer.ServiceModel/Databases.cs: -------------------------------------------------------------------------------- 1 | using ServiceStack; 2 | 3 | namespace AiServer.ServiceModel; 4 | 5 | public class Databases 6 | { 7 | public const string App = Workers.AppDb; 8 | public const string Jobs = Workers.JobsDb; 9 | } 10 | -------------------------------------------------------------------------------- /AiServer.ServiceModel/GetSummaryStats.cs: -------------------------------------------------------------------------------- 1 | using ServiceStack; 2 | 3 | namespace AiServer.ServiceModel; 4 | 5 | [Tag(Tags.Admin)] 6 | [ValidateAuthSecret] 7 | public class GetSummaryStats : IGet, IReturn 8 | { 9 | public DateTime? From { get; set; } 10 | public DateTime? To { get; set; } 11 | } 12 | 13 | public class GetSummaryStatsResponse 14 | { 15 | public List ProviderStats { get; set; } 16 | public List ModelStats { get; set; } 17 | public List MonthStats { get; set; } 18 | } 19 | 20 | public class SummaryStats 21 | { 22 | public string Name { get; set; } 23 | public int Total { get; set; } 24 | public int TotalPromptTokens { get; set; } 25 | public int TotalCompletionTokens { get; set; } 26 | public double TotalMinutes { get; set; } 27 | public double TokensPerSecond { get; set; } 28 | } 29 | -------------------------------------------------------------------------------- /AiServer.ServiceModel/Hello.cs: -------------------------------------------------------------------------------- 1 | using ServiceStack; 2 | 3 | namespace AiServer.ServiceModel; 4 | 5 | [Route("/hello/{Name}")] 6 | public class Hello : IGet, IReturn 7 | { 8 | public required string Name { get; set; } 9 | } 10 | 11 | public class HelloResponse 12 | { 13 | public required string Result { get; set; } 14 | } 15 | -------------------------------------------------------------------------------- /AiServer.ServiceModel/Icons.cs: -------------------------------------------------------------------------------- 1 | namespace AiServer.ServiceModel; 2 | 3 | public static class Icons 4 | { 5 | public const string AiModel = """ 6 | 7 | """; 8 | 9 | public const string Stats = """ 10 | 11 | """; 12 | 13 | public const string Work = """ 14 | 15 | """; 16 | 17 | public const string Type = """ 18 | 19 | """; 20 | 21 | public const string MediaProvider = """ 22 | 23 | """; 24 | } -------------------------------------------------------------------------------- /AiServer.ServiceModel/Ollama.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | using ServiceStack; 3 | 4 | namespace AiServer.ServiceModel; 5 | 6 | [Tag(Tags.AiInfo)] 7 | public class GetOllamaModels : IGet, IReturn 8 | { 9 | [ValidateNotEmpty] 10 | public string ApiBaseUrl { get; set; } 11 | } 12 | public class GetOllamaModelsResponse 13 | { 14 | public List Results { get; set; } 15 | public ResponseStatus ResponseStatus { get; set; } 16 | } 17 | 18 | [DataContract] 19 | public class OllamaModel 20 | { 21 | [DataMember(Name = "name")] 22 | public string Name { get; set; } 23 | [DataMember(Name = "model")] 24 | public string Model { get; set; } 25 | [DataMember(Name = "modified_at")] 26 | public DateTime ModifiedAt { get; set; } 27 | [DataMember(Name = "size")] 28 | public long Size { get; set; } 29 | [DataMember(Name = "digest")] 30 | public string Digest { get; set; } 31 | [DataMember(Name = "details")] 32 | public OllamaModelDetails Details { get; set; } 33 | } 34 | 35 | [DataContract] 36 | public class OllamaModelDetails 37 | { 38 | [DataMember(Name = "parent_model")] 39 | public string ParentModel { get; set; } 40 | [DataMember(Name = "format")] 41 | public string Format { get; set; } 42 | [DataMember(Name = "family")] 43 | public string Family { get; set; } 44 | [DataMember(Name = "families")] 45 | public List Families { get; set; } 46 | [DataMember(Name = "parameter_size")] 47 | public string ParameterSize { get; set; } 48 | [DataMember(Name = "quantization_level")] 49 | public string QuantizationLevel { get; set; } 50 | } 51 | -------------------------------------------------------------------------------- /AiServer.ServiceModel/Prompts.cs: -------------------------------------------------------------------------------- 1 | using ServiceStack; 2 | using ServiceStack.DataAnnotations; 3 | using ServiceStack.Model; 4 | 5 | namespace AiServer.ServiceModel; 6 | 7 | [ExcludeMetadata] 8 | public class QueryPrompts : QueryData {} 9 | public class Prompt : IHasId 10 | { 11 | public string Id { get; set; } 12 | public string Name { get; set; } 13 | public string Value { get; set; } 14 | } 15 | -------------------------------------------------------------------------------- /AiServer.ServiceModel/QueueOperations.cs: -------------------------------------------------------------------------------- 1 | using AiServer.ServiceModel.Types; 2 | using ServiceStack; 3 | 4 | namespace AiServer.ServiceModel; 5 | 6 | [Tag(Tags.Admin)] 7 | [ValidateAuthSecret] 8 | public class Reload : IPost, IReturn {} 9 | 10 | [Tag(Tags.Admin)] 11 | [ValidateAuthSecret] 12 | public class ChangeAiProviderStatus : IPost, IReturn 13 | { 14 | public string Provider { get; set; } 15 | public bool Online { get; set; } 16 | } 17 | 18 | [Tag(Tags.Admin)] 19 | [ValidateAuthSecret] 20 | public class CheckAiProviderStatus : IPost, IReturn 21 | { 22 | public string Provider { get; set; } 23 | } 24 | 25 | [Tag(Tags.Admin)] 26 | [ValidateAuthSecret] 27 | public class CheckMediaProviderStatus : IPost, IReturn 28 | { 29 | public string Provider { get; set; } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /AiServer.ServiceModel/Tags.cs: -------------------------------------------------------------------------------- 1 | namespace AiServer.ServiceModel; 2 | 3 | public static class Tags 4 | { 5 | public const string AI = nameof(AI); 6 | public const string AiInfo = "AI Info"; 7 | public const string Media = nameof(Media); 8 | public const string MediaInfo = "Media Info"; 9 | public const string Admin = nameof(Admin); 10 | public const string Files = nameof(Files); 11 | public const string Comfy = nameof(Comfy); 12 | } -------------------------------------------------------------------------------- /AiServer.ServiceModel/Types/ComfyApi.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | using ServiceStack; 3 | using ServiceStack.DataAnnotations; 4 | 5 | namespace AiServer.ServiceModel.Types; 6 | 7 | public class ComfyHostedFileOutput 8 | { 9 | public string Url { get; set; } 10 | public string FileName { get; set; } 11 | } 12 | 13 | public class AiServerHostedComfyFile 14 | { 15 | public string Url { get; set; } 16 | public string FileName { get; set; } 17 | public string ContentType { get; set; } 18 | } 19 | 20 | public class ComfyWorkflowRequest 21 | { 22 | public string? Model { get; set; } 23 | 24 | public int? Steps { get; set; } 25 | 26 | public int BatchSize { get; set; } 27 | 28 | public int? Seed { get; set; } 29 | public string? PositivePrompt { get; set; } 30 | public string? NegativePrompt { get; set; } 31 | 32 | public ComfyFileInput? Image { get; set; } 33 | public ComfyFileInput? Audio { get; set; } 34 | public ComfyFileInput? Mask { get; set; } 35 | 36 | public Stream? ImageInput { get; set; } 37 | public Stream? AudioInput { get; set; } 38 | public Stream? MaskInput { get; set; } 39 | 40 | public ComfySampler? Sampler { get; set; } 41 | public string? Scheduler { get; set; } 42 | public double? CfgScale { get; set; } 43 | public double? Denoise { get; set; } 44 | 45 | public string? UpscaleModel { get; set; } 46 | 47 | public int? Width { get; set; } 48 | public int? Height { get; set; } 49 | 50 | public ComfyTaskType TaskType { get; set; } 51 | public string? Clip { get; set; } 52 | public double? SampleLength { get; set; } 53 | public ComfyMaskSource MaskChannel { get; set; } 54 | } 55 | 56 | public class ComfyApiModel 57 | { 58 | public string? Description { get; set; } 59 | 60 | public string? Tags { get; set; } 61 | public string Filename { get; set; } 62 | public string DownloadUrl { get; set; } 63 | 64 | public string IconUrl { get; set; } 65 | public string Url { get; set; } 66 | } 67 | 68 | public class ComfyApiModelSettings 69 | { 70 | public double? CfgScale { get; set; } 71 | 72 | public string? Scheduler { get; set; } 73 | 74 | public ComfySampler? Sampler { get; set; } 75 | 76 | public int? Width { get; set; } 77 | 78 | public int? Height { get; set; } 79 | 80 | public int? Steps { get; set; } 81 | 82 | public string? NegativePrompt { get; set; } 83 | } 84 | 85 | [EnumAsInt] 86 | public enum ComfyTaskType 87 | { 88 | TextToImage = 1, 89 | ImageToImage = 2, 90 | ImageUpscale = 3, 91 | ImageWithMask = 4, 92 | ImageToText = 5, 93 | TextToAudio = 6, 94 | TextToSpeech = 7, 95 | SpeechToText = 8, 96 | } -------------------------------------------------------------------------------- /AiServer.ServiceModel/Types/OpenAiChat.cs: -------------------------------------------------------------------------------- 1 | namespace AiServer.ServiceModel.Types; 2 | 3 | public class PeriodicTasks 4 | { 5 | public PeriodicFrequency PeriodicFrequency { get; set; } 6 | } 7 | public enum PeriodicFrequency 8 | { 9 | Minute, 10 | Hourly, 11 | Daily, 12 | Monthly, 13 | } 14 | 15 | -------------------------------------------------------------------------------- /AiServer.ServiceModel/Types/README.md: -------------------------------------------------------------------------------- 1 | As part of our [Physical Project Structure](https://docs.servicestack.net/physical-project-structure) convention we recommend maintaining any shared non Request/Response DTOs in the `ServiceModel.Types` namespace. -------------------------------------------------------------------------------- /AiServer.Tests/AiServer.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | portable 6 | Library 7 | enable 8 | enable 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | PreserveNewest 28 | Never 29 | 30 | 31 | PreserveNewest 32 | Never 33 | 34 | 35 | PreserveNewest 36 | Never 37 | 38 | 39 | PreserveNewest 40 | Never 41 | 42 | 43 | 44 | 45 | 46 | PreserveNewest 47 | Never 48 | 49 | 50 | 51 | 52 | 53 | PreserveNewest 54 | Never 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /AiServer.Tests/BlazorDiffusionTasks.cs: -------------------------------------------------------------------------------- 1 | using AiServer.ServiceModel; 2 | using AiServer.ServiceModel.Types; 3 | using NUnit.Framework; 4 | 5 | namespace AiServer.Tests; 6 | 7 | [Explicit] 8 | public class BlazorDiffusionTasks 9 | { 10 | 11 | } -------------------------------------------------------------------------------- /AiServer.Tests/ConnectivityTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Headers; 2 | using System.Text; 3 | using NUnit.Framework; 4 | 5 | namespace AiServer.Tests; 6 | 7 | public class ConnectivityTests 8 | { 9 | [Test, Explicit("Integration test")] 10 | public async Task Can_call_supermicro() 11 | { 12 | // Environment.SetEnvironmentVariable("DOTNET_SYSTEM_NET_DISABLEIPV6", "1"); 13 | // var url = "http://192.168.4.200:11434/api/tags"; 14 | var url = "https://supermicro.pvq.app/api/tags"; 15 | using var client = new HttpClient(); 16 | var res = await client.GetAsync(url); 17 | res.EnsureSuccessStatusCode(); 18 | var json = await res.Content.ReadAsStringAsync(); 19 | Console.WriteLine(json); 20 | } 21 | 22 | [Test, Explicit("Integration test")] 23 | public async Task Can_call_GetOllamaModels() 24 | { 25 | // Environment.SetEnvironmentVariable("DOTNET_SYSTEM_NET_DISABLEIPV6", "1"); 26 | // var url = "https://openai.servicestack.net/api/GetOllamaModels" 27 | // .AddQueryParam("ApiBaseUrl", "https://supermicro.pvq.app"); 28 | var url = "https://okai.servicestack.com/models/gist?prompt=1735872878252"; 29 | using var client = new HttpClient(); 30 | var res = await client.GetAsync(url); 31 | res.EnsureSuccessStatusCode(); 32 | var json = await res.Content.ReadAsStringAsync(); 33 | Console.WriteLine(json); 34 | } 35 | 36 | [Test, Explicit("Integration test")] 37 | public async Task Can_call_groq() 38 | { 39 | var url = "https://api.groq.com/openai/v1/chat/completions"; 40 | using var client = new HttpClient(); 41 | var content = new StringContent( 42 | """ 43 | { 44 | "messages": [{ 45 | "role": "user", 46 | "content": "Capital of France?" 47 | }], 48 | "model": "qwen-qwq-32b", 49 | "max_tokens": 2048, 50 | "temperature": 0.7, 51 | "stream": false 52 | } 53 | """, 54 | Encoding.UTF8, "application/json"); 55 | client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("GROQ_API_KEY")); 56 | client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 57 | 58 | var res = await client.PostAsync(url, content); 59 | res.EnsureSuccessStatusCode(); 60 | var json = await res.Content.ReadAsStringAsync(); 61 | Console.WriteLine(json); 62 | } 63 | } -------------------------------------------------------------------------------- /AiServer.Tests/GoogleTests.cs: -------------------------------------------------------------------------------- 1 | using AiServer.ServiceInterface; 2 | using Microsoft.Extensions.Logging.Abstractions; 3 | using NUnit.Framework; 4 | 5 | namespace AiServer.Tests; 6 | 7 | [Explicit("Integration tests")] 8 | public class GoogleTests 9 | { 10 | private static GoogleAiProvider CreateProvider() 11 | { 12 | return new GoogleAiProvider(new NullLogger()); 13 | } 14 | 15 | [Test] 16 | public async Task Check_Google_Models() 17 | { 18 | // https://ai.google.dev/gemini-api/docs/models 19 | var aiProvider = TestUtils.GoogleAiProvider; 20 | var google = CreateProvider(); 21 | 22 | foreach (var model in aiProvider.Models) 23 | { 24 | var active = await google.IsOnlineAsync(aiProvider, model.Model); 25 | Console.WriteLine("Google {0}: {1}", model.Model, active); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /AiServer.Tests/ImageToTextTests.cs: -------------------------------------------------------------------------------- 1 | using AiServer.ServiceModel; 2 | using NUnit.Framework; 3 | using ServiceStack; 4 | using ServiceStack.Jobs; 5 | 6 | namespace AiServer.Tests; 7 | 8 | [Explicit("Integration tests require a running service")] 9 | public class ImageToTextIntegrationTests : IntegrationTestBase 10 | { 11 | [Test] 12 | public async Task Can_queue_convert_image_to_text() 13 | { 14 | var client = CreateClient(); 15 | 16 | TextGenerationResponse? response = null; 17 | try 18 | { 19 | await using var imageStream = File.OpenRead("files/comfyui_upload_test.png"); 20 | response = client.PostFilesWithRequest(new ImageToText 21 | { 22 | 23 | }, [ 24 | new UploadFile("image.png", imageStream){ FieldName = "image"} 25 | ]); 26 | } 27 | catch (Exception e) 28 | { 29 | Assert.Fail(e.Message); 30 | } 31 | 32 | Assert.That(response, Is.Not.Null); 33 | 34 | Assert.That(response.Results, Is.Not.Null); 35 | Assert.That(response.Results, Is.Not.Empty); 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /AiServer.Tests/ImageUpscaleTests.cs: -------------------------------------------------------------------------------- 1 | using AiServer.ServiceModel; 2 | using NUnit.Framework; 3 | using ServiceStack; 4 | using ServiceStack.Jobs; 5 | using SixLabors.ImageSharp; 6 | 7 | namespace AiServer.Tests; 8 | 9 | [Explicit("Integration tests require a running service")] 10 | public class ImageUpscaleIntegrationTests : IntegrationTestBase 11 | { 12 | [Test] 13 | public async Task Can_queue_upscale_image() 14 | { 15 | var client = CreateClient(); 16 | 17 | ArtifactGenerationResponse? response = null; 18 | try 19 | { 20 | await using var imageStream = File.OpenRead("files/comfyui_upload_test.png"); 21 | response = client.PostFilesWithRequest(new ImageUpscale 22 | { 23 | 24 | }, [ 25 | new UploadFile("image.png", imageStream){ FieldName = "image"} 26 | ]); 27 | } 28 | catch (Exception e) 29 | { 30 | Assert.Fail(e.Message); 31 | } 32 | 33 | Assert.That(response, Is.Not.Null); 34 | Assert.That(response, Is.Not.Null); 35 | 36 | Assert.That(response.Results, Is.Not.Null); 37 | Assert.That(response.Results, Is.Not.Empty); 38 | 39 | // Download image 40 | // Validate that the output image is a valid image 41 | var outputImage = response.Results[0]; 42 | Assert.That(outputImage.FileName, Does.EndWith(".webp")); 43 | // Download the image 44 | var downloadResponse = await client.GetHttpClient().GetStreamAsync(outputImage.Url); 45 | Assert.That(downloadResponse, Is.Not.Null); 46 | 47 | // Load the image 48 | var outputImageBytes = downloadResponse.ReadFully(); 49 | using var outputImageStream = new MemoryStream(outputImageBytes); 50 | var outputImageInfo = Image.Load(outputImageStream); 51 | Assert.That(outputImageInfo, Is.Not.Null); 52 | Assert.That(outputImageInfo.Width, Is.GreaterThan(0)); 53 | Assert.That(outputImageInfo.Height, Is.GreaterThan(0)); 54 | 55 | // Confirm the image dimensions are double the input image 56 | await using var originalImageStream = File.OpenRead("files/comfyui_upload_test.png"); 57 | var inputImageInfo = Image.Load(originalImageStream); 58 | Assert.That(outputImageInfo.Width, Is.EqualTo(inputImageInfo.Width * 2)); 59 | Assert.That(outputImageInfo.Height, Is.EqualTo(inputImageInfo.Height * 2)); 60 | } 61 | } -------------------------------------------------------------------------------- /AiServer.Tests/IntegrationTest.cs: -------------------------------------------------------------------------------- 1 | using Funq; 2 | using ServiceStack; 3 | using NUnit.Framework; 4 | using AiServer.ServiceInterface; 5 | using AiServer.ServiceModel; 6 | 7 | namespace AiServer.Tests; 8 | 9 | public class IntegrationTest 10 | { 11 | const string BaseUri = "http://localhost:2000/"; 12 | private readonly ServiceStackHost appHost; 13 | 14 | class AppHost : AppSelfHostBase 15 | { 16 | public AppHost() : base(nameof(IntegrationTest), typeof(MyServices).Assembly) { } 17 | 18 | public override void Configure(Container container) 19 | { 20 | } 21 | } 22 | 23 | public IntegrationTest() 24 | { 25 | appHost = new AppHost() 26 | .Init() 27 | .Start(BaseUri); 28 | } 29 | 30 | [OneTimeTearDown] 31 | public void OneTimeTearDown() => appHost.Dispose(); 32 | 33 | public IServiceClient CreateClient() => new JsonServiceClient(BaseUri); 34 | 35 | [Test] 36 | public void Can_call_Hello_Service() 37 | { 38 | var client = CreateClient(); 39 | 40 | var response = client.Get(new Hello { Name = "World" }); 41 | 42 | Assert.That(response.Result, Is.EqualTo("Hello, World!")); 43 | } 44 | } -------------------------------------------------------------------------------- /AiServer.Tests/MigrationTests.cs: -------------------------------------------------------------------------------- 1 | using AiServer.Migrations; 2 | using AiServer.ServiceModel; 3 | using NUnit.Framework; 4 | using ServiceStack; 5 | using ServiceStack.Data; 6 | using ServiceStack.OrmLite; 7 | using ServiceStack.Text; 8 | 9 | namespace AiServer.Tests; 10 | 11 | [TestFixture, Explicit, Category(nameof(MigrationTasks))] 12 | public class MigrationTasks 13 | { 14 | IDbConnectionFactory ResolveDbFactory() => new ConfigureDb().ConfigureAndResolve(); 15 | Migrator CreateMigrator() => new(ResolveDbFactory(), typeof(Migration1001).Assembly); 16 | 17 | [Test] 18 | public void Run_Migration1000() 19 | { 20 | OrmLiteUtils.PrintSql(); 21 | var dbFactory = ResolveDbFactory(); 22 | Migrator.Run(dbFactory, typeof(Migration1001), m => m.Up()); 23 | } 24 | 25 | [Test] 26 | public void Generate_Api_Providers_Json() 27 | { 28 | OrmLiteUtils.PrintSql(); 29 | var dbFactory = ResolveDbFactory(); 30 | 31 | using var db = dbFactory.Open(); 32 | var apiProviders = db.Select(); 33 | apiProviders.ToJson().Print(); 34 | Environment.CurrentDirectory.Print(); 35 | } 36 | 37 | [Test] 38 | public void Print_loaded_models() 39 | { 40 | string[] ollamaUrls = 41 | [ 42 | "https://macbook.pvq.app", 43 | "https://amd.pvq.app", 44 | "https://supermicro.pvq.app", 45 | "https://dell.pvq.app", 46 | ]; 47 | 48 | var map = new Dictionary>(); 49 | foreach (var url in ollamaUrls) 50 | { 51 | var apiTagsUrl = url.CombineWith("/api/tags"); 52 | var json = apiTagsUrl.GetJsonFromUrl(); 53 | var ollamaModels = new List(); 54 | map[url] = ollamaModels; 55 | var obj = (Dictionary) JSON.parse(json); 56 | if (obj.TryGetValue("models", out var oModels) && oModels is List models) 57 | { 58 | foreach (var oModel in models.Cast>()) 59 | { 60 | var dto = new OllamaModel(); 61 | oModel.PopulateInstance(dto); 62 | ollamaModels.Add(dto); 63 | } 64 | } 65 | } 66 | 67 | foreach (var entry in map) 68 | { 69 | "\n".Print(); 70 | entry.Key.Print(); 71 | entry.Value.ForEach(x => x.Model.Print()); 72 | } 73 | } 74 | 75 | [Test] 76 | public void Generate_ApIProviders() 77 | { 78 | PvqApiTests.AiProviders.ToJson().Print(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /AiServer.Tests/OllamaApiTests.cs: -------------------------------------------------------------------------------- 1 | using AiServer.ServiceModel; 2 | using NUnit.Framework; 3 | using ServiceStack; 4 | using ServiceStack.Text; 5 | 6 | namespace AiServer.Tests; 7 | 8 | [Explicit] 9 | public class OllamaApiTests 10 | { 11 | [Test] 12 | public async Task Can_execute_ollama_task() 13 | { 14 | Environment.SetEnvironmentVariable("DOTNET_SYSTEM_NET_DISABLEIPV6", "1"); 15 | 16 | var model = "gemma3:27b"; 17 | var client = TestUtils.CreatePvqClient(); 18 | 19 | var chatRequest = new OpenAiChat 20 | { 21 | Model = model, 22 | Messages = 23 | [ 24 | new() { Role = "system", Content = TestUtils.SystemPrompt }, 25 | new() { Role = "user", Content = "How can I reverse a string in JavaScript?" }, 26 | ], 27 | Temperature = 0.7, 28 | MaxTokens = 2048, 29 | Stream = false, 30 | }; 31 | 32 | var openApiChatEndpoint = "https://supermicro.pvq.app/v1/chat/completions"; 33 | var responseJson = await openApiChatEndpoint.PostJsonToUrlAsync(chatRequest); 34 | 35 | var response = responseJson.FromJson(); 36 | ClientConfig.ToSystemJson(response).Print(); 37 | 38 | $"\n\n{response.Choices?.FirstOrDefault()?.Message?.Content}".Print(); 39 | } 40 | 41 | [Test] 42 | public async Task Can_send_an_image_to_ollama() 43 | { 44 | Environment.SetEnvironmentVariable("DOTNET_SYSTEM_NET_DISABLEIPV6", "1"); 45 | var ollamaGenerateEndpoint = "https://supermicro.pvq.app/api/generate"; 46 | // var ollamaGenerateEndpoint = "http://localhost:11434/api/generate"; 47 | 48 | // var model = "gemma3:27b"; 49 | var model = "gemma3:4b"; 50 | 51 | var imgBytes = await File.ReadAllBytesAsync("/home/mythz/Downloads/test3.png"); 52 | 53 | var chatRequest = new OllamaGenerate 54 | { 55 | Model = model, 56 | Prompt = "Describe this image", 57 | Images = [Convert.ToBase64String(imgBytes)], 58 | Stream = false, 59 | }; 60 | 61 | var responseJson = await ollamaGenerateEndpoint.PostJsonToUrlAsync(chatRequest); 62 | 63 | // responseJson.Print(); 64 | 65 | var response = responseJson.FromJson(); 66 | // ClientConfig.ToSystemJson(response).Print(); 67 | 68 | $"\n\n{response.Response}".Print(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /AiServer.Tests/OpenAiChatTests.cs: -------------------------------------------------------------------------------- 1 | using AiServer.ServiceModel; 2 | using NUnit.Framework; 3 | using ServiceStack; 4 | using ServiceStack.Text; 5 | 6 | namespace AiServer.Tests; 7 | 8 | [Explicit] 9 | public class OpenAiChatTests 10 | { 11 | [Test] 12 | public async Task Can_send_adhoc_OpenAiChatRequest() 13 | { 14 | var json = """ 15 | { 16 | "messages": [ 17 | { 18 | "content": "You are a friendly AI Assistant that helps answer developer questions. Think step by step and assist the user with their question, ensuring that your answer is relevant, on topic and provides actionable advice with code examples as appropriate.", 19 | "role": "system" 20 | }, 21 | { 22 | "content": "I'm learning C#. From what I know, you have to set things up correctly to have the garbage collector actually delete everything as it should be. I'm looking for wisdom learned over the years from you, the intelligent.\r\n\r\nI'm coming from a C++ background and am VERY used to code-smells and development patterns. I want to learn what code-smells are like in C#. Give me advice!\r\n\r\nWhat are the best ways to get things deleted?\r\n\r\nHow can you figure out when you have \"memory leaks\"?\r\n\r\nI am trying to develop a punch-list of \"stuff to always do for memory management\"", 23 | "role": "user" 24 | } 25 | ], 26 | "model": "codellama", 27 | "max_tokens": 2048, 28 | "temperature": 0.7 29 | } 30 | """; 31 | 32 | var baseUrl = "https://macbook.pvq.app"; 33 | var response = await baseUrl.CombineWith("/v1/chat/completions").PostJsonToUrlAsync(json); 34 | response.Print(); 35 | } 36 | 37 | [Test] 38 | public async Task Can_send_open_ai_compatible_request_AiServer() 39 | { 40 | var apiClient = new JsonApiClient("https://localhost:5005"); 41 | apiClient = TestUtils.IgnoreSslErrors(apiClient); 42 | apiClient.BearerToken = Environment.GetEnvironmentVariable("AI_SERVER_API_KEY"); 43 | 44 | var response = await apiClient.PostAsync("/v1/chat/completions", new OpenAiChat() 45 | { 46 | Model = "llama3:8b", 47 | Messages = [ 48 | new() { Role = "system", Content = "You are a helpful AI assistant." }, 49 | new() { Role = "user", Content = "How do LLMs work?" } 50 | ], 51 | MaxTokens = 50 52 | }); 53 | 54 | response.PrintDump(); 55 | } 56 | } -------------------------------------------------------------------------------- /AiServer.Tests/PingTests.cs: -------------------------------------------------------------------------------- 1 | using AiServer.ServiceModel; 2 | using NUnit.Framework; 3 | using ServiceStack; 4 | using ServiceStack.Text; 5 | 6 | namespace AiServer.Tests; 7 | 8 | [Explicit] 9 | public class PingTests 10 | { 11 | [Test] 12 | public async Task Can_check_all_local_providers() 13 | { 14 | var client = TestUtils.CreateAdminClient(); 15 | await CheckAllActiveProviders(client); 16 | } 17 | 18 | [Test] 19 | public async Task Can_check_all_public_providers() 20 | { 21 | var client = TestUtils.CreatePublicAdminClient(); 22 | await CheckAllActiveProviders(client); 23 | } 24 | 25 | private static async Task CheckAllActiveProviders(JsonApiClient client) 26 | { 27 | var activeProviders = await client.GetAsync(new GetActiveProviders()); 28 | 29 | foreach (var provider in activeProviders.Results) 30 | { 31 | var model = provider.Models.First().Model; 32 | $"Checking {provider.Name} {model}...".Print(); 33 | 34 | var request = new OpenAiChat 35 | { 36 | Model = model, 37 | Messages = [ 38 | new() { Role = "user", Content = "1+1=" }, 39 | ], 40 | MaxTokens = 2, 41 | Stream = false, 42 | }; 43 | 44 | var api = await client.ApiAsync(new ChatAiProvider 45 | { 46 | Provider = provider.Name, 47 | Model = model, 48 | Request = request, 49 | }); 50 | 51 | api.Error?.PrintDump(); 52 | 53 | var body = api.Response.GetBody(); 54 | 55 | $"{provider.Name} {model} says 1+1={body}\n".Print(); 56 | Assert.That(body, Is.Not.Null); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /AiServer.Tests/SpeechToTextTests.cs: -------------------------------------------------------------------------------- 1 | using AiServer.ServiceModel; 2 | using NUnit.Framework; 3 | using ServiceStack; 4 | using ServiceStack.Jobs; 5 | 6 | namespace AiServer.Tests; 7 | 8 | [Explicit("Integration tests require a running service")] 9 | public class SpeechToTextIntegrationTests : IntegrationTestBase 10 | { 11 | private const string TestAudioPath = "files/speech_to_text_test.wav"; 12 | 13 | [Test] 14 | public async Task Can_transcribe_speech() 15 | { 16 | var client = CreateClient(); 17 | 18 | TextGenerationResponse? response = null; 19 | await using var fileStream = new FileStream(TestAudioPath, FileMode.Open); 20 | 21 | try 22 | { 23 | response = client.PostFilesWithRequest(new SpeechToText { 24 | }, [new UploadFile("speech.wav", fileStream) { FieldName = "audio"}]); 25 | } 26 | catch (Exception e) 27 | { 28 | Assert.Fail(e.Message); 29 | } 30 | 31 | Assert.That(response, Is.Not.Null); 32 | Assert.That(response, Is.Not.Null); 33 | 34 | Assert.That(response.Results, Is.Not.Null); 35 | Assert.That(response.Results, Is.Not.Empty); 36 | } 37 | 38 | [Test] 39 | public async Task Can_generate_speech() 40 | { 41 | var client = CreateClient(); 42 | 43 | ApiResult? response = null; 44 | try 45 | { 46 | response = await client.ApiAsync(new TextToSpeech 47 | { 48 | Input = "This is a test of synchronous text to speech generation." 49 | }); 50 | response.ThrowIfError(); 51 | } 52 | catch (Exception e) 53 | { 54 | Assert.Fail(e.Message); 55 | } 56 | 57 | Assert.That(response, Is.Not.Null); 58 | Assert.That(response.Response, Is.Not.Null); 59 | 60 | Assert.That(response.Response.Results, Is.Not.Null); 61 | Assert.That(response.Response.Results, Is.Not.Empty); 62 | Assert.That(response.Response.Results[0].FileName, Does.EndWith(".mp3").Or.EndWith(".wav")); 63 | } 64 | 65 | } -------------------------------------------------------------------------------- /AiServer.Tests/TextToImageTests.cs: -------------------------------------------------------------------------------- 1 | using AiServer.ServiceModel; 2 | using NUnit.Framework; 3 | using ServiceStack; 4 | using ServiceStack.Jobs; 5 | using SixLabors.ImageSharp; 6 | 7 | namespace AiServer.Tests; 8 | 9 | [Explicit] 10 | public class TextToImageIntegrationTests : IntegrationTestBase 11 | { 12 | // [Test] 13 | // public async Task Create_Local_ApiKeys() 14 | // { 15 | // ConfigureSecrets.ApplySecrets(); 16 | // var client = TestUtils.CreateAuthSecretClient(); 17 | // await PvqApiTests.CreateApiKeys(client); 18 | // } 19 | 20 | [Test] 21 | public async Task Can_generate_image() 22 | { 23 | var client = CreateClient(); 24 | 25 | ApiResult? response = null; 26 | try 27 | { 28 | response = await client.ApiAsync(new TextToImage 29 | { 30 | PositivePrompt = "A serene landscape with mountains and a lake", 31 | Model = "flux-schnell", 32 | Width = 1024, 33 | Height = 1024, 34 | BatchSize = 1 35 | }); 36 | response.ThrowIfError(); 37 | } 38 | catch (Exception e) 39 | { 40 | Assert.Fail(e.Message); 41 | } 42 | 43 | Assert.That(response, Is.Not.Null); 44 | Assert.That(response.Response, Is.Not.Null); 45 | 46 | Assert.That(response.Response.Results, Is.Not.Null); 47 | Assert.That(response.Response.Results, Is.Not.Empty); 48 | 49 | // Validate that the output image is a valid image 50 | var outputImage = response.Response.Results[0]; 51 | Assert.That(outputImage.FileName, Does.EndWith(".webp")); 52 | // Download the image 53 | 54 | var downloadResponse = await client.GetHttpClient().GetStreamAsync(outputImage.Url); 55 | Assert.That(downloadResponse, Is.Not.Null); 56 | 57 | // Load the image 58 | var outputImageBytes = downloadResponse.ReadFully(); 59 | using var outputImageStream = new MemoryStream(outputImageBytes); 60 | var outputImageInfo = Image.Load(outputImageStream); 61 | 62 | Assert.That(outputImageInfo, Is.Not.Null); 63 | Assert.That(outputImageInfo.Width, Is.GreaterThan(0)); 64 | Assert.That(outputImageInfo.Height, Is.GreaterThan(0)); 65 | Assert.That(outputImageInfo.Width, Is.EqualTo(1024)); 66 | Assert.That(outputImageInfo.Height, Is.EqualTo(1024)); 67 | 68 | } 69 | } -------------------------------------------------------------------------------- /AiServer.Tests/Types/Post.cs: -------------------------------------------------------------------------------- 1 | using ServiceStack; 2 | using ServiceStack.DataAnnotations; 3 | 4 | namespace AiServer.Tests.Types; 5 | 6 | [ValidateIsAuthenticated] 7 | public class GetQuestion : IGet, IReturn 8 | { 9 | public int Id { get; set; } 10 | } 11 | public class GetQuestionResponse 12 | { 13 | public required Post Result { get; set; } 14 | public ResponseStatus? ResponseStatus { get; set; } 15 | } 16 | [ValidateIsAuthenticated] 17 | public class GetQuestionBody : IGet, IReturn 18 | { 19 | public int Id { get; set; } 20 | } 21 | 22 | [Api("StackOverflow Question")] 23 | [Notes("A StackOverflow Question Post")] 24 | public class Post 25 | { 26 | public int Id { get; set; } 27 | 28 | [Required] public int PostTypeId { get; set; } 29 | 30 | public int? AcceptedAnswerId { get; set; } 31 | 32 | public int? ParentId { get; set; } 33 | 34 | public int Score { get; set; } 35 | 36 | public int? ViewCount { get; set; } 37 | 38 | public string Title { get; set; } 39 | 40 | public int? FavoriteCount { get; set; } 41 | 42 | public DateTime CreationDate { get; set; } 43 | 44 | public DateTime LastActivityDate { get; set; } 45 | 46 | public DateTime? LastEditDate { get; set; } 47 | 48 | public int? LastEditorUserId { get; set; } 49 | 50 | public int? OwnerUserId { get; set; } 51 | 52 | public List Tags { get; set; } 53 | 54 | public string Slug { get; set; } 55 | 56 | public string Summary { get; set; } 57 | 58 | public DateTime? RankDate { get; set; } 59 | 60 | public int? AnswerCount { get; set; } 61 | 62 | public string? CreatedBy { get; set; } 63 | 64 | public string? ModifiedBy { get; set; } 65 | 66 | public string? RefId { get; set; } 67 | 68 | public string? Body { get; set; } 69 | 70 | public string? ModifiedReason { get; set; } 71 | 72 | public DateTime? LockedDate { get; set; } 73 | 74 | public string? LockedReason { get; set; } 75 | 76 | public string GetRefId() => RefId ?? $"{Id}-{CreatedBy}"; 77 | } -------------------------------------------------------------------------------- /AiServer.Tests/UnitTest.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using ServiceStack; 3 | using ServiceStack.Testing; 4 | using AiServer.ServiceInterface; 5 | using AiServer.ServiceModel; 6 | using ServiceStack.Text; 7 | 8 | namespace AiServer.Tests; 9 | 10 | public class UnitTest 11 | { 12 | private readonly ServiceStackHost appHost; 13 | 14 | public UnitTest() 15 | { 16 | appHost = new BasicAppHost().Init(); 17 | appHost.Container.AddTransient(); 18 | } 19 | 20 | [OneTimeTearDown] 21 | public void OneTimeTearDown() => appHost.Dispose(); 22 | 23 | [Test] 24 | public void Can_call_MyServices() 25 | { 26 | var service = appHost.Container.Resolve(); 27 | 28 | var response = (HelloResponse)service.Any(new Hello { Name = "World" }); 29 | 30 | Assert.That(response.Result, Is.EqualTo("Hello, World!")); 31 | } 32 | } -------------------------------------------------------------------------------- /AiServer.Tests/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "HostDir": "../../../../AiServer", 3 | "ClientDir": "../../../../AiServer" 4 | } -------------------------------------------------------------------------------- /AiServer.Tests/files/comfyui_upload_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer.Tests/files/comfyui_upload_test.png -------------------------------------------------------------------------------- /AiServer.Tests/files/comfyui_upload_test_mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer.Tests/files/comfyui_upload_test_mask.png -------------------------------------------------------------------------------- /AiServer.Tests/files/speech_to_text_test.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer.Tests/files/speech_to_text_test.wav -------------------------------------------------------------------------------- /AiServer.Tests/files/test_audio.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer.Tests/files/test_audio.mp3 -------------------------------------------------------------------------------- /AiServer.Tests/files/test_audio.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer.Tests/files/test_audio.wav -------------------------------------------------------------------------------- /AiServer.Tests/files/test_image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer.Tests/files/test_image.jpg -------------------------------------------------------------------------------- /AiServer.Tests/files/test_video.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer.Tests/files/test_video.mp4 -------------------------------------------------------------------------------- /AiServer.Tests/files/test_video.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer.Tests/files/test_video.webm -------------------------------------------------------------------------------- /AiServer.Tests/files/watermark_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer.Tests/files/watermark_image.png -------------------------------------------------------------------------------- /AiServer.Tests/workflows/text_to_speech.json: -------------------------------------------------------------------------------- 1 | { 2 | "last_node_id": 22, 3 | "last_link_id": 16, 4 | "nodes": [ 5 | { 6 | "id": 16, 7 | "type": "PiperTTS", 8 | "pos": [ 9 | 370, 10 | 220 11 | ], 12 | "size": [ 13 | 350, 14 | 250 15 | ], 16 | "flags": {}, 17 | "order": 0, 18 | "mode": 0, 19 | "outputs": [ 20 | { 21 | "name": "audio_path", 22 | "type": "STRING", 23 | "links": [], 24 | "shape": 3, 25 | "slot_index": 0 26 | } 27 | ], 28 | "properties": { 29 | "Node name for S&R": "PiperTTS" 30 | }, 31 | "widgets_values": [ 32 | "{{PositivePrompt}}", 33 | "{{Model.split(':')[1]}}", 34 | "{{Model.split(':')[0]}}", 35 | { 36 | "hidden": false, 37 | "paused": false, 38 | "params": { 39 | "previews": [ 40 | { 41 | "filename": "temp123.wav", 42 | "subfolder": "piper_tts", 43 | "type": "output" 44 | } 45 | ] 46 | } 47 | } 48 | ] 49 | } 50 | ], 51 | "links": [], 52 | "groups": [], 53 | "config": {}, 54 | "extra": { 55 | "ds": { 56 | "scale": 0.9090909090909091, 57 | "offset": { 58 | "0": 233.45999145507812, 59 | "1": 80.5 60 | } 61 | } 62 | }, 63 | "version": 0.4 64 | } -------------------------------------------------------------------------------- /AiServer.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26730.3 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AiServer", "AiServer\AiServer.csproj", "{5F817400-1A3A-48DF-98A6-E7E5A3DC762F}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AiServer.ServiceInterface", "AiServer.ServiceInterface\AiServer.ServiceInterface.csproj", "{5B8FFF01-1E0B-477D-9D7F-93016C128B23}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AiServer.ServiceModel", "AiServer.ServiceModel\AiServer.ServiceModel.csproj", "{0127B6CA-1B79-46A6-8307-B36836D107F0}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AiServer.Tests", "AiServer.Tests\AiServer.Tests.csproj", "{455EC1EF-134F-4CD4-9C78-E813E4E6D8F6}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {5F817400-1A3A-48DF-98A6-E7E5A3DC762F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {5F817400-1A3A-48DF-98A6-E7E5A3DC762F}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {5F817400-1A3A-48DF-98A6-E7E5A3DC762F}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {5F817400-1A3A-48DF-98A6-E7E5A3DC762F}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {5B8FFF01-1E0B-477D-9D7F-93016C128B23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {5B8FFF01-1E0B-477D-9D7F-93016C128B23}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {5B8FFF01-1E0B-477D-9D7F-93016C128B23}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {5B8FFF01-1E0B-477D-9D7F-93016C128B23}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {0127B6CA-1B79-46A6-8307-B36836D107F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {0127B6CA-1B79-46A6-8307-B36836D107F0}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {0127B6CA-1B79-46A6-8307-B36836D107F0}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {0127B6CA-1B79-46A6-8307-B36836D107F0}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {455EC1EF-134F-4CD4-9C78-E813E4E6D8F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {455EC1EF-134F-4CD4-9C78-E813E4E6D8F6}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {455EC1EF-134F-4CD4-9C78-E813E4E6D8F6}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {455EC1EF-134F-4CD4-9C78-E813E4E6D8F6}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(ExtensibilityGlobals) = postSolution 41 | SolutionGuid = {02854F2A-8EF4-468E-80A3-CD64BBAF5D15} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /AiServer/AiServer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | DefaultContainer 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /AiServer/Configure.Auth.cs: -------------------------------------------------------------------------------- 1 | using ServiceStack.Auth; 2 | using AiServer.ServiceInterface; 3 | using ServiceStack.Html; 4 | 5 | [assembly: HostingStartup(typeof(ConfigureAuth))] 6 | 7 | namespace AiServer; 8 | 9 | public class ConfigureAuth : IHostingStartup 10 | { 11 | public void Configure(IWebHostBuilder builder) => builder 12 | .ConfigureServices(services => 13 | { 14 | services.AddPlugin(new AuthFeature([ 15 | new ApiKeyCredentialsProvider(), 16 | new AuthSecretAuthProvider(AppConfig.Instance.AuthSecret), 17 | ])); 18 | services.AddPlugin(new SessionFeature()); 19 | services.AddPlugin(new ApiKeysFeature { 20 | }); 21 | }) 22 | .ConfigureAppHost(appHost => 23 | { 24 | using var db = HostContext.AppHost.GetDbConnection(); 25 | appHost.GetPlugin().InitSchema(db); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /AiServer/Configure.AutoQuery.cs: -------------------------------------------------------------------------------- 1 | [assembly: HostingStartup(typeof(ConfigureAutoQuery))] 2 | 3 | namespace AiServer; 4 | 5 | public class ConfigureAutoQuery : IHostingStartup 6 | { 7 | public void Configure(IWebHostBuilder builder) => builder 8 | .ConfigureServices(services => { 9 | 10 | services.AddPlugin(new AutoQueryFeature { 11 | MaxLimit = 1000, 12 | //IncludeTotal = true, 13 | }); 14 | 15 | services.AddPlugin(new AutoQueryDataFeature()); 16 | 17 | //services.AddPlugin(new AutoQueryDataFeature()); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /AiServer/Configure.Db.Migrations.cs: -------------------------------------------------------------------------------- 1 | using AiServer.Migrations; 2 | using ServiceStack.Data; 3 | using ServiceStack.OrmLite; 4 | 5 | [assembly: HostingStartup(typeof(AiServer.ConfigureDbMigrations))] 6 | 7 | namespace AiServer; 8 | 9 | // Code-First DB Migrations: https://docs.servicestack.net/ormlite/db-migrations 10 | public class ConfigureDbMigrations : IHostingStartup 11 | { 12 | public void Configure(IWebHostBuilder builder) => builder 13 | .ConfigureAppHost(appHost => { 14 | var migrator = new Migrator(appHost.Resolve(), typeof(Migration1001).Assembly); 15 | AppTasks.Register("migrate", _ => 16 | { 17 | var log = appHost.GetApplicationServices().GetRequiredService>(); 18 | log.LogInformation("Running OrmLite Migrations..."); 19 | migrator.Run(); 20 | }); 21 | AppTasks.Register("migrate.revert", args => migrator.Revert(args[0])); 22 | AppTasks.Run(); 23 | }); 24 | 25 | } 26 | -------------------------------------------------------------------------------- /AiServer/Configure.Db.cs: -------------------------------------------------------------------------------- 1 | using ServiceStack.Data; 2 | using ServiceStack.OrmLite; 3 | using ServiceStack.Text; 4 | 5 | [assembly: HostingStartup(typeof(AiServer.ConfigureDb))] 6 | 7 | namespace AiServer; 8 | 9 | public class ConfigureDb : IHostingStartup 10 | { 11 | public void Configure(IWebHostBuilder builder) => builder 12 | .ConfigureServices((context, services) => { 13 | SqliteDialect.Provider.StringSerializer = new JsonStringSerializer(); 14 | 15 | var connectionString = context.Configuration.GetConnectionString("DefaultConnection") 16 | ?? "DataSource=App_Data/app.db;Cache=Shared"; 17 | 18 | // Use UTC for all DateTime's stored + retrieved in SQLite 19 | var dateConverter = SqliteDialect.Provider.GetDateTimeConverter(); 20 | dateConverter.DateStyle = DateTimeKind.Utc; 21 | 22 | var dbFactory = new OrmLiteConnectionFactory(connectionString, SqliteDialect.Provider); 23 | services.AddSingleton(dbFactory); 24 | 25 | var monthDb = dbFactory.GetNamedMonthDb(); 26 | 27 | dbFactory.RegisterConnection(monthDb, 28 | $"DataSource=App_Data/{monthDb}/app.db;Cache=Shared", SqliteDialect.Provider); 29 | 30 | // Enable built-in Database Admin UI at /admin-ui/database 31 | services.AddPlugin(new AdminDatabaseFeature 32 | { 33 | SchemasFilter = columns => columns.ForEach(x => x.Tables.Sort()) 34 | }); 35 | 36 | AppHost.GetDbMonthConnection(dbFactory, context.HostingEnvironment.ContentRootPath, monthDb); 37 | }); 38 | } -------------------------------------------------------------------------------- /AiServer/Configure.HealthChecks.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Diagnostics.HealthChecks; 2 | 3 | [assembly: HostingStartup(typeof(AiServer.HealthChecks))] 4 | 5 | namespace AiServer; 6 | 7 | public class HealthChecks : IHostingStartup 8 | { 9 | public class HealthCheck : IHealthCheck 10 | { 11 | public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken token = default) 12 | { 13 | // Perform health check logic here 14 | return HealthCheckResult.Healthy(); 15 | } 16 | } 17 | 18 | public void Configure(IWebHostBuilder builder) 19 | { 20 | builder.ConfigureServices(services => 21 | { 22 | services.AddHealthChecks() 23 | .AddCheck("HealthCheck"); 24 | 25 | services.AddTransient(); 26 | }); 27 | } 28 | 29 | public class StartupFilter : IStartupFilter 30 | { 31 | public Action Configure(Action next) 32 | => app => { 33 | app.UseHealthChecks("/up"); 34 | next(app); 35 | }; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /AiServer/Configure.Profiling.cs: -------------------------------------------------------------------------------- 1 | using ServiceStack; 2 | 3 | [assembly: HostingStartup(typeof(AiServer.ConfigureProfiling))] 4 | 5 | namespace AiServer; 6 | 7 | public class ConfigureProfiling : IHostingStartup 8 | { 9 | public void Configure(IWebHostBuilder builder) => builder 10 | .ConfigureServices((context, services) => { 11 | if (context.HostingEnvironment.IsDevelopment()) 12 | { 13 | services.AddPlugin(new ProfilingFeature 14 | { 15 | IncludeStackTrace = true, 16 | }); 17 | } 18 | }); 19 | } -------------------------------------------------------------------------------- /AiServer/Configure.RequestLogs.cs: -------------------------------------------------------------------------------- 1 | using ServiceStack.Jobs; 2 | using ServiceStack.Web; 3 | 4 | [assembly: HostingStartup(typeof(AiServer.ConfigureRequestLogs))] 5 | 6 | namespace AiServer; 7 | 8 | public class ConfigureRequestLogs : IHostingStartup 9 | { 10 | public void Configure(IWebHostBuilder builder) => builder 11 | .ConfigureServices((context, services) => { 12 | 13 | services.AddPlugin(new RequestLogsFeature { 14 | RequestLogger = new SqliteRequestLogger(), 15 | // EnableResponseTracking = true, 16 | EnableRequestBodyTracking = true, 17 | EnableErrorTracking = true 18 | }); 19 | services.AddHostedService(); 20 | }); 21 | } 22 | 23 | public class RequestLogsHostedService(ILogger log, IRequestLogger requestLogger) : BackgroundService 24 | { 25 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 26 | { 27 | var dbRequestLogger = (SqliteRequestLogger)requestLogger; 28 | using var timer = new PeriodicTimer(TimeSpan.FromSeconds(3)); 29 | while (!stoppingToken.IsCancellationRequested && await timer.WaitForNextTickAsync(stoppingToken)) 30 | { 31 | dbRequestLogger.Tick(log); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /AiServer/Program.cs: -------------------------------------------------------------------------------- 1 | using AiServer.ServiceInterface; 2 | using Microsoft.AspNetCore.Server.Kestrel.Core; 3 | 4 | Licensing.RegisterLicense("OSS GPL-3.0 2025 https://github.com/ServiceStack/ai-server B2fSVlQ1mYLSxRYTSvsS1aORN0Og++8DTDsxY0+2lBt8Wj7VwLZYbHJY4/UnJFpaagxoQepeXXHMPfZcmP9eUjyhRaqWe3OJI4+3ct/2Wr+rfR5roBrUer8mzJhrQDj1t3L3x42dy/pZiOQKMccAShk4psGLS/TG86MTzuPk2XE="); 5 | 6 | var builder = WebApplication.CreateBuilder(args); 7 | var services = builder.Services; 8 | 9 | services.Configure(options => { 10 | options.Limits.MaxRequestBodySize = int.MaxValue; // if don't set default value is: 30 MB 11 | options.Limits.MaxRequestBodySize = 50 * 1024 * 1024; //50MB 12 | }); 13 | 14 | services.AddServiceStack(typeof(MyServices).Assembly); 15 | 16 | var app = builder.Build(); 17 | 18 | // Configure the HTTP request pipeline. 19 | if (!app.Environment.IsDevelopment()) 20 | { 21 | app.UseExceptionHandler("/Error", createScopeForErrors: true); 22 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 23 | app.UseHsts(); 24 | } 25 | 26 | app.UseStaticFiles(); 27 | app.MapGet("/admin/{**path}", (IWebHostEnvironment env) => 28 | Results.File(env.WebRootPath.CombineWith("/admin/index.html"), "text/html")); 29 | 30 | app.UseServiceStack(new AppHost(), options => { 31 | options.MapEndpoints(); 32 | }); 33 | 34 | app.MapFallbackToFile("index.html"); 35 | 36 | app.Run(); 37 | -------------------------------------------------------------------------------- /AiServer/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "https://localhost:5001/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "AiServer": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "https://localhost:5005/;http://localhost:5006/" 25 | }, 26 | "AiServer-Migrate": { 27 | "commandName": "Project", 28 | "launchBrowser": true, 29 | "environmentVariables": { 30 | "ASPNETCORE_ENVIRONMENT": "Development", 31 | "REPLICATE_API_KEY": "example" 32 | }, 33 | "applicationUrl": "https://localhost:5005/", 34 | "commandLineArgs": "--AppTasks=migrate" 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /AiServer/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "DetailedErrors": true, 3 | "Logging": { 4 | "LogLevel": { 5 | "Default": "Information", 6 | "Microsoft.AspNetCore": "Warning" 7 | } 8 | }, 9 | "AppConfig": { 10 | "AssetsBaseUrl": "http://localhost:5006" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /AiServer/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "DetailedErrors": true, 3 | "Logging": { 4 | "LogLevel": { 5 | "Default": "Information", 6 | "Microsoft.AspNetCore": "Warning" 7 | } 8 | }, 9 | "AllowedHosts": "*", 10 | "AppConfig": { 11 | "AuthSecret": "p@55wOrd", 12 | "ArtifactsPath": "App_Data/artifacts", 13 | "FilesPath": "App_Data/files", 14 | "DefaultModel": { 15 | "Name": "SDXL Lightning - 4 Steps", 16 | "DownloadUrl": "https://huggingface.co/ByteDance/SDXL-Lightning/resolve/main/sdxl_lightning_4step.safetensors?download=true", 17 | "Url": "https://civitai.com/models/350352?modelVersionId=391971", 18 | "Filename": "sdxlLightning_4Steps.safetensors" 19 | }, 20 | "DefaultModelSettings": { 21 | "CfgScale": "1.0", 22 | "Steps": 4, 23 | "Scheduler": "sgm_uniform", 24 | "Sampler": "euler", 25 | "Width": 1024, 26 | "Height": 1024, 27 | "Seed": 42 28 | }, 29 | "AssetsBaseUrl": "https://ai-server-cdn.diffusion.works" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /AiServer/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "AiServer", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": {} 6 | } 7 | -------------------------------------------------------------------------------- /AiServer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "postinstall": "node postinstall.js && npm run migrate", 4 | "dtos": "x mjs", 5 | "dev": "dotnet watch", 6 | "ui:dev": "npx --yes tailwindcss@v3 -i ./tailwind.input.css -o ./wwwroot/css/app.css --watch", 7 | "ui:build": "npx --yes tailwindcss@v3 -i ./tailwind.input.css -o ./wwwroot/css/app.css --minify", 8 | "build": "npm run ui:build", 9 | "migrate": "dotnet run --AppTasks=migrate", 10 | "revert:last": "dotnet run --AppTasks=migrate.revert:last", 11 | "revert:all": "dotnet run --AppTasks=migrate.revert:all", 12 | "rerun:last": "npm run revert:last && npm run migrate" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /AiServer/seed/media-providers.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "Replicate", 4 | "mediaTypeId": "Replicate", 5 | "apiKeyVar": "REPLICATE_API_KEY", 6 | "HeartbeatUrl": "/", 7 | "concurrency": 1, 8 | "priority": 0, 9 | "enabled": true, 10 | "models": [ 11 | "black-forest-labs/flux-schnell", 12 | "black-forest-labs/flux-dev", 13 | "black-forest-labs/flux-pro", 14 | "stability-ai/stable-diffusion-3" 15 | ] 16 | }, 17 | { 18 | "id": "ComfyUI", 19 | "mediaTypeId": "ComfyUI", 20 | "apiKeyVar": "COMFY_API_KEY", 21 | "apiUrlVar": "COMFY_API_URL", 22 | "HeartbeatUrl": "/", 23 | "concurrency": 1, 24 | "priority": 0, 25 | "enabled": true 26 | }, 27 | { 28 | "id": "OpenAI", 29 | "mediaTypeId": "OpenAI", 30 | "apiKeyVar": "OPENAI_API_KEY", 31 | "HeartbeatUrl": "/", 32 | "concurrency": 1, 33 | "priority": 0, 34 | "enabled": true, 35 | "models": [ 36 | "dall-e-3" 37 | ] 38 | } 39 | ] -------------------------------------------------------------------------------- /AiServer/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ["./**/*.{html,js,mjs,md,cshtml,razor,cs}","./Pages/**/*.{cshtml,razor}","./Css.cs"], 3 | darkMode: 'class', 4 | theme: { 5 | extend: { 6 | colors: { 7 | 'accent-1': '#FAFAFA', 8 | 'accent-2': '#EAEAEA', 9 | danger: 'rgb(153 27 27)', 10 | success: 'rgb(22 101 52)', 11 | }, 12 | }, 13 | }, 14 | plugins: [], 15 | } 16 | -------------------------------------------------------------------------------- /AiServer/workflows/text_to_speech.json: -------------------------------------------------------------------------------- 1 | { 2 | "last_node_id": 22, 3 | "last_link_id": 16, 4 | "nodes": [ 5 | { 6 | "id": 16, 7 | "type": "PiperTTS", 8 | "pos": [ 9 | 370, 10 | 220 11 | ], 12 | "size": [ 13 | 350, 14 | 250 15 | ], 16 | "flags": {}, 17 | "order": 0, 18 | "mode": 0, 19 | "outputs": [ 20 | { 21 | "name": "audio_path", 22 | "type": "STRING", 23 | "links": [], 24 | "shape": 3, 25 | "slot_index": 0 26 | } 27 | ], 28 | "properties": { 29 | "Node name for S&R": "PiperTTS" 30 | }, 31 | "widgets_values": [ 32 | "{{positivePrompt}}", 33 | "{{model.split(':')[1]}}", 34 | "{{model.split(':')[0]}}", 35 | { 36 | "hidden": false, 37 | "paused": false, 38 | "params": { 39 | "previews": [ 40 | { 41 | "filename": "temp123.wav", 42 | "subfolder": "piper_tts", 43 | "type": "output" 44 | } 45 | ] 46 | } 47 | } 48 | ] 49 | } 50 | ], 51 | "links": [], 52 | "groups": [], 53 | "config": {}, 54 | "extra": { 55 | "ds": { 56 | "scale": 0.9090909090909091, 57 | "offset": { 58 | "0": 233.45999145507812, 59 | "1": 80.5 60 | } 61 | } 62 | }, 63 | "version": 0.4 64 | } -------------------------------------------------------------------------------- /AiServer/wwwroot/css/highlight.css: -------------------------------------------------------------------------------- 1 | /* highlight.js - vs.css */ 2 | .hljs {background:white;color:black} 3 | .hljs-comment,.hljs-quote,.hljs-variable{color:#008000} 4 | .hljs-keyword,.hljs-selector-tag,.hljs-built_in,.hljs-name,.hljs-tag{color:#00f} 5 | .hljs-string,.hljs-title,.hljs-section,.hljs-attribute,.hljs-literal,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-addition{color:#a31515} 6 | .hljs-deletion,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-meta{color:#2b91af} 7 | .hljs-doctag{color:#808080} 8 | .hljs-attr{color: #f00} 9 | .hljs-symbol,.hljs-bullet,.hljs-link{color:#00b0e8} 10 | .hljs-emphasis{font-style:italic} 11 | .hljs-strong{font-weight:bold} 12 | 13 | /* https://unpkg.com/@highlightjs/cdn-assets/styles/atom-one-dark.min.css */ 14 | pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#abb2bf;background:#282c34} 15 | .hljs-comment,.hljs-quote{color:#5c6370;font-style:italic}.hljs-doctag,.hljs-formula,.hljs-keyword{color:#c678dd} 16 | .hljs-deletion,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-subst,.hljs-tag{color:#e06c75} 17 | .hljs-literal{color:#56b6c2} 18 | .hljs-addition,.hljs-attribute,.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#98c379} 19 | .hljs-attr,.hljs-number,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-pseudo,.hljs-template-variable,.hljs-type,.hljs-variable{color:#d19a66} 20 | .hljs-bullet,.hljs-link,.hljs-meta,.hljs-selector-id,.hljs-symbol,.hljs-title{color:#61aeee} 21 | .hljs-built_in,.hljs-class .hljs-title,.hljs-title.class_{color:#e6c07b}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700} 22 | .hljs-link{text-decoration:underline} 23 | 24 | /*highlightjs*/ 25 | .hljs, .prose :where(pre):not(:where([class~="not-prose"] *)) .hljs { 26 | color: #e5e7eb !important; 27 | background-color: #282c34 !important; 28 | } 29 | .hljs-comment, .hljs-quote { 30 | color: rgb(148 163 184); /*text-slate-400*/ 31 | } 32 | 33 | pre { 34 | overflow-x: auto; 35 | font-weight: 400; 36 | font-size: .875em; 37 | line-height: 1.7142857; 38 | margin-top: 1.7142857em; 39 | margin-bottom: 1.7142857em; 40 | border-radius: .375rem; 41 | padding: .8571429em 1.1428571em; 42 | max-width: calc(100vw - 1rem); 43 | min-width: fit-content; 44 | background-color: #282c34 !important; 45 | } 46 | pre code.hljs { 47 | display: block; 48 | overflow-x: auto; 49 | padding: 1em; 50 | } 51 | 52 | .message pre { 53 | max-width: 100%; 54 | min-width: auto; 55 | } 56 | .message pre code.hljs { 57 | overflow-x: unset; 58 | width: 100%; 59 | } -------------------------------------------------------------------------------- /AiServer/wwwroot/css/lite-yt-embed.css: -------------------------------------------------------------------------------- 1 | lite-youtube { 2 | background-color: #000; 3 | transition: all 0.2s cubic-bezier(0, 0, 0.2, 1); 4 | position: relative; 5 | display: block; 6 | contain: content; 7 | background-position: center center; 8 | background-size: cover; 9 | cursor: pointer; 10 | max-width: 720px; 11 | } 12 | 13 | lite-youtube:hover { 14 | transition: all 0.2s cubic-bezier(0, 0, 0.2, 1); 15 | } 16 | 17 | /* gradient */ 18 | lite-youtube::before { 19 | content: ''; 20 | display: block; 21 | position: absolute; 22 | top: 0; 23 | /*background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAADGCAYAAAAT+OqFAAAAdklEQVQoz42QQQ7AIAgEF/T/D+kbq/RWAlnQyyazA4aoAB4FsBSA/bFjuF1EOL7VbrIrBuusmrt4ZZORfb6ehbWdnRHEIiITaEUKa5EJqUakRSaEYBJSCY2dEstQY7AuxahwXFrvZmWl2rh4JZ07z9dLtesfNj5q0FU3A5ObbwAAAABJRU5ErkJggg==);*/ 24 | background-position: top; 25 | background-repeat: repeat-x; 26 | height: 60px; 27 | padding-bottom: 50px; 28 | width: 100%; 29 | transition: all 0.2s cubic-bezier(0, 0, 0.2, 1); 30 | } 31 | 32 | /* responsive iframe with a 16:9 aspect ratio 33 | thanks https://css-tricks.com/responsive-iframes/ 34 | */ 35 | lite-youtube::after { 36 | content: ""; 37 | display: block; 38 | padding-bottom: calc(100% / (16 / 9)); 39 | } 40 | lite-youtube > iframe { 41 | width: 100%; 42 | height: 100%; 43 | position: absolute; 44 | top: 0; 45 | left: 0; 46 | border: 0; 47 | } 48 | 49 | /* play button */ 50 | lite-youtube > .lty-playbtn { 51 | width: 60px; 52 | height: 42px; 53 | position: absolute; 54 | cursor: pointer; 55 | transform: translate3d(-50%, -50%, 0); 56 | color: white; 57 | top: 50%; 58 | left: 50%; 59 | z-index: 1; 60 | opacity: 30%; 61 | background: transparent url('data:image/svg+xml;utf8,') no-repeat; 62 | filter: grayscale(100%); 63 | border: none; 64 | } 65 | 66 | lite-youtube:hover > .lty-playbtn, 67 | lite-youtube .lty-playbtn:focus { 68 | filter: grayscale(0); 69 | transition: opacity 0.5s, filter .1s cubic-bezier(0, 0, 0.2, 1); 70 | -webkit-transition: opacity 0.5s; 71 | opacity: 100%; 72 | outline: none; 73 | } 74 | 75 | /* Post-click styles */ 76 | lite-youtube.lyt-activated { 77 | cursor: unset; 78 | } 79 | lite-youtube.lyt-activated::before, 80 | lite-youtube.lyt-activated > .lty-playbtn { 81 | opacity: 0; 82 | pointer-events: none; 83 | } 84 | 85 | .lyt-visually-hidden { 86 | clip: rect(0 0 0 0); 87 | clip-path: inset(50%); 88 | height: 1px; 89 | overflow: hidden; 90 | position: absolute; 91 | white-space: nowrap; 92 | width: 1px; 93 | } -------------------------------------------------------------------------------- /AiServer/wwwroot/img/langs/csharp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /AiServer/wwwroot/img/langs/dart.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | 20 | -------------------------------------------------------------------------------- /AiServer/wwwroot/img/langs/fsharp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /AiServer/wwwroot/img/langs/java.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /AiServer/wwwroot/img/langs/javascript.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /AiServer/wwwroot/img/langs/kotlin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /AiServer/wwwroot/img/langs/mjs.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /AiServer/wwwroot/img/langs/python.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /AiServer/wwwroot/img/langs/swift.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /AiServer/wwwroot/img/langs/typescript.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /AiServer/wwwroot/img/langs/vbnet.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /AiServer/wwwroot/img/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /AiServer/wwwroot/img/models/chatgpt.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /AiServer/wwwroot/img/models/codellama.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | C 9 | -------------------------------------------------------------------------------- /AiServer/wwwroot/img/models/codestral.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | C 20 | -------------------------------------------------------------------------------- /AiServer/wwwroot/img/models/command-r-plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /AiServer/wwwroot/img/models/command-r.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /AiServer/wwwroot/img/models/dbrx.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /AiServer/wwwroot/img/models/deepseek-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer/wwwroot/img/models/deepseek-3.jpg -------------------------------------------------------------------------------- /AiServer/wwwroot/img/models/deepseek-coder-v2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer/wwwroot/img/models/deepseek-coder-v2.jpg -------------------------------------------------------------------------------- /AiServer/wwwroot/img/models/deepseek-coder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer/wwwroot/img/models/deepseek-coder.jpg -------------------------------------------------------------------------------- /AiServer/wwwroot/img/models/deepseek.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer/wwwroot/img/models/deepseek.jpg -------------------------------------------------------------------------------- /AiServer/wwwroot/img/models/dolphin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer/wwwroot/img/models/dolphin.png -------------------------------------------------------------------------------- /AiServer/wwwroot/img/models/gemini-pro.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /AiServer/wwwroot/img/models/gemini.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /AiServer/wwwroot/img/models/gpt-3.5.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /AiServer/wwwroot/img/models/gpt-4.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /AiServer/wwwroot/img/models/llama.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /AiServer/wwwroot/img/models/mathstral.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | M 20 | -------------------------------------------------------------------------------- /AiServer/wwwroot/img/models/mistral-nemo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | NeMo 20 | 21 | -------------------------------------------------------------------------------- /AiServer/wwwroot/img/models/mistral.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /AiServer/wwwroot/img/models/mixtral.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer/wwwroot/img/models/mixtral.jpg -------------------------------------------------------------------------------- /AiServer/wwwroot/img/models/qwen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /AiServer/wwwroot/img/models/starcoder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer/wwwroot/img/models/starcoder.png -------------------------------------------------------------------------------- /AiServer/wwwroot/img/models/thudm.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer/wwwroot/img/models/thudm.webp -------------------------------------------------------------------------------- /AiServer/wwwroot/img/models/wizardlm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer/wwwroot/img/models/wizardlm.png -------------------------------------------------------------------------------- /AiServer/wwwroot/img/models/yi.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /AiServer/wwwroot/img/providers/anthropic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /AiServer/wwwroot/img/providers/comfyui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer/wwwroot/img/providers/comfyui.png -------------------------------------------------------------------------------- /AiServer/wwwroot/img/providers/custom.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /AiServer/wwwroot/img/providers/google-cloud.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /AiServer/wwwroot/img/providers/groq.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /AiServer/wwwroot/img/providers/mistral-ai.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /AiServer/wwwroot/img/providers/openai.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /AiServer/wwwroot/img/providers/openrouter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /AiServer/wwwroot/img/providers/replicate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /AiServer/wwwroot/img/uis/Chat.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer/wwwroot/img/uis/Chat.webp -------------------------------------------------------------------------------- /AiServer/wwwroot/img/uis/ImageToImage.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer/wwwroot/img/uis/ImageToImage.webp -------------------------------------------------------------------------------- /AiServer/wwwroot/img/uis/ImageToText.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer/wwwroot/img/uis/ImageToText.webp -------------------------------------------------------------------------------- /AiServer/wwwroot/img/uis/ImageUpscale.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer/wwwroot/img/uis/ImageUpscale.webp -------------------------------------------------------------------------------- /AiServer/wwwroot/img/uis/SpeechToText.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer/wwwroot/img/uis/SpeechToText.webp -------------------------------------------------------------------------------- /AiServer/wwwroot/img/uis/TextToImage.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer/wwwroot/img/uis/TextToImage.webp -------------------------------------------------------------------------------- /AiServer/wwwroot/img/uis/TextToSpeech.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer/wwwroot/img/uis/TextToSpeech.webp -------------------------------------------------------------------------------- /AiServer/wwwroot/img/uis/admin/ai-models.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer/wwwroot/img/uis/admin/ai-models.webp -------------------------------------------------------------------------------- /AiServer/wwwroot/img/uis/admin/ai-providers-new-ollama.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer/wwwroot/img/uis/admin/ai-providers-new-ollama.webp -------------------------------------------------------------------------------- /AiServer/wwwroot/img/uis/admin/ai-providers.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer/wwwroot/img/uis/admin/ai-providers.webp -------------------------------------------------------------------------------- /AiServer/wwwroot/img/uis/admin/ai-types.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer/wwwroot/img/uis/admin/ai-types.webp -------------------------------------------------------------------------------- /AiServer/wwwroot/img/uis/admin/analytics.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer/wwwroot/img/uis/admin/analytics.webp -------------------------------------------------------------------------------- /AiServer/wwwroot/img/uis/admin/api-keys-edit.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer/wwwroot/img/uis/admin/api-keys-edit.webp -------------------------------------------------------------------------------- /AiServer/wwwroot/img/uis/admin/api-keys.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer/wwwroot/img/uis/admin/api-keys.webp -------------------------------------------------------------------------------- /AiServer/wwwroot/img/uis/admin/background-jobs-live.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer/wwwroot/img/uis/admin/background-jobs-live.webp -------------------------------------------------------------------------------- /AiServer/wwwroot/img/uis/admin/background-jobs-queue.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer/wwwroot/img/uis/admin/background-jobs-queue.webp -------------------------------------------------------------------------------- /AiServer/wwwroot/img/uis/admin/background-jobs.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer/wwwroot/img/uis/admin/background-jobs.webp -------------------------------------------------------------------------------- /AiServer/wwwroot/img/uis/admin/dashboard.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer/wwwroot/img/uis/admin/dashboard.webp -------------------------------------------------------------------------------- /AiServer/wwwroot/img/uis/admin/logging.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer/wwwroot/img/uis/admin/logging.webp -------------------------------------------------------------------------------- /AiServer/wwwroot/img/uis/admin/media-providers-comfyui.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer/wwwroot/img/uis/admin/media-providers-comfyui.webp -------------------------------------------------------------------------------- /AiServer/wwwroot/img/uis/admin/media-providers-replicate.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer/wwwroot/img/uis/admin/media-providers-replicate.webp -------------------------------------------------------------------------------- /AiServer/wwwroot/img/uis/admin/media-providers.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer/wwwroot/img/uis/admin/media-providers.webp -------------------------------------------------------------------------------- /AiServer/wwwroot/img/uis/admin/media-types.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/AiServer/wwwroot/img/uis/admin/media-types.webp -------------------------------------------------------------------------------- /AiServer/wwwroot/lib/data/image-to-image-models.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "Default", 4 | "model": "image-to-image" 5 | }, 6 | { 7 | "id": "SDXL Lightning", 8 | "model": "sdxl-lightning" 9 | }, 10 | { 11 | "id": "SDXL Realistic", 12 | "model": "jib-mix-realistic" 13 | } 14 | ] -------------------------------------------------------------------------------- /AiServer/wwwroot/lib/data/media-types.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "Replicate", 4 | "provider": "Replicate", 5 | "website": "https://replicate.com", 6 | "apiBaseUrl": "https://api.replicate.com", 7 | "heartbeatUrl": "/", 8 | "icon": "/img/providers/replicate.svg", 9 | "apiModels": { 10 | "flux-schnell": "black-forest-labs/flux-schnell", 11 | "flux-dev": "black-forest-labs/flux-dev", 12 | "flux-pro": "black-forest-labs/flux-pro", 13 | "stable-diffusion-3": "stability-ai/stable-diffusion-3" 14 | } 15 | }, 16 | { 17 | "id": "ComfyUI", 18 | "provider": "Comfy", 19 | "website": "https://github.com/comfyanonymous/ComfyUI", 20 | "apiBaseUrl": "http://localhost:7860", 21 | "heartbeatUrl": "/", 22 | "icon": "/img/providers/comfyui.png" 23 | }, 24 | { 25 | "id": "OpenAI", 26 | "provider": "OpenAi", 27 | "website": "https://openai.com", 28 | "apiBaseUrl": "https://api.openai.com", 29 | "heartbeatUrl": "/", 30 | "icon": "/img/providers/openai.svg", 31 | "apiModels": { 32 | "dall-e-3": "dall-e-3", 33 | "text-to-speech": "tts-1:alloy", 34 | "tts-alloy": "tts-1:alloy", 35 | "tts-echo": "tts-1:echo", 36 | "tts-fable": "tts-1:fable", 37 | "tts-onyx": "tts-1:onyx", 38 | "tts-nova": "tts-1:nova", 39 | "tts-shimmer": "tts-1:shimmer", 40 | "speech-to-text": "whisper-1" 41 | } 42 | } 43 | ] -------------------------------------------------------------------------------- /AiServer/wwwroot/lib/data/tts-voices.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "Default", 4 | "model": "text-to-speech" 5 | }, 6 | { 7 | "id": "Lessac", 8 | "model": "lessac" 9 | }, 10 | { 11 | "id": "Alloy", 12 | "model": "tts-alloy" 13 | }, 14 | { 15 | "id": "Echo", 16 | "model": "tts-echo" 17 | }, 18 | { 19 | "id": "Fable", 20 | "model": "tts-fable" 21 | }, 22 | { 23 | "id": "Onyx", 24 | "model": "tts-onyx" 25 | }, 26 | { 27 | "id": "Nova", 28 | "model": "tts-nova" 29 | }, 30 | { 31 | "id": "Shimmer", 32 | "model": "tts-shimmer" 33 | } 34 | ] 35 | -------------------------------------------------------------------------------- /AiServer/wwwroot/lib/mjs/app.mjs: -------------------------------------------------------------------------------- 1 | import { map } from "@servicestack/client" 2 | import { useUtils } from "@servicestack/vue" 3 | 4 | /** @param {KeyboardEvent} e */ 5 | export function hasModifierKey(e) { 6 | return e.shiftKey || e.ctrlKey || e.altKey || e.metaKey || e.code === 'MetaLeft' || e.code === 'MetaRight' 7 | } 8 | /** Is element an Input control 9 | * @param {Element} e */ 10 | let InputTags = 'INPUT,SELECT,TEXTAREA'.split(',') 11 | export function isInput(e) { 12 | return e && InputTags.indexOf(e.tagName) >= 0 13 | } 14 | 15 | export function keydown(e, ctx) { 16 | const { unRefs } = useUtils() 17 | const { canPrev, canNext, nextSkip, take, results, selected, clearFilters } = unRefs(ctx) 18 | if (hasModifierKey(e) || isInput(e.target) || results.length === 0) return 19 | if (e.key === 'Escape') { 20 | clearFilters() 21 | return 22 | } 23 | if (e.key === 'ArrowLeft' && canPrev) { 24 | routes.to({ skip:nextSkip(-take) }) 25 | return 26 | } else if (e.key === 'ArrowRight' && canNext) { 27 | routes.to({ skip:nextSkip(take) }) 28 | return 29 | } 30 | let row = selected 31 | if (!row) return routes.to({ show:map(results[0], x => x.id) || '' }) 32 | let activeIndex = results.findIndex(x => x.id === row.id) 33 | let navs = { 34 | ArrowUp: activeIndex - 1, 35 | ArrowDown: activeIndex + 1, 36 | Home: 0, 37 | End: results.length -1, 38 | } 39 | let nextIndex = navs[e.key] 40 | if (nextIndex != null) { 41 | if (nextIndex === -1) nextIndex = results.length - 1 42 | routes.to({ show: map(results[nextIndex % results.length], x => x.id) }) 43 | if (e.key.startsWith('Arrow')) { 44 | e.preventDefault() 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /AiServer/wwwroot/lib/mjs/dart.min.js: -------------------------------------------------------------------------------- 1 | /*! `dart` grammar compiled for Highlight.js 11.10.0 */ 2 | var hljsGrammar=(()=>{"use strict";return e=>{const n={className:"subst", 3 | variants:[{begin:"\\$[A-Za-z0-9_]+"}]},a={className:"subst",variants:[{ 4 | begin:/\$\{/,end:/\}/}],keywords:"true false null this is new super"},t={ 5 | className:"string",variants:[{begin:"r'''",end:"'''"},{begin:'r"""',end:'"""'},{ 6 | begin:"r'",end:"'",illegal:"\\n"},{begin:'r"',end:'"',illegal:"\\n"},{ 7 | begin:"'''",end:"'''",contains:[e.BACKSLASH_ESCAPE,n,a]},{begin:'"""',end:'"""', 8 | contains:[e.BACKSLASH_ESCAPE,n,a]},{begin:"'",end:"'",illegal:"\\n", 9 | contains:[e.BACKSLASH_ESCAPE,n,a]},{begin:'"',end:'"',illegal:"\\n", 10 | contains:[e.BACKSLASH_ESCAPE,n,a]}]};a.contains=[e.C_NUMBER_MODE,t] 11 | ;const i=["Comparable","DateTime","Duration","Function","Iterable","Iterator","List","Map","Match","Object","Pattern","RegExp","Set","Stopwatch","String","StringBuffer","StringSink","Symbol","Type","Uri","bool","double","int","num","Element","ElementList"],r=i.map((e=>e+"?")) 12 | ;return{name:"Dart",keywords:{ 13 | keyword:["abstract","as","assert","async","await","base","break","case","catch","class","const","continue","covariant","default","deferred","do","dynamic","else","enum","export","extends","extension","external","factory","false","final","finally","for","Function","get","hide","if","implements","import","in","interface","is","late","library","mixin","new","null","on","operator","part","required","rethrow","return","sealed","set","show","static","super","switch","sync","this","throw","true","try","typedef","var","void","when","while","with","yield"], 14 | built_in:i.concat(r).concat(["Never","Null","dynamic","print","document","querySelector","querySelectorAll","window"]), 15 | $pattern:/[A-Za-z][A-Za-z0-9_]*\??/}, 16 | contains:[t,e.COMMENT(/\/\*\*(?!\/)/,/\*\//,{subLanguage:"markdown",relevance:0 17 | }),e.COMMENT(/\/{3,} ?/,/$/,{contains:[{subLanguage:"markdown",begin:".", 18 | end:"$",relevance:0}]}),e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{ 19 | className:"class",beginKeywords:"class interface",end:/\{/,excludeEnd:!0, 20 | contains:[{beginKeywords:"extends implements"},e.UNDERSCORE_TITLE_MODE] 21 | },e.C_NUMBER_MODE,{className:"meta",begin:"@[A-Za-z]+"},{begin:"=>"}]}}})() 22 | ;export default hljsGrammar; -------------------------------------------------------------------------------- /AiServer/wwwroot/mjs/components/AsciiCinema.mjs: -------------------------------------------------------------------------------- 1 | import { ref, onMounted } from "vue" 2 | 3 | export default { 4 | template:` 5 |
6 | `, 7 | props:['src','loop','poster','theme','rows'], 8 | setup(props) { 9 | const el = ref() 10 | 11 | onMounted(() => { 12 | globalThis.AsciinemaPlayer.create(props.src, el.value, props) 13 | }) 14 | 15 | return { el } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /AiServer/wwwroot/mjs/components/Features.mjs: -------------------------------------------------------------------------------- 1 | import { icons, uiLabel } from "../utils.mjs" 2 | 3 | import Chat from "./Chat.mjs" 4 | import TextToImage from "./TextToImage.mjs" 5 | import ImageToText from "./ImageToText.mjs" 6 | import ImageToImage from "./ImageToImage.mjs" 7 | import ImageUpscale from "./ImageUpscale.mjs" 8 | import SpeechToText from "./SpeechToText.mjs" 9 | import TextToSpeech from "./TextToSpeech.mjs" 10 | import ConvertImage from "./ConvertImage.mjs" 11 | import ConvertVideo from "./ConvertVideo.mjs" 12 | 13 | export const Components = { 14 | Chat, 15 | TextToImage, 16 | ImageToText, 17 | ImageToImage, 18 | ImageUpscale, 19 | SpeechToText, 20 | TextToSpeech, 21 | ConvertImage, 22 | } 23 | 24 | const F = args => { 25 | const id = Object.keys(args)[0] 26 | return { id, component:args[id] } 27 | } 28 | export const Features = (() => { 29 | const ret = { 30 | chat: { 31 | ...F({ Chat }), 32 | }, 33 | txt2img: { 34 | ...F({ TextToImage }), 35 | }, 36 | img2txt: { 37 | ...F({ ImageToText }), 38 | }, 39 | img2img: { 40 | ...F({ ImageToImage }), 41 | }, 42 | upscale: { 43 | ...F({ ImageUpscale }), 44 | }, 45 | spch2txt: { 46 | ...F({ SpeechToText }), 47 | }, 48 | txt2spch: { 49 | ...F({ TextToSpeech }), 50 | }, 51 | imgconv: { 52 | ...F({ ConvertImage }), 53 | }, 54 | vidconv: { 55 | ...F({ ConvertVideo }), 56 | }, 57 | //Transform: 'ffmpeg', 58 | } 59 | Object.keys(ret).forEach(prefix => { 60 | const feature = ret[prefix] 61 | feature.label ??= uiLabel(feature.id) 62 | feature.icon ??= icons[prefix] 63 | feature.prefix ??= prefix 64 | }) 65 | //console.log('features', ret) 66 | return ret 67 | })() 68 | 69 | export const FeatureGroups = [ 70 | { 71 | label:'Text', 72 | icon:icons.chat, 73 | features:[ 74 | Features.chat, 75 | Features.img2txt, 76 | Features.spch2txt, 77 | ], 78 | }, 79 | { 80 | label:'Image', 81 | icon:icons.txt2img, 82 | features:[ 83 | Features.txt2img, 84 | Features.img2img, 85 | Features.upscale, 86 | ] 87 | }, 88 | { 89 | label:'Audio', 90 | icon:icons.spch2txt, 91 | features:[ 92 | Features.txt2spch, 93 | ] 94 | }, 95 | { 96 | label:'Transform Image', 97 | icon:icons.imgtransform, 98 | features:[ 99 | Features.imgconv, 100 | ], 101 | }, 102 | { 103 | label:'Transform Video', 104 | icon:icons.vidtransform, 105 | features:[ 106 | Features.vidconv, 107 | ], 108 | } 109 | ] 110 | -------------------------------------------------------------------------------- /AiServer/wwwroot/mjs/components/ListenButton.mjs: -------------------------------------------------------------------------------- 1 | import { ref, watch, inject } from "vue" 2 | 3 | export default { 4 | template: ` 5 | 20 | `, 21 | emits: ['play','pause'], 22 | props: { 23 | src: String, 24 | title: String, 25 | playing: Boolean, 26 | }, 27 | setup(props, { emit }) { 28 | 29 | function toggle() { 30 | const { src, title } = props 31 | if (props.playing) { 32 | emit('pause', null) 33 | } else { 34 | emit('play', { src, title }) 35 | } 36 | } 37 | 38 | return { toggle } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /AiServer/wwwroot/mjs/components/ShellCommand.mjs: -------------------------------------------------------------------------------- 1 | import { ref } from "vue" 2 | import { map } from "@servicestack/client" 3 | 4 | export default { 5 | template:`
6 |
7 |
8 | $ 9 | 12 |
13 | sh 14 |
15 | 16 |
17 |
18 |
19 | 22 |
23 |
24 |

25 | {{ successText }} 26 |

27 |
28 |
29 |
30 | 31 |
`, 32 | props:['text'], 33 | setup(props) { 34 | let successText = ref('') 35 | /** @param {MouseEvent} e */ 36 | function copy(e) { 37 | let $el = document.createElement("input") 38 | let $lbl = e.target.parentElement.querySelector('label') 39 | $el.setAttribute("value", $lbl.innerText) 40 | document.body.appendChild($el) 41 | $el.select() 42 | document.execCommand("copy") 43 | document.body.removeChild($el) 44 | if (typeof window.getSelection == "function") { 45 | const range = document.createRange() 46 | range.selectNodeContents($lbl) 47 | map(window.getSelection(), sel => { 48 | sel.removeAllRanges() 49 | sel.addRange(range) 50 | }) 51 | } 52 | successText.value = 'copied' 53 | setTimeout(() => successText.value = '', 3000) 54 | } 55 | return { successText, copy } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /AiServer/wwwroot/mjs/components/SignIn.mjs: -------------------------------------------------------------------------------- 1 | import { inject } from "vue" 2 | import SignInForm from "./SignInForm.mjs" 3 | 4 | export default { 5 | components: { 6 | SignInForm, 7 | }, 8 | template:` 9 | 10 | `, 11 | setup() { 12 | const routes = inject('routes') 13 | 14 | return { routes } 15 | } 16 | } -------------------------------------------------------------------------------- /AiServer/wwwroot/mjs/components/SignInForm.mjs: -------------------------------------------------------------------------------- 1 | import { ref } from "vue" 2 | import { useClient, useAuth } from "@servicestack/vue" 3 | import { Authenticate } from "../dtos.mjs" 4 | import { Img } from "/mjs/utils.mjs" 5 | 6 | export default { 7 | template:` 8 |
9 | 10 |
11 |
12 |
13 |
14 |
15 |
16 | 17 | 18 |
19 |
20 |
21 |
22 |
23 | Sign In 24 |
25 |
26 |
27 |
28 | `, 29 | emits:['done'], 30 | setup(props, { emit }) { 31 | const client = useClient() 32 | const { user, signIn } = useAuth() 33 | 34 | const request = ref(new Authenticate({ 35 | provider: 'credentials', 36 | userName: localStorage.getItem('displayName') ?? '', 37 | })) 38 | 39 | async function submit() { 40 | let api = await client.api(request.value) 41 | if (api.response) { 42 | localStorage.setItem('displayName', request.value.userName) 43 | if (!localStorage.getItem('profileUrl')) { 44 | localStorage.setItem('profileUrl', Img.createSvgDataUri(request.value.userName[0].toUpperCase())) 45 | } 46 | // api = await client.api(new Authenticate()) 47 | if (api.response) { 48 | signIn(api.response) 49 | emit('done') 50 | } 51 | } 52 | } 53 | 54 | return { user, request, submit } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /AiServer/wwwroot/mjs/components/Transform.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | template: ` 3 |

Media Transform

4 | `, 5 | setup() { 6 | return {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /AiServer/wwwroot/mjs/dom.mjs: -------------------------------------------------------------------------------- 1 | import { $$, createElement } from "@servicestack/client" 2 | 3 | const svg = { 4 | clipboard: ``, 5 | check: ``, 6 | } 7 | 8 | function copyBlock(btn) { 9 | // console.log('copyBlock',btn) 10 | const label = btn.previousElementSibling 11 | const code = btn.parentElement.nextElementSibling 12 | label.classList.remove('hidden') 13 | label.innerHTML = 'copied' 14 | btn.classList.add('border-gray-600', 'bg-gray-700') 15 | btn.classList.remove('border-gray-700') 16 | btn.innerHTML = svg.check 17 | navigator.clipboard.writeText(code.innerText) 18 | setTimeout(() => { 19 | label.classList.add('hidden') 20 | label.innerHTML = '' 21 | btn.innerHTML = svg.clipboard 22 | btn.classList.remove('border-gray-600', 'bg-gray-700') 23 | btn.classList.add('border-gray-700') 24 | }, 2000) 25 | } 26 | 27 | export function addCopyButtonToCodeBlocks(sel) { 28 | globalThis.copyBlock ??= copyBlock 29 | // console.log('addCopyButtonToCodeBlocks') 30 | $$(sel).forEach(code => { 31 | let pre = code.parentElement; 32 | if (pre.classList.contains('group')) return 33 | pre.classList.add('relative', 'group') 34 | 35 | const div = createElement('div', {attrs: {className: 'opacity-0 group-hover:opacity-100 transition-opacity duration-100 flex absolute right-2 -mt-1 select-none'}}) 36 | const label = createElement('div', {attrs: {className: 'hidden font-sans p-1 px-2 mr-1 rounded-md border border-gray-600 bg-gray-700 text-gray-400'}}) 37 | const btn = createElement('button', { 38 | attrs: { 39 | className: 'p-1 rounded-md border block text-gray-500 hover:text-gray-400 border-gray-700 hover:border-gray-600', 40 | onclick: 'copyBlock(this)' 41 | } 42 | }) 43 | btn.innerHTML = svg.clipboard 44 | div.appendChild(label) 45 | div.appendChild(btn) 46 | pre.insertBefore(div, code) 47 | }) 48 | } -------------------------------------------------------------------------------- /AiServer/wwwroot/tailwind/ApiKeyDialog.mjs: -------------------------------------------------------------------------------- 1 | import { ref, inject, onMounted, computed } from 'vue' 2 | import { ApiResult } from "@servicestack/client" 3 | import { Authenticate } from "dtos" 4 | export const ApiKeyDialog = { 5 | template: ` 6 | 7 |
8 |
9 |
10 |

{{title ?? 'API Key'}}

11 | 12 | 13 |
14 |
15 |
16 | 17 |
18 |
19 | Save 20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | `, 28 | emits: ['done'], 29 | props: { 30 | title: String 31 | }, 32 | setup(props, { emit }) { 33 | const store = inject('store') 34 | const server = inject('server') 35 | const routes = inject('routes') 36 | const apikey = ref(store.apikey ?? '') 37 | 38 | const modelValue = ref(new Authenticate()) 39 | const api = ref(new ApiResult()) 40 | const errorSummary = computed(() => api.value.summaryMessage()) 41 | 42 | async function submit() { 43 | store.apikey = apikey.value 44 | emit('done') 45 | } 46 | 47 | return { 48 | store, apikey, routes, api, modelValue, errorSummary, submit 49 | } 50 | } 51 | } 52 | export default ApiKeyDialog 53 | -------------------------------------------------------------------------------- /AiServer/wwwroot/tailwind/CopyIcon.mjs: -------------------------------------------------------------------------------- 1 | import { ref } from "vue" 2 | import { useUtils } from "@servicestack/vue" 3 | const CopyIcon = { 4 | template:` 5 |
6 |
7 | 8 | 9 |
10 |
11 | `, 12 | props:['text'], 13 | setup(props) { 14 | const { copyText } = useUtils() 15 | const copied = ref(false) 16 | 17 | function copy(text) { 18 | copied.value = true 19 | copyText(text) 20 | setTimeout(() => copied.value = false, 3000) 21 | } 22 | return { copied, copy, } 23 | } 24 | } 25 | export default CopyIcon 26 | -------------------------------------------------------------------------------- /AiServer/wwwroot/tailwind/README.txt: -------------------------------------------------------------------------------- 1 | Used to include Admin UI Components in generated tailwind css -------------------------------------------------------------------------------- /AiServer/wwwroot/tailwind/sync.sh: -------------------------------------------------------------------------------- 1 | x get https://localhost:5005/modules/admin-ui/lib/metadata.mjs 2 | x get https://localhost:5005/modules/admin-ui/components/BackgroundJobs.mjs 3 | x get https://localhost:5005/modules/admin-ui/components/ApiKeys.mjs 4 | x get https://localhost:5005/modules/admin-ui/components/ManageUserApiKeys.mjs 5 | x get https://localhost:5005/js/components/CopyIcon.mjs 6 | x get https://localhost:5005/js/components/ApiKeyDialog.mjs 7 | #x get https://localhost:5005/js/core.mjs 8 | -------------------------------------------------------------------------------- /NuGet.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /config/deploy.yml: -------------------------------------------------------------------------------- 1 | # Name of your application. Used to uniquely configure containers. 2 | service: ai-server 3 | 4 | # Name of the container image. 5 | image: servicestack/ai-server 6 | 7 | # Required for use of ASP.NET Core with Kamal-Proxy. 8 | env: 9 | clear: 10 | ASPNETCORE_FORWARDEDHEADERS_ENABLED: true 11 | HTTPS_METHOD: noredirect 12 | secret: 13 | - GOOGLE_API_KEY 14 | - GROQ_API_KEY 15 | - MISTRAL_API_KEY 16 | - OPENAI_API_KEY 17 | - OPENROUTER_API_KEY 18 | 19 | # Deploy to these servers. 20 | servers: 21 | # IP address of server, optionally use env variable. 22 | web: 23 | - 5.78.128.205 24 | # - <%= ENV['KAMAL_DEPLOY_IP'] %> 25 | 26 | 27 | # Enable SSL auto certification via Let's Encrypt (and allow for multiple apps on one server). 28 | # If using something like Cloudflare, it is recommended to set encryption mode 29 | # in Cloudflare's SSL/TLS setting to "Full" to enable end-to-end encryption. 30 | proxy: 31 | ssl: true 32 | hosts: 33 | - openai.servicestack.net 34 | - ai-server-cdn.diffusion.works 35 | # kamal-proxy connects to your container over port 80, use `app_port` to specify a different port. 36 | app_port: 8080 37 | 38 | # Credentials for your image host. 39 | registry: 40 | # Specify the registry server, if you're not using Docker Hub 41 | server: ghcr.io 42 | username: 43 | - KAMAL_REGISTRY_USERNAME 44 | 45 | # Always use an access token rather than real password (pulled from .kamal/secrets). 46 | password: 47 | - KAMAL_REGISTRY_PASSWORD 48 | 49 | # Configure builder setup. 50 | builder: 51 | arch: amd64 52 | 53 | volumes: 54 | - "/opt/docker/ai-server/App_Data:/app/App_Data" 55 | - "/mnt/HC_Volume_101725579/ai-server/files:/app/files" 56 | - "/mnt/HC_Volume_101725579/ai-server/artifacts:/app/artifacts" 57 | 58 | accessories: 59 | litestream: 60 | roles: ["web"] 61 | image: litestream/litestream 62 | files: ["config/litestream.yml:/etc/litestream.yml"] 63 | volumes: ["/opt/docker/ai-server/App_Data:/data"] 64 | cmd: replicate 65 | env: 66 | secret: 67 | - R2_ACCESS_KEY_ID 68 | - R2_SECRET_ACCESS_KEY 69 | -------------------------------------------------------------------------------- /config/litestream.yml: -------------------------------------------------------------------------------- 1 | access-key-id: $R2_ACCESS_KEY_ID 2 | secret-access-key: $R2_SECRET_ACCESS_KEY 3 | 4 | dbs: 5 | - path: /data/app.db 6 | replicas: 7 | - type: s3 8 | bucket: ai-server 9 | path: app.db 10 | region: auto 11 | endpoint: https://b95f38ca3a6ac31ea582cd624e6eb385.r2.cloudflarestorage.com 12 | 13 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | networks: 2 | ai-services: 3 | external: true 4 | 5 | services: 6 | app: 7 | image: servicestack/ai-server 8 | container_name: ai-server 9 | networks: 10 | - ai-services 11 | ports: 12 | - "${PORT:-5006}:8080" 13 | env_file: ".env" 14 | volumes: 15 | - ./App_Data:/app/App_Data 16 | depends_on: 17 | app-migration: 18 | condition: service_completed_successfully 19 | 20 | app-migration: 21 | image: servicestack/ai-server 22 | container_name: ai-server-migration 23 | env_file: ".env" 24 | command: --AppTasks=migrate 25 | depends_on: 26 | - app-fix-permissions 27 | volumes: 28 | - ./App_Data:/app/App_Data 29 | healthcheck: 30 | test: ["CMD-SHELL", "[ -f /app/App_Data/migration_complete ]"] 31 | interval: 10s 32 | timeout: 5s 33 | retries: 5 34 | 35 | app-fix-permissions: 36 | container_name: ai-server-fix-permissions 37 | user: "root" 38 | image: servicestack/ai-server 39 | entrypoint: ["/bin/sh", "-c", "chown -R 1654:1654 ./App_Data"] 40 | volumes: 41 | - ./App_Data:/app/App_Data 42 | healthcheck: 43 | test: ["CMD-SHELL", "[ -f /app/App_Data/fixed_permissions ]"] 44 | interval: 10s 45 | timeout: 5s 46 | retries: 5 47 | -------------------------------------------------------------------------------- /example.env: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY=your-openai-api-key 2 | GOOGLE_API_KEY=your-google-api-key 3 | OPENROUTER_API_KEY=your-openrouter-api-key 4 | MISTRAL_API_KEY=your-mistral-api-key 5 | GROQ_API_KEY=your-groq-api-key 6 | PORT=5005 7 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | ServiceStack 2 | Copyright (c) 2013-present, ServiceStack 3 | =============================================================================== 4 | 5 | This program is free software: you can redistribute it and/or modify it 6 | under the terms of the GNU Affero General Public License as published by the 7 | Free Software Foundation, either version 3 of the License, see 8 | http://www.gnu.org/licenses/agpl-3.0.html. 9 | 10 | This program is distributed in the hope that it will be useful, but WITHOUT 11 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 13 | 14 | -------------------------------------------------------------------------------- /public/text2img/sdxl_lightning_samples.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServiceStack/ai-server/291dc551a4c7c756207a75eeea3acf65a097605b/public/text2img/sdxl_lightning_samples.jpg --------------------------------------------------------------------------------